Files
7000pctAUTO 0da90b336a
Some checks failed
CI / test (push) Failing after 5s
Initial commit: Add code-doc-cli project
2026-01-29 16:50:15 +00:00

279 lines
7.6 KiB
Python

"""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={})