Initial upload: Auto README Generator CLI v0.1.0
This commit is contained in:
365
src/auto_readme/cli.py
Normal file
365
src/auto_readme/cli.py
Normal 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()
|
||||
Reference in New Issue
Block a user