From 47de8512980afe6cc64e2a7a4e002685dc977244 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 05:37:08 +0000 Subject: [PATCH] Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD --- scaffoldforge/cli/commands.py | 209 ++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 scaffoldforge/cli/commands.py diff --git a/scaffoldforge/cli/commands.py b/scaffoldforge/cli/commands.py new file mode 100644 index 0000000..fa36b34 --- /dev/null +++ b/scaffoldforge/cli/commands.py @@ -0,0 +1,209 @@ +"""CLI commands for ScaffoldForge.""" + +import os +import re +import sys +from pathlib import Path +from typing import Optional + +import click + +from scaffoldforge.config import get_config +from scaffoldforge.generators import StructureGenerator, CodeGenerator +from scaffoldforge.parsers import IssueParser +from scaffoldforge.templates import TemplateEngine + + +def parse_github_url(url: str) -> tuple[str, str, int]: + """Parse a GitHub issue URL and return owner, repo, and issue number.""" + pattern = r"github\.com[/:]([^/]+)/([^/]+)/issues/(\d+)" + match = re.search(pattern, url) + if not match: + raise click.ClickException( + f"Invalid GitHub issue URL: {url}. " + "Expected format: https://github.com/owner/repo/issues/123" + ) + owner, repo, issue_number = match.groups() + repo = repo.rstrip("/") + return owner, repo, int(issue_number) + + +@click.command() +@click.argument("issue_url") +@click.option( + "--language", + "-l", + type=click.Choice(["python", "javascript", "go", "rust"]), + help="Programming language for the generated project", +) +@click.option( + "--template", + "-t", + help="Template to use for generation", +) +@click.option( + "--output", + "-o", + type=click.Path(file_okay=False, dir_okay=True, writable=True), + help="Output directory for generated project", +) +@click.option( + "--preview", + "-p", + is_flag=True, + help="Preview the generated structure without writing files", +) +@click.option( + "--interactive", + "-i", + is_flag=True, + help="Run in interactive mode with prompts for missing values", +) +@click.pass_context +def generate( + ctx: click.Context, + issue_url: str, + language: Optional[str], + template: Optional[str], + output: Optional[str], + preview: bool, + interactive: bool, +): + """Generate a project scaffold from a GitHub issue.""" + config = get_config() + verbose = ctx.obj.get("verbose", False) + + try: + owner, repo, issue_number = parse_github_url(issue_url) + except click.ClickException: + raise + + if interactive: + if not language: + languages = config.get_supported_languages() + language = click.prompt( + "Select programming language", + type=click.Choice(languages), + default="python", + ) + + if not template: + if language: + templates = TemplateEngine.list_available_templates(language) + else: + templates = [] + if templates: + template = click.prompt( + "Select template", + type=click.Choice(templates), + default=templates[0], + ) + + if not output: + output = click.prompt( + "Output directory", + default=config.get_output_dir(), + ) + + try: + token = config.get_github_token() + parser = IssueParser(token=token) + issue_data = parser.parse_issue(owner, repo, issue_number) + + if verbose: + click.echo(f"Parsed issue #{issue_number}: {issue_data.title}") + click.echo(f"Labels: {issue_data.labels}") + click.echo(f"Checklist items: {len(issue_data.checklist)}") + + if not language: + detected = parser.detect_language(issue_data) + if verbose: + click.echo(f"Detected language: {detected}") + language = detected if detected else "python" + + if not template: + template = "default" + + template_engine = TemplateEngine() + template_engine.load_templates(language, template) + + code_generator = CodeGenerator(template_engine, issue_data) + structure_generator = StructureGenerator( + output_dir=output or config.get_output_dir(), + preview=preview, + ) + + structure_generator.generate( + language=language, + issue_data=issue_data, + code_generator=code_generator, + ) + + if preview: + click.echo("\n--- Preview Complete ---") + click.echo(f"Project would be created at: {output or config.get_output_dir()}") + else: + click.echo(f"\nProject successfully generated at: {output or config.get_output_dir()}") + + except Exception as e: + raise click.ClickException(str(e)) + + +@click.command() +@click.argument("issue_url") +@click.option( + "--language", + "-l", + type=click.Choice(["python", "javascript", "go", "rust"]), + help="Programming language for the preview", +) +@click.option( + "--output", + "-o", + type=click.Path(file_okay=False, dir_okay=True, writable=True), + help="Output directory for preview", +) +@click.pass_context +def preview( + ctx: click.Context, + issue_url: str, + language: Optional[str], + output: Optional[str], +): + """Preview the project structure that would be generated.""" + ctx.obj["preview_mode"] = True + ctx.invoke( + generate, + issue_url=issue_url, + language=language, + output=output, + preview_mode=True, + interactive=False, + ) + + +@click.command() +@click.option( + "--language", + "-l", + type=click.Choice(["python", "javascript", "go", "rust"]), + help="Filter templates by programming language", +) +@click.pass_context +def list_templates(ctx: click.Context, language: Optional[str]): + """List available templates.""" + config = get_config() + engine = TemplateEngine() + + if language: + templates = engine.list_available_templates(language) + click.echo(f"Templates for {language}:") + for t in templates: + click.echo(f" - {t}") + else: + languages = config.get_supported_languages() + for lang in languages: + templates = engine.list_available_templates(lang) + click.echo(f"{lang}:") + for t in templates: + click.echo(f" - {t}")