Initial upload: Auto README Generator CLI v0.1.0
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
2026-02-05 08:40:01 +00:00
parent 71c99680e8
commit 4b14411173

365
src/auto_readme/cli.py Normal file
View File

@@ -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()