feat(installer): add codegraph uninstall command (#313) (#318)

Adds a cross-channel uninstall that removes CodeGraph from every agent it's
configured on (Claude Code, Cursor, Codex CLI, opencode, Hermes). Prompts
global-vs-local up front (no flags required) and reports which providers it
actually hit; --location / --target / --yes supported for non-interactive use.
Removes only what install wrote; leaves the .codegraph/ index to `uninit`.

Also fixes Cursor uninstall leaving an orphaned .cursor/rules/codegraph.mdc
(its description: CodeGraph frontmatter lingered); the dedicated rules file is
now deleted outright while user content outside our markers is preserved.

Validated end-to-end on macOS and Docker Linux (global + local sweeps clean).
Adds 8 tests; full suite 730 passing. Bumps to 0.9.3 with CHANGELOG entry.

Resolves #313.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-22 04:51:42 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 77e29c436c
commit bf73f4d05c
7 changed files with 435 additions and 8 deletions
+54 -4
View File
@@ -46,7 +46,6 @@ import {
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
removeMarkedSection,
replaceOrAppendMarkedSection,
writeJsonFile,
} from './shared';
@@ -140,9 +139,7 @@ class CursorTarget implements AgentTarget {
}
if (loc === 'local') {
const rules = rulesPath();
const action = removeMarkedSection(rules, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
files.push({ path: rules, action });
files.push(removeRulesEntry());
}
return { files };
@@ -237,4 +234,57 @@ function writeRulesEntry(): WriteResult['files'][number] {
return { path: file, action: mapped };
}
/**
* Remove the Cursor rules file on uninstall.
*
* Unlike the shared CLAUDE.md / AGENTS.md files (where codegraph owns
* only a marker-delimited section), `.cursor/rules/codegraph.mdc` is a
* file we create OUTRIGHT — the frontmatter is ours too. So a plain
* `removeMarkedSection` is wrong here: it would strip our instruction
* block but leave the orphaned `description: CodeGraph ...` frontmatter
* behind, so the file lingers and still "mentions" codegraph.
*
* Instead: strip our block, and if nothing but our own frontmatter
* remains, delete the whole file. Only when the user has added their
* own content outside our markers do we keep the file (minus our block).
*/
function removeRulesEntry(): WriteResult['files'][number] {
const file = rulesPath();
if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
let content: string;
try {
content = fs.readFileSync(file, 'utf-8');
} catch {
return { path: file, action: 'not-found' };
}
const ourFrontmatter = MDC_FRONTMATTER.trim();
const startIdx = content.indexOf(CODEGRAPH_SECTION_START);
const endIdx = content.indexOf(CODEGRAPH_SECTION_END);
// Our marked block is present — strip it, then decide what's left.
if (startIdx !== -1 && endIdx > startIdx) {
const before = content.substring(0, startIdx).trimEnd();
const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length).trimStart();
const remainder = (before + (before && after ? '\n\n' : '') + after).trim();
if (remainder === '' || remainder === ourFrontmatter) {
try { fs.unlinkSync(file); } catch { /* ignore */ }
} else {
atomicWriteFileSync(file, remainder + '\n');
}
return { path: file, action: 'removed' };
}
// No block, but the file is still our pristine frontmatter-only file
// — it's ours, so remove it.
if (content.trim() === ourFrontmatter) {
try { fs.unlinkSync(file); } catch { /* ignore */ }
return { path: file, action: 'removed' };
}
// Foreign content we don't recognize — leave it alone.
return { path: file, action: 'not-found' };
}
export const cursorTarget: AgentTarget = new CursorTarget();