""" # Summary Command definitions for the doc-forge CLI. Provides the CLI structure using Click, including build, serve, and tree commands. --- Notes: - The `build` command validates requested modes before generating anything. - `--mkdocs`, `--api`, and `--wiki` share a single MkDocs build; `--mcp` generates a machine-readable bundle independently. --- """ from pathlib import Path import click from docforge.cli import api_utils, mcp_utils, mkdocs_utils from docforge.loaders import GriffeLoader from docforge.models import DocObject @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("--api", is_flag=True, help="Build API docs from an OpenAPI spec") @click.option( "--wiki", is_flag=True, help="Include a hand-written wiki in the 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( "--openapi-spec", type=click.Path(path_type=Path), help="Path to the OpenAPI JSON specification", ) @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="MkDocs documentation root", ) @click.option( "--wiki-dir", type=click.Path(path_type=Path), default=Path("docs/wiki"), help="Hand-written wiki directory included in the MkDocs site", ) @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("docs/mcp"), help="MCP output directory", ) def build( mcp: bool, mkdocs: bool, api: bool, wiki: bool, module_is_source: bool, module: str | None, openapi_spec: Path | None, project_name: str | None, site_name: str | None, docs_dir: Path, wiki_dir: Path, nav_file: Path, template: Path | None, mkdocs_yml: Path, out_dir: Path, ) -> None: """ Build documentation artifacts. This command runs the full documentation pipeline: it loads Python modules, generates renderer-specific documentation sources, and optionally builds or serves the final output. Depending on the selected options, the build can target: - MkDocs static documentation sites for library reference docs - Swagger-enabled API docs generated from an OpenAPI spec - Hand-written wiki pages included in the MkDocs site - MCP structured documentation resources Notes: - At least one of `--mcp`, `--mkdocs`, `--wiki`, or `--api` must be provided. - `--mkdocs`, `--api`, and `--wiki` are combined into a single MkDocs build, while `--mcp` emits a machine-readable bundle. Args: mcp (bool): Enable MCP documentation generation. mkdocs (bool): Enable MkDocs library documentation generation. api (bool): Enable API documentation generation from an OpenAPI spec. wiki (bool): Include a hand-written wiki directory in the MkDocs site. module_is_source (bool): Treat the specified module directory as the project root. module (str | None): Python module import path to document. openapi_spec (Path | None): Path to the OpenAPI JSON specification used for API docs. project_name (str | None): Optional override for the project name. site_name (str | None): Display name for the MkDocs site. docs_dir (Path): Shared documentation root used as the MkDocs ``docs_dir``. wiki_dir (Path): Directory containing hand-written wiki markdown files. nav_file (Path): Path to the navigation specification file. template (Path | None): Optional custom MkDocs configuration template. mkdocs_yml (Path): Output path for the generated MkDocs configuration. out_dir (Path): Output directory for generated MCP resources. Raises: click.UsageError: If required options are missing or conflicting. """ if not mcp and not mkdocs and not api and not wiki: raise click.UsageError("Must specify either --mcp, --mkdocs, --wiki, or --api") if api: if not openapi_spec: raise click.UsageError("--openapi-spec is required for API build") if site_name and not mkdocs and not wiki: raise click.UsageError( "--site-name cannot be overridden for API build; " "the OpenAPI spec provides the site name" ) if (mkdocs or mcp) and not module: raise click.UsageError( "--module is required for MkDocs build" if mkdocs else "--module is required for MCP build" ) spec: dict | None = None if api: spec = api_utils.load_openapi_spec(openapi_spec) if mkdocs: lib_dir = docs_dir / "lib" click.echo(f"Generating MkDocs sources in {lib_dir}...") mkdocs_utils.generate_sources( module, lib_dir, project_name, module_is_source, readme_dir=mkdocs_yml.parent, ) if api: api_dir = docs_dir / "api" click.echo(f"Generating API sources in {api_dir}...") api_utils.generate_api_sources(spec, api_dir) if mkdocs or api or wiki: modes: list[str] = [] if mkdocs: modes.append("lib") if api: modes.append("api") if wiki: modes.append("wiki") site_description: str | None = None site_author: str | None = None effective_site_name = site_name or module or Path.cwd().name if api: metadata = api_utils.derive_metadata(spec) effective_site_name = metadata.site_name site_description = metadata.site_description site_author = metadata.site_author click.echo(f"Generating MkDocs config {mkdocs_yml}...") mkdocs_utils.generate_config( docs_dir, nav_file, template, mkdocs_yml, effective_site_name, modes=modes, site_description=site_description, site_author=site_author, wiki_dir=wiki_dir if wiki else None, ) 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("docs/mcp"), help="MCP root directory", ) def serve( mcp: bool, mkdocs: bool, module: str | None, 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 (bool): Serve documentation using the MCP server. mkdocs (bool): Serve the MkDocs development site. module (str | None): Python module import path to serve via MCP. mkdocs_yml (Path): Path to the MkDocs configuration file. out_dir (Path): 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: str | None, ) -> 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 (str): Python module import path to introspect. project_name (str | None): 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: DocObject, 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 (DocObject): Documentation object to print. indent (str): Current indentation prefix used for nested members. """ click.echo(f"{indent}├── {obj.name}") for member in obj.get_all_members(): _print_object(member, indent + "│ ")