from __future__ import annotations import click import traceback import sys from pathlib import Path @click.group() def cli(): """codeflow — static analysis and visualization for any code repository.""" pass @cli.command("analyze") @click.argument("repo_path", type=click.Path( exists=True, file_okay=False, dir_okay=True, readable=True, path_type=str, )) @click.option("--output", "-o", required=True, type=click.Path(path_type=str), help="Path for the output HTML report (e.g., report.html)") @click.option("--cluster-threshold", default=5, show_default=True, type=click.IntRange(0, 100), help="Number of sibling nodes above which clustering is triggered. 0 disables clustering.") @click.option("--max-file-size", default=5, show_default=True, type=float, help="Skip files larger than this many megabytes.") @click.option("--burp", is_flag=True, default=False, help="Generate a Burp Suite JSON export file alongside the report.") @click.option("--review", is_flag=True, default=False, help="Generate a Markdown taint review document alongside the report (report.review.md).") @click.option("--verbose", "-v", is_flag=True, default=False, help="Print verbose progress to stderr.") def analyze(repo_path, output, cluster_threshold, max_file_size, burp, review, verbose): """Analyze a repository and produce an HTML report.""" output_path = Path(output) parent = output_path.parent if not parent.exists(): click.echo(f"Error: output directory does not exist: {parent}", err=True) sys.exit(1) if not output.lower().endswith(".html"): click.echo("Error: output must be a .html file", err=True) sys.exit(1) click.echo(f"[codeflow] Analyzing {repo_path} ...", err=True) try: from codeflow.orchestrator import run_analysis result = run_analysis( repo_path=Path(repo_path).resolve(), output_path=output_path.resolve(), cluster_threshold=cluster_threshold, max_file_size_mb=max_file_size, generate_burp=burp, generate_review=review, verbose=verbose, ) except Exception as e: click.echo(f"[codeflow] FATAL ERROR: {type(e).__name__}: {e}", err=True) if verbose: traceback.print_exc() sys.exit(2) html_path = result.get("html_path", "") burp_path = result.get("burp_path") file_count = result.get("file_count", 0) node_count = result.get("node_count", 0) flow_count = result.get("flow_count", 0) elapsed = result.get("elapsed_seconds", 0.0) errors = result.get("errors", []) review_path = result.get("review_path") click.echo("[codeflow] Done.") click.echo(f" Report: {html_path}") if burp_path: click.echo(f" Burp export: {burp_path}") if review_path: click.echo(f" Review doc: {review_path}") click.echo(f" Files: {file_count}") click.echo(f" Nodes: {node_count}") click.echo(f" Flows: {flow_count}") click.echo(f" Time: {elapsed:.1f}s") if errors: click.echo(f"[codeflow] WARNING: {len(errors)} file(s) had parse errors:", err=True) for err in errors: click.echo(f" - {err}", err=True)