"""Command-line interface for code-doc-cli.""" import os import sys import json from pathlib import Path from typing import Optional import click from datetime import datetime from ..parsers.registry import ParserRegistry from ..generators.markdown_generator import MarkdownGenerator from ..utils.file_utils import find_files, read_file_safe, ensure_directory_exists from ..utils.config import load_config, Config class ExitCode: SUCCESS = 0 NO_FILES = 1 PARSE_ERROR = 2 INVALID_CONFIG = 3 WARNING = 4 @click.group() @click.option( "--config", "-c", type=click.Path(exists=True), help="Path to configuration file", ) @click.option( "--language", "-l", type=click.Choice(["python", "typescript", "go", "auto"]), default="auto", help="Programming language", ) @click.option( "--output", "-o", type=click.Path(), help="Output file path", ) @click.option( "--format", "-f", type=click.Choice(["markdown", "json"]), default="markdown", help="Output format", ) @click.option( "--fail-on-warning", is_flag=True, default=False, help="Exit with error code on warnings", ) @click.option( "--exclude", multiple=True, help="Exclude patterns (can be used multiple times)", ) @click.option( "--include-private", is_flag=False, help="Include private members", ) @click.pass_context def cli( ctx: click.Context, config: Optional[str], language: str, output: Optional[str], format: str, fail_on_warning: bool, exclude: tuple, include_private: bool, ) -> None: """code-doc-cli: Auto-generate documentation from code.""" ctx.ensure_object(dict) loaded_config = load_config(config) if not config and not output: output = loaded_config.output_file ctx.obj["config"] = loaded_config ctx.obj["language"] = language if language != "auto" else loaded_config.language ctx.obj["output"] = output ctx.obj["format"] = format ctx.obj["fail_on_warning"] = fail_on_warning or loaded_config.fail_on_warning ctx.obj["exclude"] = list(exclude) or loaded_config.exclude_patterns ctx.obj["include_private"] = include_private or loaded_config.include_private @cli.command() @click.argument( "paths", type=click.Path(exists=True), nargs=-1, ) @click.option( "--pattern", "-p", multiple=True, help="File patterns to match (default: **/*.py, **/*.ts, **/*.go)", ) @click.pass_context def generate( ctx: click.Context, paths: tuple[str], pattern: tuple[str], ) -> None: """Generate documentation from source files.""" config: Config = ctx.obj["config"] language: Optional[str] = ctx.obj["language"] output: Optional[str] = ctx.obj["output"] output_format: str = ctx.obj["format"] fail_on_warning: bool = ctx.obj["fail_on_warning"] exclude_patterns: list[str] = ctx.obj["exclude"] include_private: bool = ctx.obj["include_private"] warnings = [] errors = [] if pattern: patterns = list(pattern) else: patterns = config.input_patterns files = set() for path in paths: path_obj = Path(path) if path_obj.is_file(): files.add(str(path_obj.resolve())) else: for pattern in patterns: for match in find_files(pattern, base_path=str(path_obj)): if match not in files: matches_exclude = False for exclude_pat in exclude_patterns: if exclude_pat in match: matches_exclude = True break if not matches_exclude: files.add(match) if not files: click.echo("Error: No source files found matching the specified patterns.", err=True) sys.exit(ExitCode.NO_FILES) click.echo(f"Found {len(files)} source file(s) to process.") all_elements = [] for file_path in sorted(files): file_ext = os.path.splitext(file_path)[1].lower() detected_lang = ParserRegistry.get_language_from_extension(file_path) if detected_lang is None: warnings.append(f"Skipping unsupported file: {file_path}") continue try: parser = ParserRegistry.get_parser(file_path, language if language != "auto" else None) elements = parser.parse() filtered_elements = [] for elem in elements: if include_private or elem.visibility == "public": if elem.element_type.value != "module": filtered_elements.append(elem) else: filtered_elements.append(elem) all_elements.extend(filtered_elements) except Exception as e: errors.append(f"Error parsing {file_path}: {e}") if warnings and fail_on_warning: for warning in warnings: click.echo(f"Warning: {warning}", err=True) sys.exit(ExitCode.WARNING) for warning in warnings: click.echo(f"Warning: {warning}", err=True) if errors: for error in errors: click.echo(f"Error: {error}", err=True) sys.exit(ExitCode.PARSE_ERROR) if not all_elements: click.echo("No documentation elements found.", err=True) sys.exit(ExitCode.NO_FILES) generator = MarkdownGenerator( template_style=config.template_style, theme=config.theme, syntax_highlighting=config.syntax_highlighting, ) if output_format == "json": output_content = generator.generate_json(all_elements) else: output_content = generator.generate( elements=all_elements, title="API Documentation", include_metadata=True, ) if output: ensure_directory_exists(os.path.dirname(output)) with open(output, "w", encoding="utf-8") as f: f.write(output_content) click.echo(f"Documentation written to: {output}") else: click.echo(output_content) sys.exit(ExitCode.SUCCESS) @cli.command() @click.pass_context def languages(ctx: click.Context) -> None: """List supported languages and file extensions.""" languages_list = ParserRegistry.get_supported_languages() extensions = ParserRegistry.get_supported_extensions() click.echo("Supported Languages:") for lang in sorted(set(languages_list)): click.echo(f" - {lang}") click.echo("") click.echo("Supported Extensions:") for ext in sorted(set(extensions)): click.echo(f" - {ext}") @cli.command() @click.pass_context def init(ctx: click.Context) -> None: """Initialize a configuration file.""" config_content = '''[tool.code-doc] input_patterns = ["**/*.py", "**/*.ts", "**/*.go"] output_file = "docs/API.md" language = "auto" template_style = "default" syntax_highlighting = true theme = "default" fail_on_warning = false exclude_patterns = ["**/test_*", "**/*_test.go"] include_private = false output_format = "markdown" ''' config_path = "code-doc.toml" if os.path.exists(config_path): if not click.confirm(f"{config_path} already exists. Overwrite?"): return with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) click.echo(f"Configuration file created: {config_path}") @cli.command() @click.pass_context def version(ctx: click.Context) -> None: """Display version information.""" from .. import __version__ click.echo(f"code-doc-cli version: {__version__}") def main() -> None: """Main entry point.""" cli(obj={})