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.
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/PKG-INFO +1 -1
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/pyproject.toml +1 -1
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/src/flowsh_cli/__init__.py +1 -1
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/src/flowsh_cli/models.py +33 -2
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/src/flowsh_cli/render.py +65 -8
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/README.md +0 -0
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/src/flowsh_cli/__main__.py +0 -0
- {flowsh_cli-0.4.1 → flowsh_cli-0.5.0}/src/flowsh_cli/cli.py +0 -0
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|