flowsh-cli 0.8.0__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowsh-cli
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Generate Bash harness scripts from workflow YAML files.
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -88,10 +88,10 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
88
88
 
89
89
  | Type | Purpose | Notes |
90
90
  |---|---|---|
91
- | `vars` | Capture command output into exported shell variables | Variable names must be uppercase shell identifiers. |
91
+ | `vars` | Execute shell commands and export their stdout into shell variables | Variable names must be uppercase shell identifiers. |
92
92
  | `bash` | Run shell commands | Runs with `bash -euo pipefail`. |
93
93
  | `agent` | Call OpenCode | Supports `agent`, `model`, `command`, `expandPrompt`, and `dangerouslySkipPermissions`. |
94
- | `for` | Iterate over newline-delimited input | Flat iteration only; nested `for` steps are not supported. |
94
+ | `for` | Iterate over newline-delimited values from a previous `vars` step | Flat iteration only; nested `for` steps are not supported. |
95
95
  | `parallel` | Run child steps concurrently | Children run as separate branches and the parent waits for all of them. |
96
96
 
97
97
  ## Agent Behavior
@@ -72,10 +72,10 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
72
72
 
73
73
  | Type | Purpose | Notes |
74
74
  |---|---|---|
75
- | `vars` | Capture command output into exported shell variables | Variable names must be uppercase shell identifiers. |
75
+ | `vars` | Execute shell commands and export their stdout into shell variables | Variable names must be uppercase shell identifiers. |
76
76
  | `bash` | Run shell commands | Runs with `bash -euo pipefail`. |
77
77
  | `agent` | Call OpenCode | Supports `agent`, `model`, `command`, `expandPrompt`, and `dangerouslySkipPermissions`. |
78
- | `for` | Iterate over newline-delimited input | Flat iteration only; nested `for` steps are not supported. |
78
+ | `for` | Iterate over newline-delimited values from a previous `vars` step | Flat iteration only; nested `for` steps are not supported. |
79
79
  | `parallel` | Run child steps concurrently | Children run as separate branches and the parent waits for all of them. |
80
80
 
81
81
  ## Agent Behavior
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "flowsh-cli"
7
- version = "0.8.0"
7
+ version = "0.8.2"
8
8
  description = "Generate Bash harness scripts from workflow YAML files."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,3 +42,6 @@ target-version = "py311"
42
42
 
43
43
  [tool.ruff.lint]
44
44
  select = ["E", "F", "I", "UP", "B", "SIM"]
45
+
46
+ [tool.ruff.lint.per-file-ignores]
47
+ "tests/test_workflow_to_harness.py" = ["E501"]
@@ -3,7 +3,7 @@
3
3
  from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows
4
4
  from flowsh_cli.render import harness_path, render_harness
5
5
 
6
- __version__ = "0.8.0"
6
+ __version__ = "0.8.2"
7
7
 
