Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD
This commit is contained in:
209
scaffoldforge/cli/commands.py
Normal file
209
scaffoldforge/cli/commands.py
Normal 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}")
|
||||
Reference in New Issue
Block a user