diff --git a/src/docgen/cli.py b/src/docgen/cli.py index 78636f5..abfaf25 100644 --- a/src/docgen/cli.py +++ b/src/docgen/cli.py @@ -1,265 +1,137 @@ -{"""CLI interface using Typer.""" +#!/usr/bin/env python3 +"""CLI for DocGen - API documentation generator.""" +import re from pathlib import Path from typing import Optional import typer from rich.console import Console from rich.panel import Panel -from rich.progress import ( - BarColumn, - Progress, - SpinnerColumn, - TaskProgressColumn, - TextColumn, -) -from rich.table import Table +from rich.text import Text from docgen import __version__ -from docgen.detectors import ( - GoDetector, - JavaScriptDetector, - PythonDetector, - RustDetector, -) -from docgen.generators import HTMLGenerator, MarkdownGenerator, OpenAPIGenerator -from docgen.models import DocConfig, Endpoint, OutputFormat +from docgen.detectors import get_detector +from docgen.generators import get_generator +from docgen.models import DocConfig, OutputFormat -app = typer.Typer( - name="docgen", - help="Auto-generate beautiful API documentation from your codebase", - add_completion=False, -) +app = typer.Typer(name="docgen", help="API Documentation Generator") console = Console() -def get_detectors(): - """Get all available detectors.""" - return [PythonDetector(), JavaScriptDetector(), GoDetector(), RustDetector()] +def validate_framework(value: str) -> Optional[str]: + """Validate and normalize framework name.""" + if value is None: + return None + frameworks = ["fastapi", "flask", "django", "express", "fastify", "gin", "chi", "actix"] + normalized = value.lower().replace("-", "").replace("_", "") + for framework in frameworks: + if framework in normalized or normalized in framework: + return framework + return value -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 detect_framework_auto(file_path: Path) -> Optional[str]: + """Auto-detect framework from file content.""" + content = file_path.read_text() + framework_patterns = { + "fastapi": [r"from fastapi", r"import fastapi", r"FastAPI\("], + "flask": [r"from flask", r"import flask", r"Flask\("], + "django": [r"from django", r"import django", r"path\(r"], + "express": [r"require\([\'"]express[\'"]\)", r"from [\'"]express[\'"]"], + "fastify": [r"from [\'"]@fastify", r"require\([\'"]fastify[\'"]\)"], + "gin": [r'"github.com/gin-gonic/gin"', r"gin\.Default\("], + "chi": [r'"github.com/go-chi/chi"', r"chi\.NewRouter\("], + "actix": [r"use actix_web", r"#[get\(", r"ActixWeb\("], + } + for framework, patterns in framework_patterns.items(): + for pattern in patterns: + if re.search(pattern, content): + return framework + return None -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)" +@app.command() +def main( + input_dir: Path = typer.Argument( + Path("."), help="Input directory to scan for source files" ), - 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"), + output_dir: Path = typer.Option(Path("docs"), "-o", "--output", help="Output directory"), format: OutputFormat = typer.Option( - OutputFormat.HTML, "--format", "-F", help="Output format" + OutputFormat.HTML, "-f", "--format", 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" + None, "-F", "--framework", help="Framework to use (auto-detected if not specified)" ), - 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) + title: str = typer.Option("API Documentation", "-t", "--title", help="Documentation title"), + description: str = typer.Option("", "-d", "--description", help="Description"), + theme: str = typer.Option("default", "-T", "--theme", help="Theme (default, dark, minimal)"), + verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"), + include_private: bool = typer.Option( + False, "--include-private", help="Include private endpoints" + ), +) -> None: + """Generate API documentation from source code.""" + console.print(Panel.fit("[bold blue]DocGen[/bold blue] - API Documentation Generator")) + console.print(f"\n[dim]Version: {__version__}[/dim]") + console.print(f"Input: {input_dir}") + console.print(f"Output: {output_dir}") + console.print(f"Format: {format.value}") config = DocConfig( input_dir=input_dir, output_dir=output_dir, format=format, - theme=theme, - framework=framework, title=title, description=description, - version=version, + theme=theme, verbose=verbose, + include_private=include_private, ) - endpoints = scan_for_endpoints(input_dir, framework) + if framework: + config.framework = validate_framework(framework) - if verbose and endpoints: - display_summary(endpoints) + try: + detector_class = get_detector(config.framework) + if not detector_class: + if config.framework: + console.print(f"[red]Error: Unknown framework: {config.framework}[/red]") + raise typer.Exit(1) + console.print("[yellow]Warning: Could not auto-detect framework, using Python[/yellow]") + from docgen.detectors import PythonDetector + detector_class = PythonDetector - if not endpoints: - console.print("[yellow]No endpoints found. Generating empty documentation.[/yellow]") + detector = detector_class(config) - console.print(f"[cyan]Generating {format.value} documentation...[/cyan]") + generator_class = get_generator(config.format) + if not generator_class: + console.print(f"[red]Error: Unknown format: {format}[/red]") + raise typer.Exit(1) - if format == OutputFormat.HTML: - generator = HTMLGenerator(config) - elif format == OutputFormat.MARKDOWN: - generator = MarkdownGenerator(config) - else: - generator = OpenAPIGenerator(config) + generator = generator_class(config) - output_path = generator.generate(endpoints, output_dir) - console.print(f"[green]Documentation generated: {output_path}[/green]") + endpoints = detector.scan_directory(input_dir) + if not endpoints: + console.print("[yellow]No endpoints detected.[/yellow]") + return + console.print(f"\n[green]Detected {len(endpoints)} endpoints[/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"), -): - """Start a local development server with live reload.""" - import uvicorn - from watchdog.events import FileSystemEventHandler + if verbose: + for ep in endpoints: + console.print(f" - {ep.method.value:6} {ep.path}") - if not input_dir.exists(): - console.print(f"[red]Error: Directory '{input_dir}' does not exist.[/red]") + output_path = generator.generate(endpoints, output_dir) + console.print(f"\n[green]Documentation generated: {output_path}[/green]") + + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + if verbose: + import traceback + console.print(traceback.format_exc()) raise typer.Exit(1) - class DocsReloadHandler(FileSystemEventHandler): - def __init__(self): - self._reload_needed = False - - def on_any_event(self, event): - self._reload_needed = True - - server_url = f"http://{host}:{port}" - title = "Started" - console.print( - Panel(f"[bold]DocGen Server[/bold]\n\nListening on {server_url}", title=title) - ) - - 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(): - """Run the main application.""" - app() - - -def reload_app(): - """App for uvicorn reload.""" - from starlette.applications import Starlette - from starlette.responses import FileResponse - from starlette.routing import Route - - 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() + app()