Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD

This commit is contained in:
2026-02-04 05:37:08 +00:00
parent 275535f7cd
commit 47de851298

View File

@@ -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}")