Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added 60
Empty file.
Empty file added list[dict[str
Empty file.
168 changes: 167 additions & 1 deletion src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ def _print_cli_warning(
from .commands import init as _init_cmd # noqa: E402
_init_cmd.register(app)

# ===== workflow commands =====
from .commands import workflow as _workflow_cmd # noqa: E402
_workflow_cmd.register(app)
Comment on lines +486 to +488
Comment on lines +486 to +488
Comment on lines +486 to +488
Comment on lines +486 to +488


@app.command()
def check():
Expand Down Expand Up @@ -607,6 +611,152 @@ def version(
app.add_typer(_self_app, name="self")


# ===== Spec / Plan Commands (direct CLI access with dry-run) =====

specify_app = typer.Typer(
name="specify",
help="Create a feature specification (direct CLI alternative to /speckit.specify in coding agents)",
add_completion=False,
)
app.add_typer(specify_app, name="specify")
Comment thread
fuleinist marked this conversation as resolved.

Comment on lines +616 to +622
Comment on lines +616 to +622
Comment on lines +616 to +622
Comment on lines +616 to +622

@specify_app.command("spec")
Comment thread
fuleinist marked this conversation as resolved.
def specify_specify(
Comment thread
fuleinist marked this conversation as resolved.
spec: str = typer.Option(
..., "--spec", "-s", help="Feature description (what to build and why)"
),
):
"""Create a feature specification from a description.

This is a direct CLI alternative to the /speckit.specify agent command.
Runs the spec workflow and generates spec.md in the feature directory.

Comment on lines +630 to +634
Examples:
specify spec --spec "Build a kanban board with drag-and-drop"
specify spec --spec "Photo album app"
"""
from .workflows.engine import WorkflowEngine

project_root = _require_specify_project()
engine = WorkflowEngine(project_root)
engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026")

try:
definition = engine.load_workflow("speckit")
except FileNotFoundError:
console.print("[red]Error:[/red] speckit workflow not installed. Run 'specify init' first.")
raise typer.Exit(1)
except ValueError as exc:
console.print(f"[red]Error:[/red] Invalid workflow: {exc}")
raise typer.Exit(1)

errors = engine.validate(definition)
if errors:
console.print("[red]Workflow validation failed:[/red]")
for err in errors:
console.print(f" \u2022 {err}")
raise typer.Exit(1)

inputs = {"spec": spec, "integration": "auto", "scope": "full"}

console.print(f"\n[bold cyan]Running:[/bold cyan] specify spec")
console.print(f"[dim]Spec: {spec[:60]}{'...' if len(spec) > 60 else ''}[/dim]\n")


try:
state = engine.execute(definition, inputs, start_at="specify", stop_after="specify")
except ValueError as exc:
Comment thread
fuleinist marked this conversation as resolved.
console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(1)
except Exception as exc:
console.print(f"[red]Workflow failed:[/red] {exc}")
raise typer.Exit(1)

status_color = {"completed": "green", "paused": "yellow", "failed": "red", "aborted": "red"}.get(
state.status.value, "white"
)
Comment thread
fuleinist marked this conversation as resolved.
console.print(f"[{status_color}]Status: {state.status.value}[/{status_color}]")

# Print dry-run step outputs so the user sees the rendered prompt/inputs
if state.status.value == "completed":
for step_id, step_data in state.step_results.items():
output = step_data.get("output", {})
if output.get("dry_run"):
msg = output.get("message", "")
if msg:
console.print(f"\n[bold cyan]Step:[/bold cyan] {step_id}")
console.print(msg)


@specify_app.command("plan")
def specify_plan(
spec: str = typer.Option(
..., "--spec", "-s", help="Feature description (what to build and why)"
),
):
"""Create an implementation plan from a feature description.

This is a direct CLI alternative to the /speckit.plan agent command.
Runs the plan step of the speckit workflow.

Examples:
specify plan --spec "Build a kanban board with drag-and-drop"
specify plan --spec "Photo album app"
"""
from .workflows.engine import WorkflowEngine

project_root = _require_specify_project()
engine = WorkflowEngine(project_root)
engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026")

try:
definition = engine.load_workflow("speckit")
except FileNotFoundError:
console.print("[red]Error:[/red] speckit workflow not installed. Run 'specify init' first.")
raise typer.Exit(1)
except ValueError as exc:
console.print(f"[red]Error:[/red] Invalid workflow: {exc}")
raise typer.Exit(1)

errors = engine.validate(definition)
if errors:
console.print("[red]Workflow validation failed:[/red]")
for err in errors:
console.print(f" \u2022 {err}")
raise typer.Exit(1)

inputs = {"spec": spec, "integration": "auto", "scope": "full"}

console.print(f"\n[bold cyan]Running:[/bold cyan] specify plan")
console.print(f"[dim]Spec: {spec[:60]}{'...' if len(spec) > 60 else ''}[/dim]\n")


try:
state = engine.execute(definition, inputs, start_at="plan", stop_after="plan")
except ValueError as exc:
console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(1)
except Exception as exc:
console.print(f"[red]Workflow failed:[/red] {exc}")
raise typer.Exit(1)

status_color = {"completed": "green", "paused": "yellow", "failed": "red", "aborted": "red"}.get(
state.status.value, "white"
)
Comment thread
fuleinist marked this conversation as resolved.
console.print(f"[{status_color}]Status: {state.status.value}[/{status_color}]")

# Print dry-run step outputs so the user sees the rendered prompt/inputs
if state.status.value == "completed":
for step_id, step_data in state.step_results.items():
output = step_data.get("output", {})
if output.get("dry_run"):
msg = output.get("message", "")
if msg:
console.print(f"\n[bold cyan]Step:[/bold cyan] {step_id}")
console.print(msg)


# ===== Extension Commands =====

extension_app = typer.Typer(
Expand Down Expand Up @@ -4163,6 +4313,9 @@ def workflow_run(
input_values: list[str] | None = typer.Option(
None, "--input", "-i", help="Input values as key=value pairs"
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show the rendered prompt/inputs for each step without invoking the AI"
),
):
"""Run a workflow from an installed ID or local YAML path."""
from .workflows.engine import WorkflowEngine
Expand Down Expand Up @@ -4201,8 +4354,11 @@ def workflow_run(
console.print(f"\n[bold cyan]Running workflow:[/bold cyan] {definition.name} ({definition.id})")
console.print(f"[dim]Version: {definition.version}[/dim]\n")

if dry_run:
console.print("[bold yellow]DRY RUN — no AI invocation will occur[/bold yellow]\n")

try:
state = engine.execute(definition, inputs)
state = engine.execute(definition, inputs, dry_run=dry_run)
Comment thread
fuleinist marked this conversation as resolved.
Comment thread
fuleinist marked this conversation as resolved.
except ValueError as exc:
console.print(f"[red]Error:[/red] {exc}")
raise typer.Exit(1)
Expand All @@ -4223,6 +4379,16 @@ def workflow_run(
if state.status.value == "paused":
console.print(f"\nResume with: [cyan]specify workflow resume {state.run_id}[/cyan]")

# Print dry-run step outputs so the user sees rendered command details
if dry_run and state.status.value == "completed":
for step_id, step_data in state.step_results.items():
output = step_data.get("output", {})
if output.get("dry_run"):
msg = output.get("message", "")
if msg:
console.print(f"\n[bold cyan]Step:[/bold cyan] {step_id}")
console.print(msg)


@workflow_app.command("resume")
def workflow_resume(
Expand Down
Loading