From f78dd523822bb4431c1ffe0f7c63935898994af5 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Fri, 30 Jan 2026 17:19:21 +0000 Subject: [PATCH] fix: resolve CI type checking and lint failures --- app/depnav/src/depnav/cli.py | 444 +++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 app/depnav/src/depnav/cli.py diff --git a/app/depnav/src/depnav/cli.py b/app/depnav/src/depnav/cli.py new file mode 100644 index 0000000..8322b33 --- /dev/null +++ b/app/depnav/src/depnav/cli.py @@ -0,0 +1,444 @@ +"""Main CLI interface for depnav.""" + +from pathlib import Path +from typing import Optional + +import click +from rich.console import Console +from rich.panel import Panel + +from .config import Config, load_config +from .detector import CycleDetector +from .graph import DependencyGraph +from .navigator import Navigator +from .renderer import ASCIIRenderer, GraphStyle + +console = Console() + + +@click.group() +@click.option( + "--config", + "-c", + type=click.Path(exists=True, path_type=Path), + help="Path to configuration file", +) +@click.option( + "--theme", + type=click.Choice(["default", "dark", "light", "mono"]), + default="default", + help="Color theme", +) +@click.option( + "--pager/--no-pager", + default=True, + help="Use pager for output", +) +@click.pass_context +def main( + ctx: click.Context, + config: Optional[Path], + theme: str, + pager: bool, +): + """Depnav - CLI Dependency Graph Navigator.""" + ctx.ensure_object(dict) + ctx.obj["config"] = config + ctx.obj["theme"] = theme + ctx.obj["pager"] = pager + + cfg = load_config(config) + ctx.obj["config_obj"] = cfg + + +@main.command("graph") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.option( + "--depth", + "-d", + type=int, + default=3, + help="Maximum traversal depth", +) +@click.option( + "--focus", + "-f", + type=str, + default=None, + help="Focus on a specific file", +) +@click.option( + "--max-nodes", + "-m", + type=int, + default=50, + help="Maximum number of nodes to display", +) +@click.option( + "--output", + "-o", + type=click.Choice(["ascii", "json", "dot"]), + default="ascii", + help="Output format", +) +@click.pass_context +def graph_command( + ctx: click.Context, + path: Path, + depth: int, + focus: Optional[str], + max_nodes: int, + output: str, +): + """Generate a dependency graph for the project.""" + cfg: Optional[Config] = ctx.obj.get("config_obj") + if cfg: + depth = cfg.get_depth() or depth + max_nodes = cfg.get_max_nodes() or max_nodes + + graph = DependencyGraph(path if path.is_dir() else path.parent) + graph.build_from_directory( + directory=path if path.is_dir() else None, + max_depth=depth, + ) + + theme = ctx.obj.get("theme", "default") + style = _get_style_from_theme(theme) + + renderer = ASCIIRenderer(console) + renderer.set_style(style) + + if output == "json": + click.echo(renderer.render_json(graph)) + elif output == "dot": + click.echo(renderer.render_dot(graph)) + else: + focus_path = None + if focus: + focus_path = graph.get_node_by_name(focus) + panel = renderer.render_graph(graph, focus_path, max_nodes) + console.print(panel, soft_wrap=True) + + +@main.command("tree") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.option( + "--depth", + "-d", + type=int, + default=2, + help="Maximum tree depth", +) +@click.pass_context +def tree_command( + ctx: click.Context, + path: Path, + depth: int, +): + """Show the project structure as a tree.""" + graph = DependencyGraph(path) + graph.build_from_directory(directory=path, max_depth=depth + 2) + + theme = ctx.obj.get("theme", "default") + style = _get_style_from_theme(theme) + + renderer = ASCIIRenderer(console) + renderer.set_style(style) + + panel = renderer.render_tree(graph, path, depth) + console.print(panel, soft_wrap=True) + + +@main.command("cycles") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.option( + "--report", + "-r", + type=click.Path(path_type=Path), + default=None, + help="Export report to file", +) +@click.pass_context +def cycles_command( + ctx: click.Context, + path: Path, + report: Optional[Path], +): + """Detect and report circular dependencies.""" + graph = DependencyGraph(path) + graph.build_from_directory() + + detector = CycleDetector(graph) + cycles = detector.detect_all_cycles() + + theme = ctx.obj.get("theme", "default") + style = _get_style_from_theme(theme) + + renderer = ASCIIRenderer(console) + renderer.set_style(style) + + panel = renderer.render_cycles(cycles) + console.print(panel, soft_wrap=True) + + if report: + detector.export_cycle_report(report) + click.echo(f"Report exported to {report}") + + +@main.command("navigate") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.option( + "--file", + "-f", + type=str, + default=None, + help="Initial file to navigate to", +) +@click.pass_context +def navigate_command( + ctx: click.Context, + path: Path, + file: Optional[str], +): + """Start interactive navigation mode.""" + graph = DependencyGraph(path) + graph.build_from_directory() + + theme = ctx.obj.get("theme", "default") + style = _get_style_from_theme(theme) + + navigator = Navigator(graph, console, style) + + if file: + file_path = graph.get_node_by_name(file) + if file_path: + navigator.navigate_to(file_path) + + _run_interactive_mode(navigator, graph) + + +def _run_interactive_mode(navigator: Navigator, graph: DependencyGraph): + """Run the interactive navigation mode.""" + click.echo("Interactive navigation mode. Type 'help' for commands.") + + while True: + try: + cmd = click.prompt( + "depnav", + type=str, + default="", + show_default=False, + ).strip() + except click.exceptions.ClickException: + break + + if not cmd: + continue + + parts = cmd.split() + action = parts[0].lower() + + if action in ("quit", "exit", "q"): + break + elif action == "help": + _show_help() + elif action == "current": + click.echo(navigator.show_current()) + elif action == "deps" and len(parts) > 1: + target = graph.get_node_by_name(parts[1]) + if target: + click.echo(navigator.show_dependencies(target)) + elif action == "imported" and len(parts) > 1: + target = graph.get_node_by_name(parts[1]) + if target: + click.echo(navigator.show_dependents(target)) + elif action == "path" and len(parts) > 2: + source = graph.get_node_by_name(parts[1]) + target = graph.get_node_by_name(parts[2]) + if source and target: + click.echo(navigator.find_path(source, target)) + elif action == "search" and len(parts) > 1: + results = navigator.search(parts[1]) + for r in results: + click.echo(f" {r}") + elif action == "stats": + renderer = ASCIIRenderer(console) + console.print(renderer.render_statistics(graph)) + elif action == "back": + prev = navigator.back() + if prev: + click.echo(f"Back to: {prev}") + else: + click.echo("No previous file") + elif action == "list": + nodes = graph.get_nodes() + for n in nodes[:20]: + click.echo(f" {n}") + if len(nodes) > 20: + click.echo(f" ... and {len(nodes) - 20} more") + else: + click.echo(f"Unknown command: {action}. Type 'help' for available commands.") + + +def _show_help(): + """Show help for interactive mode.""" + help_text = """ +Available commands: + current Show current file info + deps Show dependencies of a file + imported Show files importing a file + path Show shortest path between files + search Search for files + stats Show graph statistics + back Go back to previous file + list List all files + help Show this help + quit Exit +""" + click.echo(help_text) + + +@main.command("stats") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.pass_context +def stats_command(ctx: click.Context, path: Path): + """Show dependency graph statistics.""" + graph = DependencyGraph(path) + graph.build_from_directory() + + renderer = ASCIIRenderer(console) + console.print(renderer.render_statistics(graph)) + + +@main.command("info") +@click.argument( + "file", + type=click.Path(exists=True, path_type=Path), +) +@click.option( + "--path", + "-p", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), + help="Project root path", +) +@click.pass_context +def info_command( + ctx: click.Context, + file: Path, + path: Path, +): + """Show detailed information about a file.""" + graph = DependencyGraph(path) + graph.build_from_directory() + + if not str(file).startswith("/"): + file = path / file + + try: + rel_file = file.relative_to(path) + except ValueError: + rel_file = file + + navigator = Navigator(graph) + + info = navigator.get_file_info(rel_file) + + console.print(Panel(f"{info}", title=f"Info: {rel_file.name}")) + + +@main.command("export") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +@click.option( + "--format", + "-f", + type=click.Choice(["json", "dot"]), + default="json", + help="Export format", +) +@click.option( + "--output", + "-o", + type=click.Path(path_type=Path), + default=Path("depgraph.json"), + help="Output file", +) +@click.pass_context +def export_command( + ctx: click.Context, + path: Path, + format: str, + output: Path, +): + """Export the dependency graph to a file.""" + graph = DependencyGraph(path) + graph.build_from_directory() + + renderer = ASCIIRenderer(console) + + if format == "json": + content = renderer.render_json(graph) + else: + content = renderer.render_dot(graph) + + output.write_text(content) + click.echo(f"Exported to {output}") + + +def _get_style_from_theme(theme: str) -> GraphStyle: + """Get a GraphStyle from a theme name.""" + themes = { + "default": GraphStyle( + node_style="cyan", + edge_style="dim", + highlight_style="yellow", + cycle_style="red", + ), + "dark": GraphStyle( + node_style="green", + edge_style="dim green", + highlight_style="white", + cycle_style="red", + ), + "light": GraphStyle( + node_style="blue", + edge_style="dim blue", + highlight_style="magenta", + cycle_style="red", + ), + "mono": GraphStyle( + node_style="", + edge_style="dim", + highlight_style="bold", + cycle_style="bold", + ), + } + return themes.get(theme, themes["default"]) + + +if __name__ == "__main__": + main()