From 97f59380060774de7d543ff8b5574a06afb69cb5 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 06:56:39 +0000 Subject: [PATCH] Add CLI commands and output renderer --- vibeguard/cli/commands/analyze.py | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 vibeguard/cli/commands/analyze.py diff --git a/vibeguard/cli/commands/analyze.py b/vibeguard/cli/commands/analyze.py new file mode 100644 index 0000000..6e989da --- /dev/null +++ b/vibeguard/cli/commands/analyze.py @@ -0,0 +1,123 @@ +"""Analyze command for VibeGuard CLI.""" + +import os +from pathlib import Path +from typing import Any + +import click +from rich.console import Console + +from vibeguard.analyzers.factory import AnalyzerFactory +from vibeguard.patterns.manager import PatternManager +from vibeguard.reports.generator import ReportGenerator +from vibeguard.utils.config import Config +from vibeguard.utils.file_finder import FileFinder + + +@click.command(name="analyze") +@click.argument("path", type=click.Path(exists=True)) +@click.option("--json", "output_json", is_flag=True, help="Output as JSON") +@click.option("--html", "output_html", type=click.Path(), help="Output HTML report to file") +@click.option("--sarif", "output_sarif", type=click.Path(), help="Output SARIF report to file") +@click.option( + "--severity", + type=click.Choice(["critical", "error", "warning", "info"]), + help="Minimum severity level to report", +) +@click.option("--exit-zero", is_flag=True, help="Exit with 0 even if issues found") +@click.option("--workers", type=int, default=0, help="Number of parallel workers (0=auto)") +@click.pass_obj +def analyze( + ctx: dict[str, Any], + path: str, + output_json: bool, + output_html: str | None, + output_sarif: str | None, + severity: str | None, + exit_zero: bool, + workers: int, +) -> None: + """Analyze a repository or file for AI anti-patterns.""" + config: Config = ctx["config"] + console: Console = ctx["console"] + + if severity: + config.analyze.severity_threshold = severity + if workers > 0: + config.analyze.workers = workers + + pattern_manager = PatternManager() + pattern_manager.load_all_patterns() + + file_finder = FileFinder(config.ignore.paths) + files = file_finder.find_files(path) + + if not files: + console.print("[warning]No files found to analyze[/warning]") + return + + console.print(f"[info]Found {len(files)} files to analyze[/info]\n") + + issues: list[dict[str, Any]] = [] + + for file_path in files: + file_issues = analyze_file(file_path, pattern_manager, config) + issues.extend(file_issues) + + console.print(f"\n[summary]Analysis Complete[/summary]") + console.print(f"Files scanned: {len(files)}") + console.print(f"Issues found: {len(issues)}\n") + + severity_counts = {"critical": 0, "error": 0, "warning": 0, "info": 0} + for issue in issues: + severity = issue.get("severity", "info") + severity_counts[severity] = severity_counts.get(severity, 0) + 1 + + console.print("[critical]Critical:[/critical]", severity_counts.get("critical", 0)) + console.print("[error]Errors:[/error]", severity_counts.get("error", 0)) + console.print("[warning]Warnings:[/warning]", severity_counts.get("warning", 0)) + console.print("[info]Info:[/info]", severity_counts.get("info", 0)) + console.print() + + if output_json: + import json + + console.print_json(data={"issues": issues, "summary": severity_counts}) + elif output_html: + generator = ReportGenerator() + generator.generate_html(issues, output_html, severity_counts) + console.print(f"[success]HTML report saved to: {output_html}[/success]") + elif output_sarif: + generator = ReportGenerator() + generator.generate_sarif(issues, output_sarif) + console.print(f"[success]SARIF report saved to: {output_sarif}[/success]") + + if issues and not exit_zero: + raise SystemExit(1) + + +def analyze_file( + path: Path, pattern_manager: PatternManager, config: Config +) -> list[dict[str, Any]]: + """Analyze a single file for anti-patterns.""" + from vibeguard.analyzers.base import BaseAnalyzer + + issues: list[dict[str, Any]] = [] + + analyzer = AnalyzerFactory.get_analyzer(path) + if not analyzer: + return issues + + try: + tree = analyzer.parse_file(path) + if not tree: + return issues + + file_issues = analyzer.analyze(tree, path) + issues.extend(file_issues) + + except Exception as e: + console = Console() + console.print(f"[warning]Error analyzing {path}: {e}[/warning]") + + return issues