Skip to content
Open
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
168 changes: 106 additions & 62 deletions code_review_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ def _print_banner() -> None:
version = _get_version()

# ANSI escape codes
c = "\033[36m" if color else "" # cyan — graph art
y = "\033[33m" if color else "" # yellow — center node
b = "\033[1m" if color else "" # bold
d = "\033[2m" if color else "" # dim
g = "\033[32m" if color else "" # green — commands
r = "\033[0m" if color else "" # reset
c = "\033[36m" if color else "" # cyan — graph art
y = "\033[33m" if color else "" # yellow — center node
b = "\033[1m" if color else "" # bold
d = "\033[2m" if color else "" # dim
g = "\033[32m" if color else "" # green — commands
r = "\033[0m" if color else "" # reset

print(f"""
{c} ●──●──●{r}
Expand Down Expand Up @@ -151,74 +151,103 @@ def _handle_init(args: argparse.Namespace) -> None:
print(" 2. Restart your AI coding tool to pick up the new config")


def _cli_post_process(store: object) -> None:
"""Run post-build pipeline and print a summary line for each step."""
from .postprocessing import run_post_processing

pp = run_post_processing(store) # type: ignore[arg-type]
if pp.get("signatures_computed"):
print(f"Signatures: {pp['signatures_computed']} nodes")
if pp.get("fts_indexed"):
print(f"FTS indexed: {pp['fts_indexed']} nodes")
if pp.get("flows_detected") is not None:
print(f"Flows: {pp['flows_detected']}")
if pp.get("communities_detected") is not None:
print(f"Communities: {pp['communities_detected']}")


def main() -> None:
"""Main CLI entry point."""
ap = argparse.ArgumentParser(
prog="code-review-graph",
description="Persistent incremental knowledge graph for code reviews",
)
ap.add_argument(
"-v", "--version", action="store_true", help="Show version and exit"
)
ap.add_argument("-v", "--version", action="store_true", help="Show version and exit")
sub = ap.add_subparsers(dest="command")

# install (primary) + init (alias)
install_cmd = sub.add_parser(
"install", help="Register MCP server with AI coding platforms"
)
install_cmd = sub.add_parser("install", help="Register MCP server with AI coding platforms")
install_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
install_cmd.add_argument(
"--dry-run", action="store_true",
"--dry-run",
action="store_true",
help="Show what would be done without writing files",
)
install_cmd.add_argument(
"--no-skills", action="store_true",
"--no-skills",
action="store_true",
help="Skip generating Claude Code skill files",
)
install_cmd.add_argument(
"--no-hooks", action="store_true",
"--no-hooks",
action="store_true",
help="Skip installing Claude Code hooks",
)
# Legacy flags (kept for backwards compat, now no-ops since all is default)
install_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
install_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
install_cmd.add_argument("--all", action="store_true", dest="install_all",
help=argparse.SUPPRESS)
install_cmd.add_argument(
"--all", action="store_true", dest="install_all", help=argparse.SUPPRESS
)
install_cmd.add_argument(
"--platform",
choices=[
"claude", "claude-code", "cursor", "windsurf", "zed",
"continue", "opencode", "antigravity", "all",
"claude",
"claude-code",
"cursor",
"windsurf",
"zed",
"continue",
"opencode",
"antigravity",
"all",
],
default="all",
help="Target platform for MCP config (default: all detected)",
)

init_cmd = sub.add_parser(
"init", help="Alias for install"
)
init_cmd = sub.add_parser("init", help="Alias for install")
init_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
init_cmd.add_argument(
"--dry-run", action="store_true",
"--dry-run",
action="store_true",
help="Show what would be done without writing files",
)
init_cmd.add_argument(
"--no-skills", action="store_true",
"--no-skills",
action="store_true",
help="Skip generating Claude Code skill files",
)
init_cmd.add_argument(
"--no-hooks", action="store_true",
"--no-hooks",
action="store_true",
help="Skip installing Claude Code hooks",
)
init_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
init_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
init_cmd.add_argument("--all", action="store_true", dest="install_all",
help=argparse.SUPPRESS)
init_cmd.add_argument("--all", action="store_true", dest="install_all", help=argparse.SUPPRESS)
init_cmd.add_argument(
"--platform",
choices=[
"claude", "claude-code", "cursor", "windsurf", "zed",
"continue", "opencode", "antigravity", "all",
"claude",
"claude-code",
"cursor",
"windsurf",
"zed",
"continue",
"opencode",
"antigravity",
"all",
],
default="all",
help="Target platform for MCP config (default: all detected)",
Expand All @@ -228,11 +257,13 @@ def main() -> None:
build_cmd = sub.add_parser("build", help="Full graph build (re-parse all files)")
build_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
build_cmd.add_argument(
"--skip-flows", action="store_true",
"--skip-flows",
action="store_true",
help="Skip flow/community detection (signatures + FTS only)",
)
build_cmd.add_argument(
"--skip-postprocess", action="store_true",
"--skip-postprocess",
action="store_true",
help="Skip all post-processing (raw parse only)",
)

Expand All @@ -241,11 +272,13 @@ def main() -> None:
update_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
update_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
update_cmd.add_argument(
"--skip-flows", action="store_true",
"--skip-flows",
action="store_true",
help="Skip flow/community detection (signatures + FTS only)",
)
update_cmd.add_argument(
"--skip-postprocess", action="store_true",
"--skip-postprocess",
action="store_true",
help="Skip all post-processing (raw parse only)",
)

Expand Down Expand Up @@ -277,15 +310,17 @@ def main() -> None:
help="Rendering mode: auto (default), full, community, or file",
)
vis_cmd.add_argument(
"--serve", action="store_true",
"--serve",
action="store_true",
help="Start a local HTTP server to view the visualization (localhost:8765)",
)

# wiki
wiki_cmd = sub.add_parser("wiki", help="Generate markdown wiki from community structure")
wiki_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
wiki_cmd.add_argument(
"--force", action="store_true",
"--force",
action="store_true",
help="Regenerate all pages even if content unchanged",
)

Expand All @@ -308,9 +343,10 @@ def main() -> None:
# eval
eval_cmd = sub.add_parser("eval", help="Run evaluation benchmarks")
eval_cmd.add_argument(
"--benchmark", default=None,
"--benchmark",
default=None,
help="Comma-separated benchmarks to run (token_efficiency, impact_accuracy, "
"flow_completeness, search_quality, build_performance)",
"flow_completeness, search_quality, build_performance)",
)
eval_cmd.add_argument("--repo", default=None, help="Comma-separated repo config names")
eval_cmd.add_argument("--all", action="store_true", dest="run_all", help="Run all benchmarks")
Expand All @@ -319,12 +355,8 @@ def main() -> None:

# detect-changes
detect_cmd = sub.add_parser("detect-changes", help="Analyze change impact")
detect_cmd.add_argument(
"--base", default="HEAD~1", help="Git diff base (default: HEAD~1)"
)
detect_cmd.add_argument(
"--brief", action="store_true", help="Show brief summary only"
)
detect_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
detect_cmd.add_argument("--brief", action="store_true", help="Show brief summary only")
detect_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")

# serve
Expand All @@ -343,6 +375,7 @@ def main() -> None:

if args.command == "serve":
from .main import main as serve_main

serve_main(repo_root=args.repo)
return

Expand All @@ -351,9 +384,7 @@ def main() -> None:
from .eval.runner import run_eval

if getattr(args, "report", False):
output_dir = Path(
getattr(args, "output_dir", None) or "evaluate/results"
)
output_dir = Path(getattr(args, "output_dir", None) or "evaluate/results")
report = generate_full_report(output_dir)
report_path = Path("evaluate/reports/summary.md")
report_path.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -365,9 +396,7 @@ def main() -> None:
print(tables)
else:
repos = (
[r.strip() for r in args.repo.split(",")]
if getattr(args, "repo", None)
else None
[r.strip() for r in args.repo.split(",")] if getattr(args, "repo", None) else None
)
benchmarks = (
[b.strip() for b in args.benchmark.split(",")]
Expand Down Expand Up @@ -439,6 +468,7 @@ def main() -> None:
store = GraphStore(db_path)
try:
from .tools.build import run_postprocess

result = run_postprocess(
flows=not getattr(args, "no_flows", False),
communities=not getattr(args, "no_communities", False),
Expand Down Expand Up @@ -475,32 +505,39 @@ def main() -> None:

try:
if args.command == "build":
pp = "none" if getattr(args, "skip_postprocess", False) else (
"minimal" if getattr(args, "skip_flows", False) else "full"
pp = (
"none"
if getattr(args, "skip_postprocess", False)
else ("minimal" if getattr(args, "skip_flows", False) else "full")
)
from .tools.build import build_or_update_graph

result = build_or_update_graph(
full_rebuild=True, repo_root=str(repo_root), postprocess=pp,
full_rebuild=True,
repo_root=str(repo_root),
postprocess=pp,
)
parsed = result.get("files_parsed", 0)
nodes = result.get("total_nodes", 0)
edges = result.get("total_edges", 0)
print(
f"Full build: {parsed} files, "
f"{nodes} nodes, {edges} edges"
f" (postprocess={pp})"
)
print(f"Full build: {parsed} files, {nodes} nodes, {edges} edges (postprocess={pp})")
if result.get("errors"):
print(f"Errors: {len(result['errors'])}")
_cli_post_process(store)

elif args.command == "update":
pp = "none" if getattr(args, "skip_postprocess", False) else (
"minimal" if getattr(args, "skip_flows", False) else "full"
pp = (
"none"
if getattr(args, "skip_postprocess", False)
else ("minimal" if getattr(args, "skip_flows", False) else "full")
)
from .tools.build import build_or_update_graph

result = build_or_update_graph(
full_rebuild=False, repo_root=str(repo_root),
base=args.base, postprocess=pp,
full_rebuild=False,
repo_root=str(repo_root),
base=args.base,
postprocess=pp,
)
updated = result.get("files_updated", 0)
nodes = result.get("total_nodes", 0)
Expand All @@ -510,6 +547,8 @@ def main() -> None:
f"{nodes} nodes, {edges} edges"
f" (postprocess={pp})"
)
if result.get("files_updated", 0) > 0:
_cli_post_process(store)

elif args.command == "status":
stats = store.get_stats()
Expand All @@ -526,6 +565,7 @@ def main() -> None:
if stored_sha:
print(f"Built at commit: {stored_sha[:12]}")
from .incremental import _git_branch_info

current_branch, current_sha = _git_branch_info(repo_root)
if stored_branch and current_branch and stored_branch != current_branch:
print(
Expand All @@ -535,10 +575,13 @@ def main() -> None:
)

elif args.command == "watch":
watch(repo_root, store)
from .postprocessing import run_post_processing

watch(repo_root, store, on_files_updated=run_post_processing)

elif args.command == "visualize":
from .visualization import generate_html

html_path = repo_root / ".code-review-graph" / "graph.html"
vis_mode = getattr(args, "mode", "auto") or "auto"
generate_html(store, html_path, mode=vis_mode)
Expand All @@ -565,6 +608,7 @@ def main() -> None:

elif args.command == "wiki":
from .wiki import generate_wiki

wiki_dir = repo_root / ".code-review-graph" / "wiki"
result = generate_wiki(store, wiki_dir, force=args.force)
total = result["pages_generated"] + result["pages_updated"] + result["pages_unchanged"]
Expand Down
Loading