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