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