8
8
  __all__ = [
9
9
  "Workflow",
@@ -20,7 +20,6 @@ app = typer.Typer(
20
20
  context_settings={"terminal_width": 120, "max_content_width": 120},
21
21
  help="Generate reproducible OpenCode Bash harness scripts from MADE workflow YAML.",
22
22
  pretty_exceptions_enable=False,
23
- rich_markup_mode=None,
24
23
  )
25
24
 
26
25
 
@@ -46,6 +46,7 @@ class StrictModel(BaseModel):
46
46
 
47
47
  class BaseStep(StrictModel):
48
48
  name: str | None = None
49
+ when: str | None = None
49
50
 
50
51
  @field_validator("name")
51
52
  @classmethod
@@ -56,6 +57,15 @@ class BaseStep(StrictModel):
56
57
  raise ValueError("must not contain control characters")
57
58
  return value
58
59
 
60
+ @field_validator("when")
61
+ @classmethod
62
+ def validate_when(cls, value: str | None) -> str | None:
63
+ if value is not None and value.strip() == "":
64
+ raise ValueError("must not be empty")
65
+ if value is not None and has_unsafe_control_characters(value):
66
+ raise ValueError("must not contain unsafe control characters")
67
+ return value
68
+
59
69
 
60
70
  class BashStep(BaseStep):
61
71
  type: Literal["bash"]
@@ -135,7 +145,14 @@ class AgentStep(BaseStep):
135
145
 
136
146
  class VarsStep(BaseStep):
137
147
  type: Literal["vars"]
138
- values: dict[str, str]
148
+ values: dict[str, str] = Field(
149
+ description=(
150
+ "Each value is a shell command. "
151
+ "The command is executed and its stdout (stripped of trailing newline) "
152
+ "becomes the variable value. "
153
+ "It is NOT a literal string."
154
+ )
155
+ )
139
156
 
140
157
  @field_validator("values")
141
158
  @classmethod
@@ -156,8 +173,19 @@ class VarsStep(BaseStep):
156
173
 
157
174
  class ForStep(BaseStep):
158
175
  type: Literal["for"]
159
- in_: str = Field(..., validation_alias="in")
160
- item: str = Field(...)
176
+ in_: str = Field(
177
+ ...,
178
+ validation_alias="in",
179
+ description=(
180
+ "Name of a variable (defined by a preceding vars step) whose value is "
181
+ "a newline-delimited list. "
182
+ "Each line becomes one iteration."
183
+ ),
184
+ )
185
+ item: str = Field(
186
+ ...,
187
+ description="Name of the shell variable exported into each iteration body.",
188
+ )
161
189
  steps: list[Step] = Field(min_length=1)
162
190
 
163
191
  @field_validator("in_", "item")
@@ -241,7 +241,7 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
241
241
  inner_title = inner_step.name or default_step_title(i, inner_step)
242
242
  prefix_lines.append(section(f"For-inner step ({inner_step.type}): {inner_title}"))
243
243
  prefix_lines.append(f"{inner_fn}() {{")
244
- prefix_lines.extend(_render_step_body(inner_step))
244
+ prefix_lines.extend(_render_step_body(inner_step, inner_title))
245
245
  prefix_lines.append("}")
246
246
  prefix_lines.append("")
247
247
 
@@ -259,7 +259,7 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
259
259
  child_fns.append(child_fn)
260
260
  prefix_lines.append(section(f"Parallel child {i} ({child.type}): {child_title}"))
261
261
  prefix_lines.append(f"{child_fn}() {{")
262
- prefix_lines.extend(_render_step_body(child))
262
+ prefix_lines.extend(_render_step_body(child, child_title))
263
263
  prefix_lines.append("}")
264
264
  prefix_lines.append("")
265
265
 
@@ -271,23 +271,36 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
271
271
  body_lines.append(f' wait "$pid_{child_fn}" || status=$?')
272
272
  body_lines.append(' return "$status"')
273
273
  else:
274
- body_lines = _render_step_body(step)
274
+ body_lines = _render_step_body(step, title)
275
275
 
276
276
  runner = "run_stateful_step" if isinstance(step, (VarsStep, ForStep)) else "run_step"
277
277
  outer_lines = [
278
278
  section(f"Step {index} ({step.type}): {title}"),
279
279
  f"{function_name}() {{",
280
- *body_lines,
281
- "}",
282
- f"{runner} {function_name}",
283
- "",
284
280
  ]
281
+
282
+ outer_lines.extend(body_lines)
283
+ outer_lines.extend(
284
+ [
285
+ "}",
286
+ f"{runner} {function_name}",
287
+ "",
288
+ ]
289
+ )
285
290
  return prefix_lines + outer_lines
286
291
 
287
292
 
288
- def _render_step_body(step: Step) -> list[str]:
293
+ def _render_step_body(step: Step, title: str) -> list[str]:
289
294
  """Return indented body lines for a step function (no wrapper, no run_step call)."""
290
295
  lines: list[str] = []
296
+
297
+ if step.when is not None:
298
+ lines.append(f" if ! ({step.when}); then")
299
+ lines.append(f" log INFO {bash_quote(f'Step skipped (when): {title}')}")
300
+ lines.append(" return 0")
301
+ lines.append(" fi")
302
+ lines.append("")
303
+
291
304
  if isinstance(step, VarsStep):
292
305
  lines.append(" local status=0")
293
306
  for name, command in step.values.items():