diff --git a/cli/src/__tests__/install-command.test.ts b/cli/src/__tests__/install-command.test.ts index a266b4f0e78..38845fe5473 100644 --- a/cli/src/__tests__/install-command.test.ts +++ b/cli/src/__tests__/install-command.test.ts @@ -107,6 +107,33 @@ describe("managed install commands", () => { expect(fs.readFileSync(userData, "utf8")).toBe("keep"); }); + it("preserves the managed install when an existing systemd unit cannot be checked", async () => { + const paths = resolveInstallStorePaths(); + fs.mkdirSync(path.dirname(paths.shimPath), { recursive: true }); + fs.writeFileSync(paths.shimPath, "managed shim"); + const unitPath = path.join( + process.env.HOME!, + ".config", + "systemd", + "user", + "paperclipai.service", + ); + fs.mkdirSync(path.dirname(unitPath), { recursive: true }); + fs.writeFileSync(unitPath, "unit"); + + await expect(uninstallCommand({ + detectServiceManager: vi.fn(async () => ({ + supported: false as const, + reason: "No usable systemd user manager was detected", + })), + platform: "linux", + userHomeDir: process.env.HOME!, + })).rejects.toThrow("Cannot verify or remove the background service"); + + expect(fs.existsSync(paths.shimPath)).toBe(true); + expect(fs.existsSync(unitPath)).toBe(true); + }); + it("rejects a symlinked installs root before npm writes outside the store", async () => { const paths = resolveInstallStorePaths(); const outside = path.join(root, "outside"); diff --git a/cli/src/commands/uninstall.ts b/cli/src/commands/uninstall.ts index 992af2cb584..1c2764afc7f 100644 --- a/cli/src/commands/uninstall.ts +++ b/cli/src/commands/uninstall.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import pc from "picocolors"; import { @@ -9,17 +10,36 @@ import { withInstallStoreLock, } from "../install-store.js"; import { resolvePaperclipInstanceId } from "../config/home.js"; -import { detectServiceManager } from "../services/service-manager.js"; +import { detectServiceManager, systemdServiceName } from "../services/service-manager.js"; type UninstallDependencies = { detectServiceManager: typeof detectServiceManager; + platform: NodeJS.Platform; + userHomeDir: string; }; export async function uninstallCommand( dependencies: Partial = {}, ): Promise { + const instanceId = resolvePaperclipInstanceId(); const detect = dependencies.detectServiceManager ?? detectServiceManager; - const detection = await detect({ instanceId: resolvePaperclipInstanceId() }); + const platform = dependencies.platform ?? process.platform; + const userHomeDir = dependencies.userHomeDir ?? os.homedir(); + const detection = await detect({ instanceId, platform }); + if (!detection.supported && platform === "linux") { + const definitionPath = path.join( + userHomeDir, + ".config", + "systemd", + "user", + systemdServiceName(instanceId), + ); + if (fs.existsSync(definitionPath)) { + throw new Error( + `Cannot verify or remove the background service: ${detection.reason}. Retry when the service manager is available.`, + ); + } + } if (detection.supported) { const status = await detection.manager.status(); if (status.installed || status.active) await detection.manager.uninstall();