Skip to content

Commit 72bbb54

Browse files
tbrandenburgTom Brandenburg
andauthored
feat: add --examples and --example flags (#23) (#30)
New users had no built-in way to discover working workflow examples or bootstrap a starter file. Add two flags following the existing --schema / --version eager-callback pattern. Changes: - Add src/flowsh_cli/examples.py: single source of truth for three example tiers (simple, medium, sophisticated) with helpers examples_index() and example_yaml() - Add --examples (bool) to cli.py: prints the index and exits - Add --example NAME (str) to cli.py: prints the named YAML and exits, exits 1 with a clear error on unknown names - Update EXPECTED_HELP constant and add 5 new tests covering listing, printing, parse validation, dry-run, and unknown-name error Fixes #23 Co-authored-by: Tom Brandenburg <t_bh@gmx.de>
1 parent 7085a19 commit 72bbb54

3 files changed

Lines changed: 200 additions & 0 deletions

File tree

src/flowsh_cli/cli.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import typer
1212

1313
from flowsh_cli import __version__
14+
from flowsh_cli.examples import example_yaml, examples_index
1415
from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows, workflow_schema_yaml
1516
from flowsh_cli.render import harness_path, render_harness
1617

@@ -71,11 +72,32 @@ def generate(
7172
is_eager=True,
7273
),
7374
] = False,
75+
examples: Annotated[
76+
bool,
77+
typer.Option(
78+
"--examples",
79+
callback=lambda value: print_examples_index(value),
80+
help="List available workflow examples and exit.",
81+
is_eager=True,
82+
),
83+
] = False,
84+
example: Annotated[
85+
str | None,
86+
typer.Option(
87+
"--example",
88+
metavar="NAME",
89+
callback=lambda value: print_example(value),
90+
help="Print a named example workflow YAML to stdout and exit.",
91+
is_eager=True,
92+
),
93+
] = None,
7494
) -> None:
7595
"""Generate Bash harnesses from workflow YAML."""
7696

7797
_ = version
7898
_ = schema
99+
_ = examples
100+
_ = example
79101

80102
try:
81103
workflows = parse_workflows(workflow_yaml)
@@ -117,6 +139,26 @@ def print_schema(value: bool) -> None:
117139
raise typer.Exit
118140

119141

142+
def print_examples_index(value: bool) -> None:
143+
if not value:
144+
return
145+
146+
print(examples_index())
147+
raise typer.Exit
148+
149+
150+
def print_example(value: str | None) -> None:
151+
if value is None:
152+
return
153+
154+
try:
155+
print(example_yaml(value), end="")
156+
except ValueError as error:
157+
print(f"Error: {error}", file=sys.stderr)
158+
raise typer.Exit(1) from error
159+
raise typer.Exit
160+
161+
120162
def write_harnesses(workflows: list[Workflow], *, dry_run: bool, force: bool) -> None:
121163
output_paths = [(workflow, harness_path(workflow)) for workflow in workflows]
122164

src/flowsh_cli/examples.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from __future__ import annotations
2+
3+
EXAMPLES: dict[str, tuple[str, str, str]] = {
4+
# name -> (short_description, step_types, yaml)
5+
"simple": (
6+
"Vars + sequential bash steps",
7+
"vars, bash",
8+
"""\
9+
workflows:
10+
- id: wf_simple
11+
name: Simple example — vars and bash
12+
13+
steps:
14+
- type: vars
15+
name: Capture date
16+
values:
17+
TODAY: date -u +%F # shell command; stdout becomes the variable
18+
19+
- type: bash
20+
name: Greet
21+
run: echo "Hello, today is $TODAY"
22+
23+
- type: bash
24+
name: Done
25+
run: echo "Workflow complete"
26+
""",
27+
),
28+
"medium": (
29+
"Params + vars + bash + agent with prompt expansion",
30+
"vars, bash, agent",
31+
"""\
32+
workflows:
33+
- id: wf_medium
34+
name: Medium example — params, vars, agent
35+
params:
36+
- name: TOPIC
37+
description: Subject to summarise
38+
required: true
39+
steps:
40+
- type: vars
41+
values:
42+
TODAY: date -u +%F
43+
- type: bash
44+
run: 'echo "Running on $TODAY for topic: $TOPIC"'
45+
- type: agent
46+
expandPrompt: true
47+
prompt: |
48+
Today is ${TODAY}. Write a one-paragraph summary about ${TOPIC}.
49+
""",
50+
),
51+
"sophisticated": (
52+
"Params + vars + bash + agent + for + parallel + file handoff",
53+
"vars, bash, agent, for, parallel",
54+
"""\
55+
workflows:
56+
- id: wf_sophisticated
57+
name: Sophisticated example — for, parallel, file handoff
58+
params:
59+
- name: ITEMS
60+
description: Newline-delimited list of items to process
61+
required: true
62+
steps:
63+
- type: vars
64+
values:
65+
OUTFILE: mktemp
66+
- type: parallel
67+
steps:
68+
- type: bash
69+
run: echo "worker A started"
70+
- type: bash
71+
run: echo "worker B started"
72+
- type: for
73+
in: ITEMS
74+
item: ITEM
75+
steps:
76+
- type: bash
77+
run: echo "$ITEM" >> "$OUTFILE"
78+
- type: agent
79+
expandPrompt: true
80+
prompt: |
81+
The file ${OUTFILE} contains one processed item per line.
82+
Summarise the results.
83+
""",
84+
),
85+
}
86+
87+
88+
def examples_index() -> str:
89+
lines = ["Available examples (use --example <name> to print runnable YAML):\n"]
90+
for name, (desc, step_types, _) in EXAMPLES.items():
91+
lines.append(f" {name:<14}{desc}")
92+
lines.append(f" {'':14}Step types: {step_types}\n")
93+
return "\n".join(lines)
94+
95+
96+
def example_yaml(name: str) -> str:
97+
if name not in EXAMPLES:
98+
known = ", ".join(EXAMPLES)
99+
raise ValueError(f"unknown example {name!r}. Available: {known}")
100+
return EXAMPLES[name][2]

