import click from pathlib import Path from typing import Sequence, Optional from docforge.loaders import GriffeLoader from docforge.cli import mkdocs_utils from docforge.cli import mcp_utils @click.group() def cli() -> None: """ Root command group for the doc-forge CLI. Provides commands for building, serving, and inspecting documentation generated from Python source code. """ pass @cli.command() @click.option("--mcp", is_flag=True, help="Build MCP resources") @click.option("--mkdocs", is_flag=True, help="Build MkDocs site") @click.option("--module-is-source", is_flag=True, help="Module is source folder and to be treated as root folder") @click.option("--module", help="Python module to document") @click.option("--project-name", help="Project name override") @click.option("--site-name", help="MkDocs site name") @click.option("--docs-dir", type=click.Path(path_type=Path), default=Path("docs"), help="Directory for MD sources") @click.option("--nav", "nav_file", type=click.Path(path_type=Path), default=Path("docforge.nav.yml"), help="Nav spec path") @click.option("--template", type=click.Path(path_type=Path), help="MkDocs template path") @click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="Output config path") @click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP output directory") def build( mcp: bool, mkdocs: bool, module_is_source: bool, module: Optional[str], project_name: Optional[str], site_name: Optional[str], docs_dir: Path, nav_file: Path, template: Optional[Path], mkdocs_yml: Path, out_dir: Path, ) -> None: """ Build documentation artifacts. This command performs the full documentation build pipeline: 1. Introspects the Python project using Griffe 2. Generates renderer-specific documentation sources 3. Optionally builds the final documentation output Depending on the selected options, the build can target: - MkDocs static documentation sites - MCP structured documentation resources Args: mcp: Enable MCP documentation generation. mkdocs: Enable MkDocs documentation generation. module_is_source: Treat the specified module directory as the project root. module: Python module import path to document. project_name: Optional override for the project name. site_name: Display name for the MkDocs site. docs_dir: Directory where Markdown documentation sources will be generated. nav_file: Path to the navigation specification file. template: Optional custom MkDocs configuration template. mkdocs_yml: Output path for the generated MkDocs configuration. out_dir: Output directory for generated MCP resources. Raises: click.UsageError: If required options are missing or conflicting. """ if not mcp and not mkdocs: raise click.UsageError("Must specify either --mcp or --mkdocs") if mkdocs: if not module: raise click.UsageError("--module is required for MkDocs build") if not site_name: site_name = module click.echo(f"Generating MkDocs sources in {docs_dir}...") mkdocs_utils.generate_sources( module, docs_dir, project_name, module_is_source, ) click.echo(f"Generating MkDocs config {mkdocs_yml}...") mkdocs_utils.generate_config(docs_dir, nav_file, template, mkdocs_yml, site_name) click.echo("Running MkDocs build...") mkdocs_utils.build(mkdocs_yml) click.echo("MkDocs build completed.") if mcp: if not module: raise click.UsageError("--module is required for MCP build") click.echo(f"Generating MCP resources in {out_dir}...") mcp_utils.generate_resources(module, project_name, out_dir) click.echo("MCP build completed.") @cli.command() @click.option("--mcp", is_flag=True, help="Serve MCP documentation") @click.option("--mkdocs", is_flag=True, help="Serve MkDocs site") @click.option("--module", help="Python module to serve") @click.option("--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), help="MkDocs config path") @click.option("--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), help="MCP root directory") def serve( mcp: bool, mkdocs: bool, module: Optional[str], mkdocs_yml: Path, out_dir: Path, ) -> None: """ Serve generated documentation locally. Depending on the selected mode, this command starts either: - A MkDocs development server for browsing documentation - An MCP server exposing structured documentation resources Args: mcp: Serve documentation using the MCP server. mkdocs: Serve the MkDocs development site. module: Python module import path to serve via MCP. mkdocs_yml: Path to the MkDocs configuration file. out_dir: Root directory containing MCP documentation resources. Raises: click.UsageError: If invalid or conflicting options are provided. """ if mcp and mkdocs: raise click.UsageError("Cannot specify both --mcp and --mkdocs") if not mcp and not mkdocs: raise click.UsageError("Must specify either --mcp or --mkdocs") if mcp and not module: raise click.UsageError("--module is required for MCP serve") if mkdocs: mkdocs_utils.serve(mkdocs_yml) elif mcp: mcp_utils.serve(module, out_dir) @cli.command() @click.option( "--module", required=True, help="Python module import path to introspect", ) @click.option( "--project-name", help="Project name (defaults to specified module)", ) def tree( module: str, project_name: Optional[str], ) -> None: """ Display the documentation object tree for a module. This command introspects the specified module and prints a hierarchical representation of the discovered documentation objects, including modules, classes, functions, and members. Args: module: Python module import path to introspect. project_name: Optional name to display as the project root. """ loader = GriffeLoader() project = loader.load_project([module], project_name) click.echo(project.name) for module in project.get_all_modules(): click.echo(f"├── {module.path}") for obj in module.get_all_objects(): _print_object(obj, indent="│ ") def _print_object(obj, indent: str) -> None: """ Recursively print a documentation object and its members. This helper function traverses the documentation object graph and prints each object with indentation to represent hierarchy. Args: obj: Documentation object to print. indent: Current indentation prefix used for nested members. """ click.echo(f"{indent}├── {obj.name}") for member in obj.get_all_members(): _print_object(member, indent + "│ ")