fix(uninstall): remove the CLI binaries too, not just agent configs (#1254)

* fix(uninstall): remove the CLI binaries too, not just agent configs (#1071)

`codegraph uninstall` swept agent configurations and stopped — every
installed binary stayed behind, so `codegraph` still ran afterward. Three
disconnected paths each removed a fraction of an installation (uninstall:
configs; install.sh --uninstall: the bundle; npm preuninstall: configs +
npm's own package), and none cleared a shadowed second install — the
uninstall edition of the #1071 PATH shadow.

The uninstall now PLANS every install present on the machine — the bundle
layout(s) (running binary's own, the platform default, a custom
CODEGRAPH_INSTALL_DIR), the npm global package (found by asking
`npm root -g`, so nvm/fnm/volta prefixes resolve correctly), and the
bin-dir launcher link (only when it verifiably points into a detected
install) — confirms with the user, then removes them all. `--yes` skips
the prompt; the new `--keep-cli` flag keeps the old configs-only behavior.

Safety rules: a source checkout is reported, never deleted; a
project-local npm install is left to the project; on unix the default
install dir doubles as the machine state dir, so only the install
artifacts (versions/, current) are removed there — telemetry choice and
daemon records survive. Windows can't delete a running exe but can rename
it (the in-place upgrade's trick): a locked node.exe is renamed aside and
surfaced as a one-file leftover instead of failing the removal, and npm
is routed through cmd.exe (a direct .cmd spawn EINVALs on modern Node).

Planner/executor are split with injected side effects (the upgrade
orchestrator's convention) and unit-tested across the shadow case,
state-dir preservation, custom dirs, foreign-shim protection, and the
locked-exe dance; validated end-to-end on macOS against a fake HOME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): key path math on the target platform, not the host

Real-Windows validation caught it: the planner/executor used the host
path module, so win32 fixtures were meaningless on a POSIX host and
POSIX fixtures failed on the Windows VM. Same convention as
detectInstallMethod now — path.win32/path.posix chosen by the injected
platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(upgrade): route npm through cmd.exe on Windows — a direct npm.cmd spawn EINVALs on modern Node

Found while validating the uninstall change on the Windows VM: upgradeNpm
spawned npm.cmd without a shell, which every current Node rejects with
EINVAL (the CVE-2024-27980 hardening) — so `codegraph upgrade` on a
Windows npm install failed before doing anything. Verified live on the VM:
spawnSync('npm.cmd') → EINVAL; cmd.exe /d /s /c npm → works.

npmInvocation moves into the upgrade orchestrator (remove-binary imports
it from there — same direction as its existing imports, no cycle), and the
win32 test now pins the WORKING invocation instead of the broken one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-10 17:37:20 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 47823944a3
commit 40aa092f5b
8 changed files with 625 additions and 8 deletions
+60 -1
View File
@@ -300,6 +300,13 @@ export interface RunUninstallerOptions {
location?: Location;
/** Non-interactive: location=global, target=all, no prompts. */
yes?: boolean;
/** Remove agent configs only — leave the CLI binary installed. */
keepCli?: boolean;
/**
* `__filename` of the CLI entry (dist/bin/codegraph.js) — install-method
* detection is keyed off the running binary's real location.
*/
cliFilename?: string;
}
export type UninstallStatus = 'removed' | 'not-configured' | 'unsupported';
@@ -497,6 +504,55 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
clack.log.info(`The ${codeGraphDirName()}/ index for this project is still here. Run \`codegraph uninit\` to delete it.`);
}
// Step 4b: the CLI binary itself (global uninstall only — a project-scoped
// uninstall must not touch the machine-wide install). Before this step,
// `codegraph uninstall` removed agent configs but left every installed
// binary — bundle AND npm global — so `codegraph` still resolved afterward
// (the #1071 shadow, uninstall edition). Plan every install present on the
// machine, confirm, then remove them all. Skippable with --keep-cli.
let cliRemoved = false;
if (location === 'global' && opts.keepCli !== true && opts.cliFilename) {
const { planBinaryRemoval, executeBinaryRemoval, defaultProbes } =
await import('../upgrade/remove-binary');
const plan = planBinaryRemoval(defaultProbes(opts.cliFilename));
if (plan.sourceRoot) {
clack.log.info(`Running from a source checkout (${tildify(plan.sourceRoot)}) — leaving it untouched.`);
}
if (plan.summary.length > 0) {
let removeBinaries = useDefaults;
if (!useDefaults) {
const sel = await clack.confirm({
message: `Also remove the CodeGraph CLI from this machine?\n${plan.summary.map((s) => ` - ${s}`).join('\n')}`,
initialValue: true,
});
if (clack.isCancel(sel)) {
clack.cancel('Uninstall cancelled.');
process.exit(0);
}
removeBinaries = sel;
}
if (removeBinaries) {
const result = executeBinaryRemoval(plan);
for (const p of result.removed) clack.log.success(`Removed ${tildify(p)}`);
if (result.npm === 'removed') {
clack.log.success('Removed the npm global package (npm uninstall -g).');
} else if (result.npm === 'failed') {
clack.log.warn('npm uninstall failed — run `npm uninstall -g @colbymchenry/codegraph` yourself (EACCES usually means it needs sudo).');
}
for (const p of result.leftovers) {
clack.log.warn(`Could not remove ${tildify(p)} — delete it manually${process.platform === 'win32' ? ' after this window closes' : ''}.`);
}
cliRemoved = result.removed.length > 0 || result.npm === 'removed';
if (cliRemoved && process.platform === 'win32') {
clack.log.info('If your PATH still lists a codegraph bin directory, remove that entry from your user PATH.');
}
} else {
clack.log.info('Kept the CLI. Remove it later with `codegraph uninstall` or `npm uninstall -g @colbymchenry/codegraph`.');
}
}
}
// Telemetry churn signal (agent IDs only) — flush now, since after an
// uninstall there is usually no "next run" to deliver it.
if (removed.length > 0) {
@@ -505,12 +561,15 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
}
// Step 5: summary.
const cliNote = cliRemoved ? ' The CLI is removed too — this was its last run.' : '';
if (removed.length > 0) {
const names = removed.map((r) => r.displayName).join(', ');
clack.outro(
`Removed CodeGraph from ${removed.length} agent${removed.length > 1 ? 's' : ''}: ${names}. ` +
`Restart ${removed.length > 1 ? 'them' : 'it'} to apply.`,
`Restart ${removed.length > 1 ? 'them' : 'it'} to apply.` + cliNote,
);
} else if (cliRemoved) {
clack.outro(`No ${location} agent had CodeGraph configured.` + cliNote);
} else {
clack.outro(`CodeGraph was not configured in any ${location} agent — nothing to remove.`);
}