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: """ doc-forge CLI: A tool for introspecting Python projects and generating documentation. """ 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", help="Python module to document") @click.option("--project-name", help="Project name override") # MkDocs specific @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") # MCP specific @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: 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 (MkDocs site or MCP resources). """ 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, project_name, docs_dir) 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("--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, mkdocs_yml: Path, out_dir: Path, ) -> None: """ Serve documentation. """ 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 mkdocs: mkdocs_utils.serve(mkdocs_yml) elif mcp: mcp_utils.serve(out_dir) @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. """ 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. """ click.echo(f"{indent}├── {obj.name}") for member in obj.get_all_members(): _print_object(member, indent + "│ ")