tests/test_workflow_to_harness.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
--force Overwrite existing files. Without this, existing files cause a failure.
3535
--version Show the flowsh-cli version and exit.
3636
--schema Show the workflow YAML schema and exit.
37+
--examples List available workflow examples and exit.
38+
--example NAME Print a named example workflow YAML to stdout and exit.
3739
--help Show this message and exit.
3840
"""
3941

@@ -2615,3 +2617,59 @@ def test_render_harness_parallel_coexists_with_sequential_steps() -> None:
26152617
# Children not individually called at top level
26162618
assert "run_step step_build" not in script
26172619
assert "run_step step_test" not in script
2620+
2621+
2622+
# ---------------------------------------------------------------------------
2623+
# --examples / --example tests (issue #23)
2624+
# ---------------------------------------------------------------------------
2625+
2626+
2627+
def test_cli_lists_examples_without_workflow_argument() -> None:
2628+
result = runner.invoke(app, ["--examples"])
2629+
2630+
assert result.exit_code == 0, result.output
2631+
assert "simple" in result.output
2632+
assert "medium" in result.output
2633+
assert "sophisticated" in result.output
2634+
assert "vars, bash" in result.output
2635+
2636+
2637+
def test_cli_prints_named_example_yaml_simple() -> None:
2638+
result = runner.invoke(app, ["--example", "simple"])
2639+
2640+
assert result.exit_code == 0, result.output
2641+
assert "wf_simple" in result.output
2642+
assert result.output.startswith("workflows:")
2643+
2644+
2645+
def test_cli_named_example_is_valid_workflow(tmp_path: Path) -> None:
2646+
result = runner.invoke(app, ["--example", "simple"])
2647+
assert result.exit_code == 0
2648+
2649+
workflow_file = tmp_path / "simple.yml"
2650+
workflow_file.write_text(result.output, encoding="utf-8")
2651+
2652+
workflows = parse_workflows(workflow_file)
2653+
assert len(workflows) == 1
2654+
assert workflows[0].id == "wf_simple"
2655+
2656+
2657+
def test_cli_named_example_dry_run(tmp_path: Path) -> None:
2658+
for name in ("simple", "medium", "sophisticated"):
2659+
result = runner.invoke(app, ["--example", name])
2660+
assert result.exit_code == 0, f"--example {name} failed: {result.output}"
2661+
2662+
workflow_file = tmp_path / f"{name}.yml"
2663+
workflow_file.write_text(result.output, encoding="utf-8")
2664+
2665+
dry = runner.invoke(app, [str(workflow_file), "--dry-run"])
2666+
assert dry.exit_code == 0, f"dry-run for {name} failed: {dry.output}"
2667+
2668+
2669+
def test_cli_rejects_unknown_example_name() -> None:
2670+
result = runner.invoke(app, ["--example", "typo"])
2671+
2672+
assert result.exit_code == 1
2673+
combined = result.output + (result.stderr or "")
2674+
assert "unknown example" in combined
2675+
assert "simple" in combined

0 commit comments

Comments
 (0)