From 2e7e6d81c565a9d2784991f3e133688c712a6a6b Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 29 Jan 2026 22:27:57 +0000 Subject: [PATCH] Initial upload: TermDiagram v0.1.0 --- src/termdiagram/cli.py | 423 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 src/termdiagram/cli.py diff --git a/src/termdiagram/cli.py b/src/termdiagram/cli.py new file mode 100644 index 0000000..ec8e606 --- /dev/null +++ b/src/termdiagram/cli.py @@ -0,0 +1,423 @@ +import os +import sys +import click +from typing import Optional +from pathlib import Path + +from . import __version__ +from .parser import CodeParser +from .generators import AsciiDiagramGenerator, MermaidDiagramGenerator, SvgDiagramGenerator +from .ui import TermDiagramTUI +from .git import GitHistoryTracker, ArchitectureDiffAnalyzer +from .utils import Config + + +@click.group() +@click.version_option(version=__version__, prog_name="termdiagram") +@click.option( + "--config", + "-c", + type=click.Path(exists=False), + help="Path to configuration file", +) +@click.pass_context +def main(ctx: click.Context, config: Optional[str]): + """TermDiagram - Generate architecture diagrams from codebases.""" + ctx.ensure_object(dict) + ctx.obj["config"] = Config() + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +@click.option( + "--output", + "-o", + type=click.Path(), + help="Output file path", +) +@click.option( + "--format", + "-f", + type=click.Choice(["ascii", "mermaid", "svg", "dot"]), + default="ascii", + help="Output format", +) +@click.option( + "--extensions", + "-e", + multiple=True, + help="File extensions to parse", +) +@click.option( + "--exclude", + multiple=True, + help="Patterns to exclude", +) +@click.option( + "--no-imports", + is_flag=True, + help="Don't show imports", +) +@click.option( + "--no-methods", + is_flag=True, + help="Don't show methods", +) +@click.option( + "--max-methods", + type=int, + default=5, + help="Maximum methods to show per class", +) +def diagram( + path: str, + output: Optional[str], + format: str, + extensions: tuple, + exclude: tuple, + no_imports: bool, + no_methods: bool, + max_methods: int, +): + """Generate an architecture diagram for a codebase.""" + parser = CodeParser() + path = os.path.abspath(path) + + click.echo(f"Parsing {path}...") + + ext_list = list(extensions) if extensions else None + exclude_list = list(exclude) if exclude else None + + modules = parser.parse_directory(path, ext_list, exclude_list) + + if not modules: + click.echo("No modules found. Check your path and extensions.") + return + + click.echo(f"Found {len(modules)} modules") + + stats = parser.get_statistics(modules) + click.echo(f" {stats['total_modules']} modules") + click.echo(f" {stats['total_classes']} classes") + click.echo(f" {stats['total_functions']} functions") + click.echo(f" {stats['total_methods']} methods") + + if format == "ascii": + generator = AsciiDiagramGenerator() + result = generator.generate(modules) + + if output: + with open(output, "w") as f: + f.write(result) + click.echo(f"ASCII diagram saved to {output}") + else: + click.echo("") + click.echo(result) + + elif format == "mermaid": + generator = MermaidDiagramGenerator() + result = generator.generate_class_diagram(modules) + + if output: + with open(output, "w") as f: + f.write(result) + click.echo(f"Mermaid diagram saved to {output}") + else: + click.echo("") + click.echo(result) + + elif format in ("svg", "dot"): + generator = SvgDiagramGenerator() + + if format == "dot": + result = generator.generate_dot(modules) + + if output: + with open(output, "w") as f: + f.write(result) + click.echo(f"DOT file saved to {output}") + else: + click.echo(result) + else: + svg = generator.generate(modules) + if svg: + if output: + generator.save_svg(modules, output) + click.echo(f"SVG saved to {output}") + else: + click.echo("SVG generation requires output file") + else: + click.echo("Graphviz not available. Install Graphviz and pygraphviz.") + click.echo(" Falling back to Mermaid format...") + result = generator.generate_dot(modules) + click.echo(result) + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +@click.option( + "--format", + "-f", + type=click.Choice(["json", "yaml"]), + default="json", + help="Output format", +) +def json( + path: str, + format: str, +): + """Export parsed data as JSON or YAML.""" + parser = CodeParser() + path = os.path.abspath(path) + + click.echo(f"Parsing {path}...") + + modules = parser.parse_directory(path) + + if not modules: + click.echo("No modules found.") + return + + if format == "json": + import json + + data = { + "modules": [m.to_dict() for m in modules], + "statistics": parser.get_statistics(modules), + } + + click.echo(json.dumps(data, indent=2)) + else: + import yaml + + data = { + "modules": [m.to_dict() for m in modules], + "statistics": parser.get_statistics(modules), + } + + click.echo(yaml.dump(data, default_flow_style=False)) + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +def stats(path: str): + """Show statistics about the codebase.""" + parser = CodeParser() + path = os.path.abspath(path) + + click.echo(f"Analyzing {path}...") + + modules = parser.parse_directory(path) + + if not modules: + click.echo("No modules found.") + return + + stats = parser.get_statistics(modules) + + click.echo("") + click.echo("Codebase Statistics") + click.echo("=" * 40) + click.echo(f" Modules: {stats['total_modules']}") + click.echo(f" Classes: {stats['total_classes']}") + click.echo(f" Functions: {stats['total_functions']}") + click.echo(f" Methods: {stats['total_methods']}") + click.echo("") + click.echo("Languages:") + for lang, count in stats["languages"].items(): + click.echo(f" - {lang}: {count} modules") + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +def tree(path: str): + """Display the project structure as a tree.""" + parser = CodeParser() + from .ui import TreeView + from rich import print as rprint + + path = os.path.abspath(path) + + click.echo(f"Parsing {path}...") + + modules = parser.parse_directory(path) + + if not modules: + click.echo("No modules found.") + return + + tree_view = TreeView() + tree = tree_view.create_tree(modules, show_methods=True) + rprint(tree) + + +@main.command(context_settings={"ignore_unknown_options": True}) +@click.argument("path", type=click.Path(exists=True), default=".") +@click.option( + "--search", + "-s", + help="Search for symbols", +) +def search(path: str, search: Optional[str]): + """Search for symbols in the codebase.""" + parser = CodeParser() + from .ui import FuzzySearcher + + path = os.path.abspath(path) + + click.echo(f"Parsing {path}...") + + modules = parser.parse_directory(path) + + if not modules: + click.echo("No modules found.") + return + + all_symbols = [] + for module in modules: + all_symbols.append(module) + all_symbols.extend(module.classes) + all_symbols.extend(module.functions) + + searcher = FuzzySearcher() + searcher.build_index(all_symbols) + + if search: + results = searcher.search(search) + click.echo(f"\nSearch results for '{search}':") + click.echo("-" * 50) + + for result in results[:10]: + click.echo( + f" {result.name} ({result.symbol_type}) - {result.path}:{result.line_number}" + ) + else: + count = searcher.get_symbol_count() + click.echo(f"{count} symbols indexed") + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +def interactive(path: str): + """Launch interactive TUI mode.""" + from .ui import TermDiagramTUI + + path = os.path.abspath(path) + + if not os.path.isdir(path): + click.echo(f"{path} is not a directory") + return + + tui = TermDiagramTUI() + tui.run(path) + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +@click.option( + "--since", + type=str, + help="Show changes since commit or date", +) +@click.option( + "--limit", + type=int, + default=20, + help="Limit number of commits", +) +def history(path: str, since: Optional[str], limit: int): + """Show git history for the project.""" + tracker = GitHistoryTracker(path) + + if not tracker.is_git_repo(): + click.echo(f"{path} is not a git repository") + return + + click.echo(f"Git history for {path}") + click.echo("=" * 50) + + commits = tracker.get_commit_history(limit=limit) + + for commit in commits: + click.echo(f" {commit.hash} - {commit.message[:50]}") + click.echo(f" {commit.author} - {commit.date.strftime('%Y-%m-%d')}") + + +@main.command() +@click.argument("path", type=click.Path(exists=True), default=".") +@click.argument("old_commit", type=str) +@click.argument("new_commit", type=str) +def diff(path: str, old_commit: str, new_commit: str): + """Show architecture changes between two commits.""" + analyzer = ArchitectureDiffAnalyzer(path) + + if not analyzer.initialize(): + click.echo(f"{path} is not a git repository") + return + + report = analyzer.get_change_report(old_commit, new_commit) + click.echo(report) + + +@main.command() +def version(): + """Show version information.""" + click.echo(f"TermDiagram version {__version__}") + + +@main.command() +def check(): + """Check system dependencies.""" + click.echo("Checking dependencies...") + + click.echo("") + click.echo("Python packages:") + try: + import rich + click.echo(f" rich: {rich.__version__}") + except ImportError: + click.echo(" rich: not installed") + + try: + import tree_sitter + click.echo(f" tree-sitter: {tree_sitter.__version__}") + except ImportError: + click.echo(" tree-sitter: not installed") + + try: + import rapidfuzz + click.echo(f" rapidfuzz: {rapidfuzz.__version__}") + except ImportError: + click.echo(" rapidfuzz: not installed") + + try: + import git + click.echo(f" gitpython: installed") + except ImportError: + click.echo(" gitpython: not installed") + + try: + import pygraphviz + click.echo(f" pygraphviz: installed") + except ImportError: + click.echo(" pygraphviz: not installed (SVG generation unavailable)") + + click.echo("") + click.echo("System dependencies:") + + try: + import subprocess + + result = subprocess.run( + ["dot", "-V"], capture_output=True, text=True + ) + if result.returncode == 0: + click.echo(" Graphviz: installed") + else: + click.echo(" Graphviz: version check failed") + except FileNotFoundError: + click.echo(" Graphviz: not found (SVG generation unavailable)") + + +if __name__ == "__main__": + main()