Files
docgen-cli/src/docgen/cli.py
7000pctAUTO 7d3a5c5f09
Some checks failed
CI / test (push) Failing after 9s
Add CLI module
2026-01-31 17:10:02 +00:00

246 lines
7.9 KiB
Python

"""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()