diff --git a/src/lib/actions/uninstall/run-plan.test.ts b/src/lib/actions/uninstall/run-plan.test.ts index f6db340fcd4..a28fce99625 100644 --- a/src/lib/actions/uninstall/run-plan.test.ts +++ b/src/lib/actions/uninstall/run-plan.test.ts @@ -60,6 +60,43 @@ describe("uninstall run plan", () => { ); }); + it("removes the nemohermes shim when it is an installer-managed symlink (#6098)", () => { + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-hermes-shim-")); + const userBin = path.join(tmpHome, ".local", "bin"); + fs.mkdirSync(userBin, { recursive: true }); + const nemohermsShimPath = path.join(userBin, "nemohermes"); + fs.symlinkSync("/dev/null", nemohermsShimPath); + + const removed: string[] = []; + try { + runUninstallPlan( + { assumeYes: true, deleteModels: false, keepOpenShell: true }, + { + commandExists: () => false, + env: { + HOME: tmpHome, + NEMOCLAW_AGENT: "hermes", + TMPDIR: tmpHome, + } as NodeJS.ProcessEnv, + existsSync: (target) => fs.existsSync(target), + isTty: false, + log: () => {}, + rmSync: vi.fn((target: fs.PathLike, opts?: fs.RmOptions) => { + removed.push(String(target)); + fs.rmSync(target, opts); + }), + run: vi.fn(() => ok()), + runDocker: () => ok(""), + }, + ); + + expect(removed).toContain(nemohermsShimPath); + expect(fs.existsSync(nemohermsShimPath)).toBe(false); + } finally { + fs.rmSync(tmpHome, { recursive: true, force: true }); + } + }); + it("applies a non-destructive uninstall run with fake tools", () => { const logs: string[] = []; const run = vi.fn((_command: string, args: string[]) => { diff --git a/src/lib/actions/uninstall/run-plan.ts b/src/lib/actions/uninstall/run-plan.ts index e21c9bde1db..9b9ab8510a5 100644 --- a/src/lib/actions/uninstall/run-plan.ts +++ b/src/lib/actions/uninstall/run-plan.ts @@ -686,6 +686,13 @@ function removeNemoclawCli(paths: UninstallPaths, runtime: UninstallRuntime): vo `Leaving ${paths.nemoclawShimPath} in place because it is not an installer-managed shim.`, ); } + const agentShim = classifyShimPath(paths.nemohermsShimPath); + if (agentShim.remove) removePath(paths.nemohermsShimPath, runtime); + else if (agentShim.kind === "preserve-foreign-file") { + runtime.warn( + `Leaving ${paths.nemohermsShimPath} in place because it is not an installer-managed shim.`, + ); + } removeNvmLeftovers(paths, runtime); removeAliases(paths, runtime); } diff --git a/src/lib/domain/uninstall/paths.test.ts b/src/lib/domain/uninstall/paths.test.ts index 77b371e026b..d5118618e18 100644 --- a/src/lib/domain/uninstall/paths.test.ts +++ b/src/lib/domain/uninstall/paths.test.ts @@ -27,6 +27,7 @@ describe("uninstall paths", () => { expect(paths.openshellConfigDir).toBe(path.join("/home/test", ".config", "openshell")); expect(paths.nemoclawConfigDir).toBe(path.join("/home/test", ".config", "nemoclaw")); expect(paths.nemoclawShimPath).toBe(path.join("/home/test", ".local", "bin", "nemoclaw")); + expect(paths.nemohermsShimPath).toBe(path.join("/home/test", ".local", "bin", "nemohermes")); expect(paths.openshellInstallPaths).toEqual([ ...OPENSHELL_MANAGED_BINARIES.map((binary) => path.join("/usr/local/bin", binary)), ...OPENSHELL_MANAGED_BINARIES.map((binary) => path.join("/xdg/bin", binary)), diff --git a/src/lib/domain/uninstall/paths.ts b/src/lib/domain/uninstall/paths.ts index c2a1adaf3b1..33bbacc8719 100644 --- a/src/lib/domain/uninstall/paths.ts +++ b/src/lib/domain/uninstall/paths.ts @@ -31,6 +31,7 @@ export interface UninstallPaths { managedSwapMarkerPath: string; nemoclawConfigDir: string; nemoclawShimPath: string; + nemohermsShimPath: string; nemoclawStateDir: string; gatewayLocalStateDir: string; openshellConfigDir: string; @@ -59,6 +60,7 @@ export function defaultUninstallPaths(options: UninstallPathOptions): UninstallP managedSwapMarkerPath: path.join(options.home, ".nemoclaw", "managed_swap"), nemoclawConfigDir: path.join(options.home, ".config", "nemoclaw"), nemoclawShimPath: path.join(options.home, ".local", "bin", "nemoclaw"), + nemohermsShimPath: path.join(options.home, ".local", "bin", "nemohermes"), nemoclawStateDir: path.join(options.home, ".nemoclaw"), gatewayLocalStateDir: path.join(options.home, ".local", "state", "nemoclaw"), openshellConfigDir: path.join(options.home, ".config", "openshell"),