From 4b144111736b9df7e09ed25f6151530f35a2e798 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 08:40:01 +0000 Subject: [PATCH] Initial upload: Auto README Generator CLI v0.1.0 --- src/auto_readme/cli.py | 365 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 src/auto_readme/cli.py diff --git a/src/auto_readme/cli.py b/src/auto_readme/cli.py new file mode 100644 index 0000000..5c8d098 --- /dev/null +++ b/src/auto_readme/cli.py @@ -0,0 +1,365 @@ +"""Main CLI interface for the Auto README Generator.""" + +import sys +from pathlib import Path +from typing import Optional + +import click +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +from auto_readme import __version__ +from auto_readme.models import Project, ProjectType, ProjectConfig, GitInfo, SourceFile, FileType +from auto_readme.parsers import DependencyParserFactory +from auto_readme.analyzers import CodeAnalyzerFactory +from auto_readme.utils import scan_project, get_git_info, FileScanner +from auto_readme.templates import TemplateRenderer +from auto_readme.config import ConfigLoader, ReadmeConfig +from auto_readme.interactive import run_wizard +from auto_readme.github import GitHubActionsGenerator + + +console = Console() + + +@click.group() +@click.version_option(version=__version__, prog_name="auto-readme") +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose output", +) +def main(verbose: bool): + """Auto README Generator - Automatically generate comprehensive README files.""" + pass + + +@main.command() +@click.option( + "--input", + "-i", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), + help="Input directory to analyze (default: current directory)", +) +@click.option( + "--output", + "-o", + type=click.Path(dir_okay=False, path_type=Path), + default=Path("README.md"), + help="Output file path for the generated README", +) +@click.option( + "--interactive", + "-I", + is_flag=True, + help="Run in interactive mode to customize the README", +) +@click.option( + "--template", + "-t", + type=click.Choice(["base", "minimal", "detailed"]), + default="base", + help="Template to use for generation", +) +@click.option( + "--config", + "-c", + type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), + help="Path to configuration file", +) +@click.option( + "--github-actions", + is_flag=True, + help="Generate GitHub Actions workflow for auto-updating README", +) +@click.option( + "--force", + "-f", + is_flag=True, + help="Force overwrite existing README", +) +@click.option( + "--dry-run", + is_flag=True, + help="Generate README but don't write to file", +) +def generate( + input: Path, + output: Path, + interactive: bool, + template: str, + config: Optional[Path], + github_actions: bool, + force: bool, + dry_run: bool, +): + """Generate a README.md file for your project.""" + try: + if output.exists() and not force and not dry_run: + if not click.confirm(f"File {output} already exists. Overwrite?"): + click.echo("Aborted.") + sys.exit(0) + + project = analyze_project(input) + + if config: + project_config = ConfigLoader.load(config) + if project_config.project_name: + if not project.config: + project.config = ProjectConfig(name=project_config.project_name) + else: + project.config.name = project_config.project_name + if project_config.description: + project.config.description = project_config.description + + if interactive: + project = run_wizard(project) + + renderer = TemplateRenderer() + readme_content = renderer.render(project, template_name=template) + + if dry_run: + click.echo(readme_content) + else: + output.write_text(readme_content) + click.echo(f"Successfully generated README.md at {output}") + + if github_actions: + if GitHubActionsGenerator.can_generate(project): + workflow_path = GitHubActionsGenerator.save_workflow(project, input) + click.echo(f"Generated workflow at {workflow_path}") + else: + click.echo("GitHub Actions workflow not generated: Not a GitHub repository or missing owner info.") + elif not dry_run and click.get_current_context().params.get("interactive", False) is False: + pass # Skip prompt in non-interactive mode + + except Exception as e: + console.print(Panel(f"Error: {e}", style="red")) + sys.exit(1) + + +@main.command() +@click.option( + "--output", + "-o", + type=click.Path(dir_okay=False, path_type=Path), + default=Path(".readmerc"), + help="Output file path for the configuration template", +) +def init_config(output: Path): + """Generate a template configuration file.""" + from auto_readme.config import ConfigValidator + + template = ConfigValidator.generate_template() + output.write_text(template) + click.echo(f"Generated configuration template at {output}") + + +@main.command() +@click.option( + "--input", + "-i", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), + help="Directory to preview README for", +) +@click.option( + "--template", + "-t", + type=click.Choice(["base", "minimal", "detailed"]), + default="base", + help="Template to preview", +) +def preview(input: Path, template: str): + """Preview the generated README without writing to file.""" + try: + project = analyze_project(input) + renderer = TemplateRenderer() + readme_content = renderer.render(project, template_name=template) + click.echo(readme_content) + except Exception as e: + console.print(Panel(f"Error: {e}", style="red")) + sys.exit(1) + + +@main.command() +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), + default=Path("."), +) +def analyze(path: Path): + """Analyze a project and display information.""" + try: + project = analyze_project(path) + + info = [ + f"Project: {project.config.name if project.config else 'Unknown'}", + f"Type: {project.project_type.value}", + f"Files: {len(project.files)}", + f"Source files: {len(project.source_files())}", + f"Test files: {len(project.test_files())}", + f"Dependencies: {len(project.dependencies)}", + f"Dev dependencies: {len(project.dev_dependencies)}", + f"Classes: {len(project.all_classes())}", + f"Functions: {len(project.all_functions())}", + ] + + if project.git_info and project.git_info.is_repo: + info.append(f"Git repository: {project.git_info.remote_url}") + + click.echo("\n".join(info)) + + except Exception as e: + console.print(Panel(f"Error: {e}", style="red")) + sys.exit(1) + + +def analyze_project(path: Path) -> Project: + """Analyze a project and return a Project object.""" + scan_result = scan_project(path) + + project_type = scan_result.project_type + if project_type == ProjectType.UNKNOWN: + project_type = detect_project_type(path) + + git_info = get_git_info(path) + + project = Project( + root_path=scan_result.root_path, + project_type=project_type, + git_info=git_info, + files=scan_result.files, + ) + + project.config = parse_project_config(path, project_type) + + dependencies = parse_dependencies(path, project_type) + project.dependencies = [d for d in dependencies if not d.is_dev] + project.dev_dependencies = [d for d in dependencies if d.is_dev] + + project = analyze_code(path, project) + + return project + + +def detect_project_type(path: Path) -> ProjectType: + """Detect the project type based on marker files.""" + markers = { + "pyproject.toml": ProjectType.PYTHON, + "setup.py": ProjectType.PYTHON, + "requirements.txt": ProjectType.PYTHON, + "package.json": ProjectType.JAVASCRIPT, + "go.mod": ProjectType.GO, + "Cargo.toml": ProjectType.RUST, + } + + for marker, project_type in markers.items(): + if (path / marker).exists(): + return project_type + + return ProjectType.UNKNOWN + + +def parse_project_config(path: Path, project_type: ProjectType) -> Optional[ProjectConfig]: + """Parse project configuration from marker files.""" + config = ProjectConfig(name=path.name) + + if project_type == ProjectType.PYTHON: + pyproject = path / "pyproject.toml" + if pyproject.exists(): + try: + import tomllib + with open(pyproject, "rb") as f: + data = tomllib.load(f) + if "project" in data: + project_data = data["project"] + config.name = project_data.get("name", config.name) + config.version = project_data.get("version") + config.description = project_data.get("description") + config.license = project_data.get("license") + config.homepage = project_data.get("urls", {}).get("Homepage") + except Exception: + pass + + elif project_type == ProjectType.JAVASCRIPT: + package_json = path / "package.json" + if package_json.exists(): + try: + import json + with open(package_json) as f: + data = json.load(f) + config.name = data.get("name", config.name) + config.version = data.get("version") + config.description = data.get("description") + config.license = data.get("license") + except Exception: + pass + + elif project_type == ProjectType.GO: + go_mod = path / "go.mod" + if go_mod.exists(): + try: + content = go_mod.read_text() + for line in content.splitlines(): + if line.startswith("module "): + config.name = line.replace("module ", "").strip() + elif line.startswith("go "): + config.version = line.replace("go ", "").strip() + except Exception: + pass + + elif project_type == ProjectType.RUST: + cargo_toml = path / "Cargo.toml" + if cargo_toml.exists(): + try: + import tomllib + with open(cargo_toml, "rb") as f: + data = tomllib.load(f) + if "package" in data: + pkg = data["package"] + config.name = pkg.get("name", config.name) + config.version = pkg.get("version") + config.description = pkg.get("description") + config.license = pkg.get("license") + except Exception: + pass + + return config + + +def parse_dependencies(path: Path, project_type: ProjectType) -> list: + """Parse project dependencies.""" + dependencies = [] + + for parser in DependencyParserFactory.get_all_parsers(): + for dep_file in path.rglob("*"): + if parser.can_parse(dep_file): + deps = parser.parse(dep_file) + dependencies.extend(deps) + + return dependencies + + +def analyze_code(path: Path, project: Project) -> Project: + """Analyze source code to extract functions, classes, and imports.""" + for source_file in project.files: + if source_file.file_type != FileType.SOURCE: + continue + + analyzer = CodeAnalyzerFactory.get_analyzer(Path(path / source_file.path)) + if analyzer: + full_path = path / source_file.path + analysis = analyzer.analyze(full_path) + source_file.functions = analysis.get("functions", []) + source_file.classes = analysis.get("classes", []) + source_file.imports = analysis.get("imports", []) + + return project + + +if __name__ == "__main__": + main() \ No newline at end of file