""" Main entry point for the doc-forge CLI. This module defines the core command group and the 'tree', 'generate', 'build', and 'serve' commands. """ from pathlib import Path from typing import Sequence, Optional import click from docforge.loaders import GriffeLoader, discover_module_paths from docforge.renderers import MkDocsRenderer, MCPRenderer from docforge.cli.mkdocs import mkdocs_cmd @click.group() def cli() -> None: """ doc-forge CLI: A tool for introspecting Python projects and generating documentation. """ pass cli.add_command(mkdocs_cmd) # --------------------------------------------------------------------- # tree # --------------------------------------------------------------------- @cli.command() @click.option( "--modules", multiple=True, required=True, help="Python module import paths to introspect", ) @click.option( "--project-name", help="Project name (defaults to first module)", ) def tree( modules: Sequence[str], project_name: Optional[str], ) -> None: """ Visualize the project structure including modules and their members. Args: modules: List of module paths to introspect. project_name: Optional project name override. """ loader = GriffeLoader() project = loader.load_project(list(modules), 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: """ Recursive helper to print doc objects and their members to the console. Args: obj: The DocObject to print. indent: The current line indentation string. """ click.echo(f"{indent}├── {obj.name}") for member in obj.get_all_members(): _print_object(member, indent + "│ ") # --------------------------------------------------------------------- # generate # --------------------------------------------------------------------- @cli.command() @click.option( "--module", required=True, help="Python module import paths to document", ) @click.option( "--project-name", help="Project name (defaults to first module)", ) @click.option( "--docs-dir", type=click.Path(path_type=Path), default=Path("docs"), ) def generate( module: str, project_name: Optional[str], docs_dir: Path, ) -> None: """ Generate Markdown source files for the specified module. Args: module: The primary module path to document. project_name: Optional project name override. docs_dir: Directory where documentation sources will be written. """ loader = GriffeLoader() discovered_paths = discover_module_paths( module, ) project = loader.load_project( discovered_paths, project_name ) renderer = MkDocsRenderer() renderer.generate_sources(project, docs_dir) click.echo(f"Documentation sources generated in {docs_dir}") # --------------------------------------------------------------------- # mcp-build # --------------------------------------------------------------------- @cli.command(name="generate-mcp") @click.option( "--module", required=True, help="Python module import path to document", ) @click.option( "--project-name", help="Project name (defaults to first module)", ) @click.option( "--out-dir", type=click.Path(path_type=Path), default=Path("mcp_docs"), ) def generate_mcp( module: str, project_name: str | None, out_dir: Path, ) -> None: """ Generate MCP-compatible documentation resources for the specified module. Args: module: The primary module path to document. project_name: Optional project name override. out_dir: Directory where MCP resources will be written. """ loader = GriffeLoader() discovered_paths = discover_module_paths(module) project = loader.load_project( discovered_paths, project_name, ) renderer = MCPRenderer() renderer.generate_sources(project, out_dir) click.echo(f"MCP documentation resources generated in {out_dir}") # --------------------------------------------------------------------- # build # --------------------------------------------------------------------- @cli.command() @click.option( "--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), ) def build(mkdocs_yml: Path) -> None: """ Build the documentation site using MkDocs. Args: mkdocs_yml: Path to the mkdocs.yml configuration file. """ if not mkdocs_yml.exists(): raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}") from mkdocs.config import load_config from mkdocs.commands.build import build as mkdocs_build mkdocs_build(load_config(str(mkdocs_yml))) click.echo("MkDocs build completed") # --------------------------------------------------------------------- # serve-mcp # --------------------------------------------------------------------- @cli.command(name="serve-mcp") def serve_mcp() -> None: """ Serve MCP documentation from the local mcp_docs directory. """ from docforge.servers import MCPServer mcp_root = Path.cwd() / "mcp_docs" if not mcp_root.exists(): raise click.ClickException("mcp_docs directory not found") required = [ mcp_root / "index.json", mcp_root / "nav.json", mcp_root / "modules", ] for path in required: if not path.exists(): raise click.ClickException(f"Invalid MCP bundle, missing: {path.name}") server = MCPServer( mcp_root=mcp_root, name="doc-forge-mcp", ) server.run() # --------------------------------------------------------------------- # serve # --------------------------------------------------------------------- @cli.command() @click.option( "--mkdocs-yml", type=click.Path(path_type=Path), default=Path("mkdocs.yml"), ) def serve(mkdocs_yml: Path) -> None: """ Serve the documentation site with live-reload using MkDocs. Args: mkdocs_yml: Path to the mkdocs.yml configuration file. """ if not mkdocs_yml.exists(): raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}") from mkdocs.commands.serve import serve as mkdocs_serve host = "127.0.0.1" port = 8000 url = f"http://{host}:{port}/" click.echo(f"Serving documentation at {url}") mkdocs_serve(config_file=str(mkdocs_yml)) # --------------------------------------------------------------------- # entry point # --------------------------------------------------------------------- def main() -> None: """ CLI Entry point. Boots the click application. """ cli() if __name__ == "__main__": main()