From 3eafbb13a34fb804b51dc32d46bb9a53068c6fd5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 22 Jul 2026 19:18:59 +0000 Subject: [PATCH] fix(installer): harden dry runs and unit escaping Co-Authored-By: Paperclip --- cli/src/__tests__/service-manager.test.ts | 11 +++++++++++ cli/src/services/service-manager.ts | 6 +++++- scripts/install.sh | 7 ++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cli/src/__tests__/service-manager.test.ts b/cli/src/__tests__/service-manager.test.ts index 37775609072..f1fa153b81b 100644 --- a/cli/src/__tests__/service-manager.test.ts +++ b/cli/src/__tests__/service-manager.test.ts @@ -37,6 +37,17 @@ describe("service definition generation", () => { expect(unit).not.toContain("API_KEY"); }); + it("escapes systemd variable and specifier expansion in configured values", () => { + const unit = renderSystemdUnit({ + instanceId: "team-$USER-%i", + shimPath: "/home/$USER/%i/paperclipai", + homeDir: "/home/$USER/%i/.paperclip", + }); + + expect(unit).toContain('ExecStart="/home/$$USER/%%i/paperclipai" run --instance "team-$$USER-%%i"'); + expect(unit).toContain('Environment="PAPERCLIP_HOME=/home/$$USER/%%i/.paperclip"'); + }); + it("generates a launchd agent with keepalive and instance logs", () => { const plist = renderLaunchdPlist({ instanceId: "team-a", shimPath: "/Users/alice/.local/bin/paperclipai", homeDir: "/Users/alice/.paperclip", stdoutPath: "/Users/alice/.paperclip/instances/team-a/logs/service.log", stderrPath: "/Users/alice/.paperclip/instances/team-a/logs/service.err.log" }); expect(plist).toContain("ing.paperclip.paperclipai.team-a"); diff --git a/cli/src/services/service-manager.ts b/cli/src/services/service-manager.ts index 7b010832305..2fe8a0d86a8 100644 --- a/cli/src/services/service-manager.ts +++ b/cli/src/services/service-manager.ts @@ -53,7 +53,11 @@ export const defaultCommandRunner: CommandRunner = async (command, args, options }; function escapeSystemd(value: string): string { - return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); + return value + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"') + .replaceAll("$", () => "$$") + .replaceAll("%", "%%"); } function escapeXml(value: string): string { diff --git a/scripts/install.sh b/scripts/install.sh index beca007bd9b..501c914de3a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -354,15 +354,16 @@ INSTALL_ARGS=(install) [ "$CANARY" = "1" ] && INSTALL_ARGS+=(--canary) [ -n "$VERSION" ] && INSTALL_ARGS+=(--version "$VERSION") [ "$NO_PROMPT" = "1" ] && INSTALL_ARGS+=(--yes) +INSTALL_COMMAND=(npx --yes "$PACKAGE_SPEC" "${INSTALL_ARGS[@]}") log "Delegating to the Paperclip CLI" -print_command npx --yes "$PACKAGE_SPEC" "${INSTALL_ARGS[@]}" - if [ "$DRY_RUN" = "1" ]; then + print_command "${INSTALL_COMMAND[@]}" exit 0 fi -npx --yes "$PACKAGE_SPEC" "${INSTALL_ARGS[@]}" +print_command "${INSTALL_COMMAND[@]}" +"${INSTALL_COMMAND[@]}" if [ "$INSTALL_SERVICE" = "1" ]; then log "Installing the Paperclip service"