245
src/docgen/cli.py
Normal file
245
src/docgen/cli.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user