Skip to content

feat(cli): add 'uninstall' command (closes #482)#491

Open
zintaen wants to merge 1 commit into
tirth8205:mainfrom
zintaen:feat/uninstall-command
Open

feat(cli): add 'uninstall' command (closes #482)#491
zintaen wants to merge 1 commit into
tirth8205:mainfrom
zintaen:feat/uninstall-command

Conversation

@zintaen
Copy link
Copy Markdown

@zintaen zintaen commented May 19, 2026

PR: feat(cli): add uninstall command to remove all installed artifacts

Closes #482.

Summary

code-review-graph install writes a lot of files: a graph database, generated skills, MCP configs across 14 platforms, hooks at both repo and user level, and an injected section in seven different instruction files. Issue #482 asks for a symmetric way to remove all of it.

This PR adds code-review-graph uninstall.

What it does

Per-repo

  • Removes the .code-review-graph/ data directory and the legacy .code-review-graph.db file.
  • Removes generated skill directories under .claude/skills/, .gemini/skills/, and .qoder/skills/.
  • Removes our pre-commit hook block (and only ours — user lines and other hooks in the same file are preserved).
  • Removes the .code-review-graph/ entry from .gitignore.
  • Removes the <!-- code-review-graph MCP tools --> section from CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, .windsurfrules, QODER.md, the Kiro steering file, and the GitHub Copilot instruction file. If a file contained only our section, the file is deleted.

User-level

  • Removes ~/.code-review-graph/ (registry, daemon state, watch.toml, logs).
  • Removes ~/.config/opencode/plugins/crg-plugin.ts and the user-level Cursor hook scripts directory.
  • Removes our entry from every MCP / hooks config we write: Codex (TOML), Cursor, Windsurf, Zed, Continue, Antigravity, Qwen, Copilot CLI.

Surgical edits, not nukes

Every shared config file is edited rather than deleted. Only the code-review-graph entry is removed; other MCP servers, hooks, and content stay untouched. The tests pin this contract explicitly.

Flags

code-review-graph uninstall              # preview + confirm, then clean current repo + ~/
code-review-graph uninstall --dry-run    # show every planned action, change nothing
code-review-graph uninstall --yes        # skip the confirmation prompt
code-review-graph uninstall --repo PATH  # target a specific repo (default: cwd)
code-review-graph uninstall --all-repos  # also sweep every repo in the registry
code-review-graph uninstall --keep-data  # only remove configs, keep the graph DB
code-review-graph uninstall --keep-user-configs  # only clean the target repo(s)

Tests

tests/test_uninstall.py adds 18 tests covering:

  • Dry-run leaves the filesystem byte-identical.
  • Per-repo data dir / legacy DB removal.
  • --keep-data preserves the data dir and reports it as skipped.
  • MCP entry removal preserves unrelated servers (workspace and user level).
  • Claude settings.json keeps user-defined hooks and non-hook keys.
  • Generated skill directories are removed.
  • Pre-commit hook preserves preexisting user lines.
  • Instruction-file section removal preserves other content; the file is deleted only when nothing else remains.
  • .gitignore: our entry removed, user entries preserved.
  • Codex config.toml: our section removed, other sections kept.
  • Continue array-format MCP config: our entry removed by name.
  • --keep-user-configs skips the user-level pass.
  • A clean repo is a no-op.

All 18 pass; existing tests in test_cli, test_cli_install, test_skills, and test_registry still pass (the four pre-existing failures in test_incremental.py are an environmental tree-sitter mismatch unrelated to this change).

Files changed

 CHANGELOG.md                   |   3 +
 README.md                      |  15 +
 code_review_graph/cli.py       |  88 ++++++
 code_review_graph/uninstall.py | 685 +++++++++++++++++++++++++++++++++++
 tests/test_uninstall.py        | 387 ++++++++++++++++++++
 5 files changed, 1178 insertions(+)

Closes tirth8205#482.

Adds a symmetric counterpart to 'code-review-graph install' that walks the
full inventory of files install creates and removes them — both per-repo
and at user level.

Per-repo it removes:
- The .code-review-graph/ data directory and the legacy .code-review-graph.db.
- Generated skill directories under .claude/skills, .gemini/skills,
  and .qoder/skills.
- Our pre-commit hook block (preserving any user hooks that share the file).
- The .code-review-graph/ entry from .gitignore.
- The '<!-- code-review-graph MCP tools -->' section from CLAUDE.md,
  AGENTS.md, GEMINI.md, .cursorrules, .windsurfrules, QODER.md, the
  Kiro steering file, and the GitHub Copilot instruction file.

User-level it removes:
- ~/.code-review-graph/ (registry, daemon state, logs).
- ~/.config/opencode/plugins/crg-plugin.ts.
- Our entry from every MCP / hooks config we ever write: Codex (TOML),
  Cursor, Windsurf, Zed, Continue, Antigravity, Qwen, and Copilot CLI.

All edits to shared configuration files are surgical: only the
'code-review-graph' entry is removed; other MCP servers, hooks, and
content survive untouched.

Flags:
  --dry-run            preview without changing anything
  --yes / -y           skip the confirmation prompt
  --repo PATH          target a specific repo (default: cwd)
  --all-repos          also sweep every repo in the registry
  --keep-data          leave .code-review-graph/ data dirs in place
  --keep-user-configs  skip the user-level pass

Adds tests/test_uninstall.py with 18 tests covering:
- Dry-run leaves the filesystem untouched.
- Per-repo data dir / legacy DB removal.
- --keep-data preserves the data dir and reports it as skipped.
- MCP entry removal preserves unrelated servers (workspace + user level).
- Claude settings.json keeps user-defined hooks and non-hook keys.
- Generated skill directories are removed.
- Pre-commit hook preserves preexisting user lines.
- Instruction-file section removal preserves other content; file is
  deleted only when nothing else remains.
- .gitignore: our entry removed, user entries preserved.
- Codex config.toml: our section removed, other sections kept.
- Continue array-format MCP config: our entry removed by name.
- --keep-user-configs skips the user-level pass.
- A clean repo is a no-op.
@zintaen
Copy link
Copy Markdown
Author

zintaen commented May 19, 2026

@tirth8205 I got this PR ready for review

@asdf8675309
Copy link
Copy Markdown

I was reviewing this repo, saw this issue #482 and then saw this. I had claude review it as part of my thought process as to whether or not to use code-review-graph. Here's the out put in case it helps. I'm going to hold off on using it till an uninstall process is added.


Architecture is sound. uninstall.py (685 LOC) has a clean three-layer design:

  1. Inventory — _per_repo_artifacts(repo_root) and _user_artifacts() enumerate every path with a kind tag (dir / file / json_mcp / toml_codex / json_hooks / instruction / git_hook).
  2. Surgical edit handlers — one per kind. Each preserves unrelated entries.
  3. Orchestration — run() walks inventory, dispatches by kind, collects results into an UninstallReport.

Conservative-by-default UX: CLI always runs a dry-run first, prints planned actions, then asks for confirmation unless --yes (cli.py:849–895). --dry-run short-circuits at preview.

Surgical-not-nuke is testable contract. 18 tests in test_uninstall.py pin exactly this — every MCP config and hook config test injects an unrelated server/hook and asserts it survives the uninstall.

Strengths

  1. No shell interpolation anywhere. All filesystem ops via pathlib.Path, shutil.rmtree, path.unlink(). JSON via json.loads. TOML via textual section-removal. Zero os.system / subprocess(shell=True).
  2. Idempotent. Every helper guards if not path.exists(): return. Tests cover "clean repo is a no-op."
  3. Errors surfaced, not swallowed. Parse failures → report.skipped_paths. OS errors → report.errors, CLI exits non-zero. ✓
  4. Tests are the right shape — they verify the contract (other tools' configs survive) not just "the file got deleted."
  5. Docs updated (README + CHANGELOG). Author wrote a thorough PR description that matches the code. ✓
  6. Bounded blast radius by design — the inventory is hard-coded with specific subpath names (.code-review-graph, .claude/skills/explore-codebase, etc.). Even --repo / can't recursively delete / because no
    path in the inventory is the root itself.

Concerns (ranked by severity)

  1. MEDIUM — No "is this a repo?" guard on --repo. cli.py:849 does Path(args.repo).expanduser().resolve() and trusts the result. The inventory bounds the blast radius (good), but adding a .git /
    known-CRG-artifact existence check before processing would harden against typos like --repo ~ deleting parts of $HOME. The user-level pass also targets ~, so combining --repo ~ with the default user pass
    could compound footprint. Mitigation: cheap — skip processing a repo root that has zero code_review_graph artifacts.
  2. MEDIUM — _remove_git_hook docstring/behavior mismatch. Docstring (uninstall.py:444–449) says the block runs "from the marker through the next blank line (or EOF)." Code drops "until stripped == 'fi'."
    If a user's own hook contains an if … fi that follows our marker before any blank line, the behavior is well-defined (stop at fi), but the docstring misleads future maintainers. Fix: align docstring with
    code, or change the boundary to a more robust marker (# End code-review-graph block) emitted by install.
  3. MEDIUM — TOML section-end heuristic is fragile. _remove_codex_toml_entry (uninstall.py:223–232) stops dropping when it sees a line that "starts with [ and ends with ]." A TOML value like tags = "[work]"
    on its own line would falsely terminate the drop. Low-probability in practice (Codex config.toml is mostly headers + scalars) but cheap to harden with a real TOML parser (tomllib is stdlib in 3.11+).
  4. LOW — .kiro and .github instruction files use marker-strip but may be wholly-owned. _INSTRUCTION_FILES includes .kiro/steering/code-review-graph.md and .github/code-review-graph.instruction.md. These
    names strongly suggest install writes the entire file, not an appended section. If so, the marker-based _remove_instruction_section is a no-op for them and leaves orphan files. Worth verifying against
    skills.py install logic.
  5. LOW — No install-manifest design. The inventory is hand-maintained to mirror install. Any future install path that gets added without a matching uninstall entry causes silent drift. Robust long-term
    fix: install writes ~/.code-review-graph/install-manifest.json listing every path it touched; uninstall reads it. Out of scope for this PR; worth filing as a follow-up issue.
  6. LOW — Bare except Exception in collect_repo_roots (line ~580). Marked # pragma: no cover — defensive. Acceptable here (registry is optional dependency) but worth narrowing if the registry module's
    exception surface is known.

PR Verdict

APPROVE-WITH-CHANGES (lean toward approve — this is high-quality work):

  • Required changes: (1) align _remove_git_hook docstring with code; (2) add a minimal "looks like a repo or has CRG artifacts" guard before processing --repo PATH.
  • Recommended follow-ups (not blockers): replace TOML heuristic with tomllib; add an install-manifest mechanism; verify .kiro / .github file behavior.

The core design — inventory + per-kind handlers + dry-run-first + surgical edits + test-the-contract — is exactly what an uninstall should look like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add a uninstall command for code-review-graph

2 participants