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:
co-authored by
Claude Fable 5
parent
47823944a3
commit
40aa092f5b
+60
-1
@@ -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.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user