fix(service): make managed installs start reliably

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-27 15:32:13 +00:00
1 parent 8925d3f38a
commit c9559a5d90
10 files changed
+88 -45

No files matched your search

+11 -2
View File
@@ -93,7 +93,15 @@ describe("managed install commands", () => {
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 === "corepack") {
if (args.includes("pack")) {
const destination = args[args.indexOf("--pack-destination") + 1];
const packageName = args.includes("packages/db") ? "paperclipai-db" : "paperclipai-server";
fs.writeFileSync(path.join(destination, `${packageName}-0.3.1.tgz`), "package");
}
return { stdout: "", stderr: "" };
}
if (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: "" };
@@ -105,7 +113,8 @@ describe("managed install commands", () => {
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);
expect(runCommand.mock.calls.filter(([command, args]) => command === "corepack" && args[1] === "install")).toHaveLength(1);
expect(runCommand.mock.calls.filter(([command, args]) => command === "corepack" && args.includes("pack"))).toHaveLength(2);
});
it("installs through the shim, reports provenance, and uninstalls without deleting user data", async () => {
@@ -12,15 +12,19 @@ const config = {
} as PaperclipConfig;
let previousPaperclipHome: string | undefined;
let previousServiceManaged: string | undefined;
beforeEach(() => {
previousPaperclipHome = process.env.PAPERCLIP_HOME;
previousServiceManaged = process.env.PAPERCLIP_SERVICE_MANAGED;
process.env.PAPERCLIP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-service-restart-"));
});
afterEach(() => {
if (previousPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
else process.env.PAPERCLIP_HOME = previousPaperclipHome;
if (previousServiceManaged === undefined) delete process.env.PAPERCLIP_SERVICE_MANAGED;
else process.env.PAPERCLIP_SERVICE_MANAGED = previousServiceManaged;
});
function managerFixture(active = true) {
@@ -52,6 +56,15 @@ function managerFixture(active = true) {
}
describe("service health doctor checks", () => {
it("skips live service checks during the managed unit's own activation", async () => {
process.env.PAPERCLIP_SERVICE_MANAGED = "1";
const detect = vi.fn();
const probe = vi.fn();
await expect(serviceHealthChecks(config, { detect, probe })).resolves.toEqual([]);
expect(detect).not.toHaveBeenCalled();
expect(probe).not.toHaveBeenCalled();
});
it("skips exact version matching unless a restart version is explicit", () => {
expect(resolveRestartExpectedVersion(null)).toBeNull();
expect(resolveRestartExpectedVersion(undefined)).toBeNull();
+2
View File
@@ -40,6 +40,8 @@ export async function serviceHealthChecks(
config: PaperclipConfig,
dependencies: Partial<ServiceCheckDependencies> = {},
): Promise<CheckResult[]> {
if (process.env.PAPERCLIP_SERVICE_MANAGED === "1") return [];
const deps: ServiceCheckDependencies = {
detect: (instanceId) => detectServiceManager({ instanceId }),
probe: probeHealth,
+10 -3
View File
@@ -185,11 +185,18 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C
await runCommand("tar", ["-xzf", archivePath, "--strip-components=1", "-C", checkoutPath], { maxBuffer: 4 * 1024 * 1024 });
await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
await runCommand("bash", ["scripts/build-npm.sh", "--skip-checks"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
await runCommand("corepack", ["pnpm", "--filter", "@paperclipai/db", "build"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
await runCommand("corepack", ["pnpm", "--filter", "@paperclipai/server", "build"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
const metadata = JSON.parse(fs.readFileSync(path.join(checkoutPath, "cli", "package.json"), "utf8")) as { version: string };
await runCommand("corepack", ["pnpm", "--dir", "packages/db", "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, maxBuffer: 16 * 1024 * 1024 });
await runCommand("corepack", ["pnpm", "--dir", "server", "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, env: { ...process.env, PAPERCLIP_RELEASE_REUSE_UI_DIST: "1" }, maxBuffer: 32 * 1024 * 1024 });
await runCommand("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), maxBuffer: 16 * 1024 * 1024 });
const tarball = fs.readdirSync(stagingRoot).find((entry) => entry.endsWith(".tgz"));
if (!tarball) throw new Error("npm pack did not produce a paperclipai tarball.");
await runCommand("npm", ["install", "--prefix", stagedPayload, path.join(stagingRoot, tarball), "--no-audit", "--no-fund"], { cwd: stagingRoot, maxBuffer: 32 * 1024 * 1024 });
const tarballs = fs.readdirSync(stagingRoot).filter((entry) => entry.endsWith(".tgz"));
const cliTarball = tarballs.find((entry) => /^paperclipai-(?!db-|server-)/.test(entry));
const dbTarball = tarballs.find((entry) => entry.startsWith("paperclipai-db-"));
const serverTarball = tarballs.find((entry) => entry.startsWith("paperclipai-server-"));
if (!cliTarball || !dbTarball || !serverTarball) throw new Error("Git install packaging did not produce paperclipai, @paperclipai/db, and @paperclipai/server tarballs.");
await runCommand("npm", ["install", "--prefix", stagedPayload, path.join(stagingRoot, cliTarball), path.join(stagingRoot, dbTarball), path.join(stagingRoot, serverTarball), "--no-audit", "--no-fund"], { cwd: stagingRoot, maxBuffer: 32 * 1024 * 1024 });
await smokePayload(stagedPayload, metadata.version, runCommand);
fs.renameSync(stagedPayload, payloadPath);
return { payloadPath, reused: false, version: metadata.version };