Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/lib/actions/uninstall/run-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) => {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/domain/uninstall/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
2 changes: 2 additions & 0 deletions src/lib/domain/uninstall/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface UninstallPaths {
managedSwapMarkerPath: string;
nemoclawConfigDir: string;
nemoclawShimPath: string;
nemohermsShimPath: string;
nemoclawStateDir: string;
gatewayLocalStateDir: string;
openshellConfigDir: string;
Expand Down Expand Up @@ -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"),
Expand Down
Loading