""" # 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` each emit their own MkDocs config and build (`docs/mkdocs.{kind}.yml` into `site/{kind}`); `--mcp` generates a machine-readable bundle independently. --- """ import os 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 the lib MkDocs site") @click.option("--api", is_flag=True, help="Build API docs from an OpenAPI spec") @click.option( "--wiki", is_flag=True, help="Build a hand-written wiki as its own MkDocs site" ) @click.option( "--refresh", is_flag=True, help="Regenerate existing docs/mkdocs.{kind}.yml configs from templates", ) @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 for lib and wiki sites") @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 built as its own 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( "--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, refresh: 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, 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 the final output. Depending on the selected options, the build can target: - A lib MkDocs site (`--mkdocs`) for library reference docs - A swagger-enabled API MkDocs site (`--api`) built from an OpenAPI spec - A wiki MkDocs site (`--wiki`) built from hand-written markdown - MCP structured documentation resources (`--mcp`) Each enabled site kind produces its own MkDocs configuration (`docs/mkdocs.{kind}.yml`) and its own build (`site/{kind}`). Notes: - At least one of `--mcp`, `--mkdocs`, `--wiki`, or `--api` must be provided. - `--mkdocs`, `--api`, and `--wiki` emit independent MkDocs builds, while `--mcp` emits a machine-readable bundle. - Configuration files are generated only when absent; an existing `docs/mkdocs.{kind}.yml` is used as-is. Pass `--refresh` to rebaseline it from the templates. Args: mcp (bool): Enable MCP documentation generation. mkdocs (bool): Enable the lib MkDocs documentation generation. api (bool): Enable API documentation generation from an OpenAPI spec. wiki (bool): Build a hand-written wiki directory as its own MkDocs site. refresh (bool): Regenerate ``docs/mkdocs.{kind}.yml`` from templates even when it already exists. By default, existing configs are used as-is. 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 lib and wiki MkDocs sites. docs_dir (Path): Shared documentation root used for generated sources. 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. 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 and not module: raise click.UsageError("--module is required for MkDocs build") if mcp and not module: raise click.UsageError("--module is required for MCP build") spec: dict | None = None if api: spec = api_utils.load_openapi_spec(openapi_spec) kinds: list[str] = [] if mkdocs: kinds.append("lib") 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=Path(".").resolve(), ) if api: kinds.append("api") api_dir = docs_dir / "api" click.echo(f"Generating API sources in {api_dir}...") api_utils.generate_api_sources(spec, api_dir) if wiki: kinds.append("wiki") if kinds: api_metadata: api_utils.OpenAPIMetadata | None = None if api: api_metadata = api_utils.derive_metadata(spec) theme_icon = mkdocs_utils.load_spec_icon(nav_file) config_paths: list[Path] = [] for kind in kinds: out = docs_dir / f"mkdocs.{kind}.yml" if out.exists() and not refresh: click.echo( f"Using existing MkDocs config {out} (run --refresh to rebaseline)..." ) config_paths.append(out) continue kind_root = wiki_dir if kind == "wiki" else docs_dir / kind site_name_kind = site_name or module or Path.cwd().name site_description: str | None = None site_author: str | None = None if kind == "api" and api_metadata: site_name_kind = api_metadata.site_name site_description = api_metadata.site_description site_author = api_metadata.site_author if kind == "lib": nav_block, _kind_icon = mkdocs_utils.build_lib_nav(nav_file, docs_dir) elif kind == "wiki": nav_block = mkdocs_utils.build_wiki_nav_block(wiki_dir) else: nav_block = [{"API Reference": "index.md"}] click.echo(f"Generating MkDocs config {out}...") mkdocs_utils.generate_site_config( kind, kind_root, nav_block, out, site_name_kind, Path(os.path.relpath(kind_root, docs_dir)).as_posix(), Path(os.path.relpath(Path.cwd() / "site" / kind, docs_dir)).as_posix(), template=template, site_description=site_description, site_author=site_author, theme_icon=theme_icon, ) config_paths.append(out) click.echo("Running MkDocs builds...") mkdocs_utils.build_configs(config_paths) if mcp: 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 an MkDocs site from --mkdocs-yml") @click.option("--lib", is_flag=True, help="Serve the lib MkDocs site") @click.option("--api", is_flag=True, help="Serve the API MkDocs site") @click.option("--wiki", is_flag=True, help="Serve the wiki MkDocs site") @click.option("--module", help="Python module to serve") @click.option( "--mkdocs-yml", type=click.Path(path_type=Path), default=Path("docs/mkdocs.wiki.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, lib: bool, api: bool, wiki: 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 a site, or - An MCP server exposing structured documentation resources The kind flags (`--lib`, `--api`, `--wiki`) select the generated per-kind config (`docs/mkdocs.{kind}.yml`); `--mkdocs` serves the config passed via `--mkdocs-yml`. Args: mcp (bool): Serve documentation using the MCP server. mkdocs (bool): Serve the MkDocs development site from ``--mkdocs-yml``. lib (bool): Serve the lib MkDocs site. api (bool): Serve the API MkDocs site. wiki (bool): Serve the wiki MkDocs 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. """ selected = [ name for name, enabled in ( ("mcp", mcp), ("mkdocs", mkdocs), ("lib", lib), ("api", api), ("wiki", wiki), ) if enabled ] if len(selected) != 1: raise click.UsageError( "Must specify exactly one of --mcp, --mkdocs, --lib, --api, --wiki" ) if mcp: if not module: raise click.UsageError("--module is required for MCP serve") mcp_utils.serve(module, out_dir) return config = mkdocs_yml if lib: config = Path("docs/mkdocs.lib.yml") elif api: config = Path("docs/mkdocs.api.yml") elif wiki: config = Path("docs/mkdocs.wiki.yml") mkdocs_utils.serve(config) @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 + "│ ")