diff --git a/codemap/cli/analyze.py b/codemap/cli/analyze.py new file mode 100644 index 0000000..976aedc --- /dev/null +++ b/codemap/cli/analyze.py @@ -0,0 +1,144 @@ +import os +import sys +from pathlib import Path +from typing import Optional, List +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.text import Text +from codemap.parsers import PythonParser, JavaScriptParser, GoParser +from codemap.core import GraphBuilder, MermaidGenerator +from codemap.templates import render_html + +console = Console() + +def analyze( + path: str = typer.Argument(..., help="Directory or file to analyze"), + output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"), + format: str = typer.Option("mermaid", "--format", "-f", help="Output format (mermaid, html, markdown)"), + max_depth: int = typer.Option(3, "--max-depth", "-d", help="Maximum dependency depth"), + include_packages: bool = typer.Option(True, "--include-packages/--no-packages", help="Include package groupings"), +) -> None: + target_path = Path(path) + if not target_path.exists(): + console.print(f"[red]Error: Path '{path}' does not exist[/red]") + sys.exit(1) + + if not target_path.is_dir() and not target_path.suffix in [".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".mjs"]: + console.print(f"[red]Error: Unsupported file type[/red]") + sys.exit(1) + + with console.status("[bold green]Analyzing codebase...") as status: + files = _collect_files(target_path) + if not files: + console.print("[yellow]No supported files found[/yellow]") + return + + status.update("[bold green]Parsing files...") + + parsers = [PythonParser(), JavaScriptParser(), GoParser()] + parsed_files = [] + errors = [] + + for file_path in files: + for parser in parsers: + if parser.can_parse(file_path): + try: + parsed = parser.parse(file_path) + parsed_files.append(parsed) + except Exception as e: + errors.append((file_path, str(e))) + break + + status.update("[bold green]Building dependency graph...") + + builder = GraphBuilder() + builder.build_from_files(parsed_files) + + status.update("[bold green]Generating diagram...") + + if max_depth > 0: + start_nodes = [str(f.file_path.absolute()) for f in parsed_files] + filtered_graph = builder.filter_by_depth(start_nodes, max_depth) + graph_data = builder._build_graph_data_from_filtered(filtered_graph) + else: + graph_data = builder.get_graph_data() + + mermaid_gen = MermaidGenerator(graph_data) + if format == "mermaid": + output_content = mermaid_gen.generate_flowchart(include_packages) + elif format == "markdown": + output_content = _generate_markdown(graph_data, mermaid_gen, include_packages) + elif format == "html": + output_content = render_html(mermaid_gen.generate_flowchart(include_packages)) + else: + output_content = mermaid_gen.generate_flowchart(include_packages) + + _display_results(builder, errors, path) + + if output: + Path(output).write_text(output_content, encoding="utf-8") + console.print(f"[green]Output written to {output}[/green]") + else: + console.print(output_content) + +def _collect_files(target_path: Path) -> List[Path]: + files = [] + extensions = [".py", ".pyi", ".js", ".jsx", ".ts", ".tsx", ".go", ".mjs"] + + if target_path.is_file(): + if target_path.suffix in extensions: + return [target_path] + return [] + + for ext in extensions: + files.extend(target_path.rglob(f"*{ext}")) + + return sorted(set(files)) + +def _display_results(builder: GraphBuilder, errors: List, path: str) -> None: + stats = builder.get_stats() + + table = Table(title="Analysis Summary") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="magenta") + + table.add_row("Files Analyzed", str(stats["file_count"])) + table.add_row("Nodes", str(stats["node_count"])) + table.add_row("Dependencies", str(stats["edge_count"])) + table.add_row("Is DAG", str(stats["is_dag"])) + + console.print(Panel(table, title=f"Analysis Results for {path}", expand=False)) + + if errors: + error_table = Table(title="Parse Errors") + error_table.add_column("File", style="red") + error_table.add_column("Error", style="yellow") + + for file_path, error in errors[:10]: + error_table.add_row(str(file_path), error) + + console.print(Panel(error_table, title="Errors", expand=False)) + +def _generate_markdown(graph_data, mermaid_gen: MermaidGenerator, include_packages: bool) -> str: + md = ["# Code Map Analysis\n"] + md.append(f"## Summary\n") + md.append(f"- **Nodes**: {len(graph_data.nodes)}") + md.append(f"- **Edges**: {len(graph_data.edges)}") + md.append(f"- **Packages**: {len(graph_data.packages)}\n") + md.append("## Dependency Graph\n") + md.append("```mermaid") + md.append(mermaid_gen.generate_flowchart(include_packages)) + md.append("```\n") + + if graph_data.packages: + md.append("## Packages\n") + for package, nodes in graph_data.packages.items(): + md.append(f"### {package}") + for node_id in nodes: + node = next((n for n in graph_data.nodes if n.id == node_id), None) + if node: + md.append(f"- {node.name}") + + return "\n".join(md)