From f82ecd92bbdb0c522dd2c11a3c7d0fbbdeb1e2c4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 22 Jul 2026 19:33:53 +0000 Subject: [PATCH] fix(cli): allow cached managed reinstalls Co-Authored-By: Paperclip --- cli/src/__tests__/install-command.test.ts | 21 +++++++++++++++++++++ cli/src/install-store.ts | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cli/src/__tests__/install-command.test.ts b/cli/src/__tests__/install-command.test.ts index 9da98708dac..13335bb2161 100644 --- a/cli/src/__tests__/install-command.test.ts +++ b/cli/src/__tests__/install-command.test.ts @@ -85,6 +85,27 @@ describe("managed install commands", () => { expect(runCommand.mock.calls[0]?.[0]).toBe(process.execPath); }); + it("installs a GitHub branch through codeload and reuses the resolved SHA", async () => { + const sha = "c".repeat(40); + const runCommand = vi.fn(async (file: string, args: string[]) => { + if (file === "curl" && !args.includes("--output")) return { stdout: JSON.stringify({ sha }), stderr: "" }; + if (file === "curl") { fs.writeFileSync(args[args.indexOf("--output") + 1], "archive"); return { stdout: "", stderr: "" }; } + if (file === "tar") { const checkout = args[args.indexOf("-C") + 1]; fs.mkdirSync(path.join(checkout, "cli"), { recursive: true }); fs.writeFileSync(path.join(checkout, "cli", "package.json"), JSON.stringify({ version: "0.3.1" })); return { stdout: "", stderr: "" }; } + if (file === "corepack" || file === "bash") return { stdout: "", stderr: "" }; + if (file === "npm" && args[0] === "pack") { fs.writeFileSync(path.join(args[args.indexOf("--pack-destination") + 1], "paperclipai-0.3.1.tgz"), "package"); return { stdout: "", stderr: "" }; } + if (file === "npm" && args[0] === "install") { const prefix = args[args.indexOf("--prefix") + 1]; const packageRoot = path.join(prefix, "node_modules", "paperclipai"); fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true }); fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({ version: "0.3.1" })); fs.writeFileSync(path.join(packageRoot, "dist", "index.js"), "#!/usr/bin/env node\n"); return { stdout: "", stderr: "" }; } + if (file === process.execPath) return { stdout: "0.3.1\n", stderr: "" }; + throw new Error(`Unexpected command: ${file} ${args.join(" ")}`); + }); + await installCommand({ ref: "master", repo: "HenkDz/paperclip" }, { runCommand }); + await installCommand({ ref: "master", repo: "HenkDz/paperclip" }, { runCommand }); + const manifest = readInstallManifest(resolveInstallStorePaths()); + expect(manifest).toMatchObject({ source: "git", repo: "HenkDz/paperclip", ref: "master", sha }); + expect(manifest?.payloadPath).toContain(path.join("git", sha.slice(0, 12))); + expect(runCommand.mock.calls.filter(([command, args]) => command === "curl" && args.includes("--output"))).toHaveLength(1); + expect(runCommand.mock.calls.filter(([command]) => command === "corepack")).toHaveLength(1); + }); + it("installs through the shim, reports provenance, and uninstalls without deleting user data", async () => { const version = "2026.720.0"; const runCommand = vi.fn(async (file: string, args: string[], _options?: unknown) => { diff --git a/cli/src/install-store.ts b/cli/src/install-store.ts index d4bb75cb657..c74b3f18ded 100644 --- a/cli/src/install-store.ts +++ b/cli/src/install-store.ts @@ -381,8 +381,8 @@ export function writeManagedShim(paths = resolveInstallStorePaths()): void { const homeDir = path.dirname(path.dirname(path.dirname(paths.shimPath))); const localDir = path.dirname(path.dirname(paths.shimPath)); fs.mkdirSync(homeDir, { recursive: true, mode: 0o700 }); - fs.mkdirSync(localDir, { mode: 0o755 }); - fs.mkdirSync(path.dirname(paths.shimPath), { mode: 0o755 }); + fs.mkdirSync(localDir, { recursive: true, mode: 0o755 }); + fs.mkdirSync(path.dirname(paths.shimPath), { recursive: true, mode: 0o755 }); assertManagedShimWritable(paths); const entrypoint = path.join(paths.currentPath, "node_modules", "paperclipai", "dist", "index.js"); const contents = `#!/bin/sh\n# ${MANAGED_SHIM_MARKER}\nset -eu\nexec ${shellQuote(process.execPath)} ${shellQuote(entrypoint)} "\$@"\n`;