diff --git a/src/docgen/cli.py b/src/docgen/cli.py new file mode 100644 index 0000000..8b8d746 --- /dev/null +++ b/src/docgen/cli.py @@ -0,0 +1,245 @@ +"""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()