flowsh-cli 0.4.1__tar.gz → 0.5.0__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.4.1
3
+ Version: 0.5.0
4
4
  Summary: Generate Bash harness scripts from workflow YAML files.
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "flowsh-cli"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "Generate Bash harness scripts from workflow YAML files."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.4.1"
6
+ __version__ = "0.5.0"
7
7
 
8
8
  __all__ = [
9
9
  "Workflow",
@@ -73,7 +73,14 @@ class BashStep(BaseStep):
73
73
 
74
74
  class AgentStep(BaseStep):
75
75
  type: Literal["agent"]
76
- prompt: str
76
+ prompt: str = Field(
77
+ description=(
78
+ "The prompt text sent to the agent. "
79
+ "May contain markdown, code fences, backticks, $(...), and other shell syntax freely — "
80
+ "the prompt is never passed through a shell. "
81
+ "Use expandPrompt: true to substitute ${VAR} / $VAR tokens from vars steps."
82
+ )
83
+ )
77
84
  agent: str | None = None
78
85
  model: str | None = None
79
86
  command: str | None = None
@@ -84,7 +91,17 @@ class AgentStep(BaseStep):
84
91
  "dangerously-skip-permissions",
85
92
  ),
86
93
  )
87
- expandPrompt: bool = False
94
+ expandPrompt: bool = Field(
95
+ default=False,
96
+ description=(
97
+ "When true, substitutes ${VAR} and $VAR tokens from vars steps into the prompt "
98
+ "at runtime using safe plain-text replacement. "
99
+ "Shell expressions such as $(cmd), `cmd`, $((expr)), and globs are NOT evaluated — "
100
+ "they pass through to the agent literally. "
101
+ "Only uppercase variable names matching [A-Z_][A-Z0-9_]* found in the prompt text "
102
+ "are replaced. All other shell syntax in the prompt is safe to use."
103
+ ),
104
+ )
88
105
 
89
106
  @model_validator(mode="before")
90
107
  @classmethod
@@ -140,9 +157,23 @@ class VarsStep(BaseStep):
140
157
  Step = Annotated[VarsStep | BashStep | AgentStep, Field(discriminator="type")]
141
158
 
142
159
 
160
+ class WorkflowParam(StrictModel):
161
+ name: str
162
+ description: str | None = None
163
+ required: bool = False
164
+
165
+ @field_validator("name")
166
+ @classmethod
167
+ def validate_name(cls, value: str) -> str:
168
+ if not re.fullmatch(r"[A-Z_][A-Z0-9_]*", value):
169
+ raise ValueError("must match ^[A-Z_][A-Z0-9_]*$")
170
+ return value
171
+
172
+
143
173
  class Workflow(StrictModel):
144
174
  id: str
145
175
  name: str
176
+ params: list[WorkflowParam] = []
146
177
  steps: list[Step]
147
178
 
148
179
  @field_validator("id")
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import re
4
4
  from pathlib import Path
5
5
 
6
- from flowsh_cli.models import AgentStep, BashStep, Step, VarsStep, Workflow
6
+ from flowsh_cli.models import AgentStep, BashStep, Step, VarsStep, Workflow, WorkflowParam
7
7
 
8
8
 
9
9
  def harness_path(workflow: Workflow) -> Path:
@@ -26,13 +26,7 @@ def render_harness(workflow: Workflow) -> str:
26
26
  'LOG_BASENAME="flowsh-${WORKFLOW_SLUG}-${LOG_TIMESTAMP}-$$.log"',
27
27
  "",
28
28
  section("Argument handling"),
29
- "DRY_RUN=false",
30
- 'if [[ $# -eq 1 && "$1" == "--dry-run" ]]; then',
31
- " DRY_RUN=true",
32
- "elif [[ $# -gt 0 ]]; then",
33
- ' printf "Usage: %s [--dry-run]\\n" "$0" >&2',
34
- " exit 2",
35
- "fi",
29
+ *_render_arg_block(workflow.params),
36
30
  "",
37
31
  section("refuse_symlink_path() - keep generated logs inside plain relative paths"),
38
32
  "refuse_symlink_path() {",
@@ -353,6 +347,69 @@ def bash_quote(value: str) -> str:
353
347
  return "'" + value.replace("'", "'\\''") + "'"
354
348
 
355
349
 
350
+ def _render_arg_block(params: list[WorkflowParam]) -> list[str]:
351
+ if not params:
352
+ return [
353
+ "DRY_RUN=false",
354
+ 'if [[ $# -eq 1 && "$1" == "--dry-run" ]]; then',
355
+ " DRY_RUN=true",
356
+ "elif [[ $# -gt 0 ]]; then",
357
+ ' printf "Usage: %s [--dry-run]\\n" "$0" >&2',
358
+ " exit 2",
359
+ "fi",
360
+ ]
361
+
362
+ usage_parts: list[str] = ["[--dry-run]"]
363
+ for p in params:
364
+ usage_parts.append(f"<{p.name}>" if p.required else f"[{p.name}]")
365
+ usage_str = " ".join(usage_parts)
366
+
367
+ lines: list[str] = [
368
+ "DRY_RUN=false",
369
+ "POSITIONAL_ARGS=()",
370
+ "",
371
+ "while [[ $# -gt 0 ]]; do",
372
+ ' case "$1" in',
373
+ " --dry-run)",
374
+ " DRY_RUN=true",
375
+ " shift",
376
+ " ;;",
377
+ " --*)",
378
+ ' printf "Unknown option: %s\\n" "$1" >&2',
379
+ " exit 2",
380
+ " ;;",
381
+ " *)",
382
+ ' POSITIONAL_ARGS+=("$1")',
383
+ " shift",
384
+ " ;;",
385
+ " esac",
386
+ "done",
387
+ "",
388
+ ]
389
+
390
+ for idx, param in enumerate(params):
391
+ lines += [
392
+ f"if [[ ${{#POSITIONAL_ARGS[@]}} -gt {idx} ]]; then",
393
+ f' {param.name}="${{POSITIONAL_ARGS[{idx}]}}"',
394
+ f" export {param.name}",
395
+ "fi",
396
+ ]
397
+
398
+ lines.append("")
399
+
400
+ required = [p for p in params if p.required]
401
+ if required:
402
+ for param in required:
403
+ lines += [
404
+ f'if [[ -z "${{{param.name}:-}}" ]]; then',
405
+ f' printf "Usage: %s {usage_str}\\n" "$0" >&2',
406
+ " exit 2",
407
+ "fi",
408
+ ]
409
+
410
+ return lines
411
+
412
+
356
413
  def section(title: str) -> str:
357
414
  safe_title = truncate_one_line(title, limit=120)
358
415
  return "\n".join(
File without changes