"""CLI interface using Typer.""" import sys from pathlib import Path from typing import Optional import typer from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn from rich.table import Table from rich.panel import Panel from rich.text import Text from docgen import __version__ from docgen.models import DocConfig, OutputFormat, Endpoint from docgen.detectors import PythonDetector, JavaScriptDetector, GoDetector, RustDetector from docgen.generators import HTMLGenerator, MarkdownGenerator, OpenAPIGenerator app = typer.Typer( name="docgen", help="Auto-generate beautiful API documentation from your codebase", add_completion=False, ) console = Console() def get_detectors(): """Get all available detectors.""" return [PythonDetector(), JavaScriptDetector(), GoDetector(), RustDetector()] def scan_for_endpoints( input_dir: Path, framework: Optional[str] = None, recursive: bool = True, ) -> list[Endpoint]: """Scan directory for API endpoints.""" endpoints = [] detectors = get_detectors() with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), TaskProgressColumn(), console=console, ) as progress: task = progress.add_task("Scanning for endpoints...", total=None) for detector in detectors: if framework and detector.framework_name != framework: continue try: found = detector.scan_directory(input_dir, recursive) endpoints.extend(found) except Exception: continue progress.update(task, completed=True) return endpoints def display_summary(endpoints: list[Endpoint]) -> None: """Display a summary of detected endpoints.""" if not endpoints: console.print(Panel("[yellow]No endpoints found.[/yellow]", title="Result")) return table = Table(title="Detected Endpoints") table.add_column("Method", style="bold") table.add_column("Path", style="cyan") table.add_column("Summary", style="dim") for endpoint in endpoints[:20]: method_color = { "GET": "blue", "POST": "green", "PUT": "orange3", "PATCH": "turquoise2", "DELETE": "red", }.get(endpoint.method.value, "white") table.add_row( f"[{method_color}]{endpoint.method.value}[/]", endpoint.path, endpoint.summary or "-", ) if len(endpoints) > 20: table.add_row("...", "...", f"[dim]({len(endpoints) - 20} more)[/]") console.print(Panel(table, title=f"Found {len(endpoints)} Endpoints")) @app.command("version") def show_version(): """Show the DocGen version.""" console.print(f"[bold]DocGen-CLI[/bold] v{__version__}") @app.command("detect") def detect_command( input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"), framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Specific framework to use (python, javascript, go, rust)"), recursive: bool = typer.Option(True, "--no-recursive", help="Disable recursive scanning"), ): """Detect API endpoints in source code.""" if not input_dir.exists(): console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]") raise typer.Exit(1) endpoints = scan_for_endpoints(input_dir, framework, recursive) display_summary(endpoints) @app.command("generate") def generate_command( input_dir: Path = typer.Argument(Path("."), help="Directory to scan for endpoints"), output_dir: Path = typer.Option(Path("docs"), "--output", "-o", help="Output directory for documentation"), format: OutputFormat = typer.Option(OutputFormat.HTML, "--format", "-F", help="Output format"), theme: str = typer.Option("default", "--theme", "-t", help="Theme for HTML output"), framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Specific framework to use"), title: str = typer.Option("API Documentation", "--title", help="Documentation title"), description: str = typer.Option("", "--description", "-d", help="Documentation description"), version: str = typer.Option("1.0.0", "--version", "-v", help="API version"), verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"), ): """Generate API documentation.""" if not input_dir.exists(): console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]") raise typer.Exit(1) config = DocConfig( input_dir=input_dir, output_dir=output_dir, format=format, theme=theme, framework=framework, title=title, description=description, version=version, verbose=verbose, ) endpoints = scan_for_endpoints(input_dir, framework) if verbose and endpoints: display_summary(endpoints) if not endpoints: console.print("[yellow]No endpoints found. Generating empty documentation.[/yellow]") console.print(f"[cyan]Generating {format.value} documentation...[/cyan]") if format == OutputFormat.HTML: generator = HTMLGenerator(config) elif format == OutputFormat.MARKDOWN: generator = MarkdownGenerator(config) else: generator = OpenAPIGenerator(config) output_path = generator.generate(endpoints, output_dir) console.print(f"[green]Documentation generated: {output_path}[/green]") @app.command("serve") def serve_command( input_dir: Path = typer.Argument(Path("docs"), help="Directory containing documentation to serve"), host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"), port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"), reload: bool = typer.Option(True, "--no-reload", help="Enable/disable auto-reload on changes"), ): """Start a local development server with live reload.""" import uvicorn import asyncio from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler if not input_dir.exists(): console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]") raise typer.Exit(1) class DocsReloadHandler(FileSystemEventHandler): def __init__(self): self._reload_needed = False def on_any_event(self, event): self._reload_needed = True console.print(Panel(f"[bold]DocGen Server[/bold]\n\nListening on http://{host}:{port}", title="Started")) config = uvicorn.Config("docgen.cli:reload_app", host=host, port=port, reload=reload) server = uvicorn.Server(config) server.run() @app.command("init") def init_command( output_dir: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"), ): """Initialize DocGen configuration file.""" config_content = '''# DocGen Configuration # Delete this file if you prefer zero-config operation [tool.docgen] input-dir = "." output-dir = "docs" format = "html" theme = "default" title = "API Documentation" version = "1.0.0" # Framework detection is automatic, but you can specify: # framework = "python" # python, javascript, go, rust ''' config_path = output_dir / "docgen.toml" config_path.write_text(config_content) console.print(f"[green]Configuration file created: {config_path}[/green]") def main(): """Main entry point.""" app() def reload_app(): """App for uvicorn reload.""" from starlette.applications import Starlette from starlette.routing import Route from starlette.responses import FileResponse async def serve_static(request): path = request.path_params.get("path", "index.html") return FileResponse("docs/" + path) routes = [ Route("/{path:path}", serve_static), ] return Starlette(routes=routes) if __name__ == "__main__": main()