diff --git a/.code_doc_cli/cli/main.py b/.code_doc_cli/cli/main.py new file mode 100644 index 0000000..0b47e23 --- /dev/null +++ b/.code_doc_cli/cli/main.py @@ -0,0 +1,278 @@ +"""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={})