flowsh-cli 0.2.2__tar.gz → 0.4.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.2.2 → flowsh_cli-0.4.0}/PKG-INFO +25 -3
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/README.md +24 -2
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/pyproject.toml +1 -1
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/src/flowsh_cli/__init__.py +1 -1
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/src/flowsh_cli/cli.py +19 -1
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/src/flowsh_cli/models.py +41 -2
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/src/flowsh_cli/render.py +22 -6
- {flowsh_cli-0.2.2 → flowsh_cli-0.4.0}/src/flowsh_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowsh-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -38,13 +38,30 @@ workflows:
|
|
|
38
38
|
- type: agent
|
|
39
39
|
name: Ask OpenCode
|
|
40
40
|
agent: general
|
|
41
|
+
model: openai/gpt-5
|
|
42
|
+
command: review
|
|
41
43
|
prompt: |
|
|
42
44
|
Summarize the current repository state.
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Supported step types are only `vars`, `bash`, and `agent`.
|
|
46
48
|
|
|
47
|
-
The input path must be a regular file no larger than 1 MiB. The input file must be valid UTF-8, non-empty YAML with a mapping root, no duplicate mapping keys, and no YAML aliases. Workflow and step names are single-line labels. Executable fields reject unsafe control bytes while allowing normal newlines and tabs. `vars` keys must be uppercase shell variable names, and `agent` names may contain only letters, digits, `_`, and `-`.
|
|
49
|
+
The input path must be a regular file no larger than 1 MiB. The input file must be valid UTF-8, non-empty YAML with a mapping root, no duplicate mapping keys, and no YAML aliases. Workflow and step names are single-line labels. Executable fields reject unsafe control bytes while allowing normal newlines and tabs. `vars` keys must be uppercase shell variable names, and `agent` names may contain only letters, digits, `_`, and `-`. Agent `model` values are passed through to OpenCode, so provider/model IDs such as `openai/gpt-5` are valid. Agent `command` values map to OpenCode `--command`; the prompt remains the message/arguments after `--`.
|
|
50
|
+
|
|
51
|
+
Agent prompts are literal by default. Set `expandPrompt: true` on an `agent` step only when the prompt should be expanded by Bash at harness runtime, for example to insert values exported by earlier `vars` steps:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
- type: agent
|
|
55
|
+
name: Fix captured issue
|
|
56
|
+
agent: general
|
|
57
|
+
expandPrompt: true
|
|
58
|
+
prompt: |
|
|
59
|
+
Follow issue #$ISSUE_NUMBER.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`expandPrompt: true` is a security-sensitive opt-in: Bash also performs command substitution such as `$(...)` and backticks in the prompt body before OpenCode receives it. Keep it disabled for prompts that contain shell examples or untrusted content.
|
|
63
|
+
|
|
64
|
+
Set `dangerouslySkipPermissions: true` on an `agent` step only when the generated harness should pass OpenCode `--dangerously-skip-permissions`. The flag is false by default, accepts the YAML alias `dangerously-skip-permissions`, and auto-approves permissions that are not explicitly denied. Treat it as security-sensitive and avoid it for untrusted workflows.
|
|
48
65
|
|
|
49
66
|
Harness paths are derived from workflow ids. `wf_example` writes `.harness/example.sh`.
|
|
50
67
|
|
|
@@ -65,6 +82,9 @@ uvx flowsh-cli .made/workflows.yml --force
|
|
|
65
82
|
|
|
66
83
|
# Show version
|
|
67
84
|
uvx flowsh-cli --version
|
|
85
|
+
|
|
86
|
+
# Show the workflow YAML schema
|
|
87
|
+
uvx flowsh-cli --schema
|
|
68
88
|
```
|
|
69
89
|
|
|
70
90
|
You can also run it via `uv run flowsh-cli` if installed locally.
|
|
@@ -88,6 +108,7 @@ Options:
|
|
|
88
108
|
--dry-run Print planned output paths without writing scripts.
|
|
89
109
|
--force Overwrite existing files. Without this, existing files cause a failure.
|
|
90
110
|
--version Show the flowsh-cli version and exit.
|
|
111
|
+
--schema Show the workflow YAML schema and exit.
|
|
91
112
|
--help Show this message and exit.
|
|
92
113
|
```
|
|
93
114
|
|
|
@@ -100,6 +121,7 @@ Exit codes:
|
|
|
100
121
|
|---|---:|---|---|
|
|
101
122
|
| `--help` | `0` | Help text | Empty |
|
|
102
123
|
| `--version` | `0` | `flowsh-cli <version>` | Empty |
|
|
124
|
+
| `--schema` | `0` | Workflow schema as YAML-formatted JSON Schema | Empty |
|
|
103
125
|
| Valid generation | `0` | One `Wrote <path>` line per harness | Empty |
|
|
104
126
|
| Valid `--dry-run` | `0` | One `DRY-RUN would write <path>` line per selected workflow | Empty |
|
|
105
127
|
| Missing required CLI argument | `2` | Empty | Typer usage error |
|
|
@@ -112,7 +134,7 @@ Generated harnesses are also non-interactive. `harness.sh --dry-run` exits `0` a
|
|
|
112
134
|
|
|
113
135
|
Generated harnesses are written with owner-only executable permissions and refuse to overwrite existing paths unless `--force` is passed. Multi-workflow generation preflights overwrite conflicts before writing any harness. `--force` replaces regular harness files and harness-file symlinks, but never replaces a directory at a harness file path. The `.harness` output directory must be a real directory, not a symlink or file. Harness dry runs do not create log files or directories. Real harness logs go to `.flowsh/logs` by default with owner-private directory and file permissions. Set `FLOWSH_LOG_DIR` when running a harness to use another local relative log directory; absolute paths, `..` path segments, symlinked path components, and non-directory log paths are refused. Logging setup and write failures fail the harness instead of being silently ignored.
|
|
114
136
|
|
|
115
|
-
Generated `bash` and `vars` bodies run with `bash -euo pipefail`, so command failures stop the workflow instead of being masked by later successful commands. Captured `vars` values are exported for later `bash` steps. `agent` steps invoke only `opencode run --format json
|
|
137
|
+
Generated `bash` and `vars` bodies run with `bash -euo pipefail`, so command failures stop the workflow instead of being masked by later successful commands. Captured `vars` values are exported for later `bash` steps. `agent` prompt heredocs are quoted by default and unquoted only when `expandPrompt: true` is set. `agent` steps invoke only `opencode run --format json` with optional `--agent <agent>`, `--model <provider/model>`, `--command <command>`, and `--dangerously-skip-permissions` flags before `-- <prompt>`, so dash-prefixed prompts are message content rather than OpenCode flags. Agent steps fail with a clear error if `opencode` is not on `PATH`.
|
|
116
138
|
|
|
117
139
|
## Development
|
|
118
140
|
|
|
@@ -22,13 +22,30 @@ workflows:
|
|
|
22
22
|
- type: agent
|
|
23
23
|
name: Ask OpenCode
|
|
24
24
|
agent: general
|
|
25
|
+
model: openai/gpt-5
|
|
26
|
+
command: review
|
|
25
27
|
prompt: |
|
|
26
28
|
Summarize the current repository state.
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
Supported step types are only `vars`, `bash`, and `agent`.
|
|
30
32
|
|
|
31
|
-
The input path must be a regular file no larger than 1 MiB. The input file must be valid UTF-8, non-empty YAML with a mapping root, no duplicate mapping keys, and no YAML aliases. Workflow and step names are single-line labels. Executable fields reject unsafe control bytes while allowing normal newlines and tabs. `vars` keys must be uppercase shell variable names, and `agent` names may contain only letters, digits, `_`, and `-`.
|
|
33
|
+
The input path must be a regular file no larger than 1 MiB. The input file must be valid UTF-8, non-empty YAML with a mapping root, no duplicate mapping keys, and no YAML aliases. Workflow and step names are single-line labels. Executable fields reject unsafe control bytes while allowing normal newlines and tabs. `vars` keys must be uppercase shell variable names, and `agent` names may contain only letters, digits, `_`, and `-`. Agent `model` values are passed through to OpenCode, so provider/model IDs such as `openai/gpt-5` are valid. Agent `command` values map to OpenCode `--command`; the prompt remains the message/arguments after `--`.
|
|
34
|
+
|
|
35
|
+
Agent prompts are literal by default. Set `expandPrompt: true` on an `agent` step only when the prompt should be expanded by Bash at harness runtime, for example to insert values exported by earlier `vars` steps:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
- type: agent
|
|
39
|
+
name: Fix captured issue
|
|
40
|
+
agent: general
|
|
41
|
+
expandPrompt: true
|
|
42
|
+
prompt: |
|
|
43
|
+
Follow issue #$ISSUE_NUMBER.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`expandPrompt: true` is a security-sensitive opt-in: Bash also performs command substitution such as `$(...)` and backticks in the prompt body before OpenCode receives it. Keep it disabled for prompts that contain shell examples or untrusted content.
|
|
47
|
+
|
|
48
|
+
Set `dangerouslySkipPermissions: true` on an `agent` step only when the generated harness should pass OpenCode `--dangerously-skip-permissions`. The flag is false by default, accepts the YAML alias `dangerously-skip-permissions`, and auto-approves permissions that are not explicitly denied. Treat it as security-sensitive and avoid it for untrusted workflows.
|
|
32
49
|
|
|
33
50
|
Harness paths are derived from workflow ids. `wf_example` writes `.harness/example.sh`.
|
|
34
51
|
|
|
@@ -49,6 +66,9 @@ uvx flowsh-cli .made/workflows.yml --force
|
|
|
49
66
|
|
|
50
67
|
# Show version
|
|
51
68
|
uvx flowsh-cli --version
|
|
69
|
+
|
|
70
|
+
# Show the workflow YAML schema
|
|
71
|
+
uvx flowsh-cli --schema
|
|
52
72
|
```
|
|
53
73
|
|
|
54
74
|
You can also run it via `uv run flowsh-cli` if installed locally.
|
|
@@ -72,6 +92,7 @@ Options:
|
|
|
72
92
|
--dry-run Print planned output paths without writing scripts.
|
|
73
93
|
--force Overwrite existing files. Without this, existing files cause a failure.
|
|
74
94
|
--version Show the flowsh-cli version and exit.
|
|
95
|
+
--schema Show the workflow YAML schema and exit.
|
|
75
96
|
--help Show this message and exit.
|
|
76
97
|
```
|
|
77
98
|
|
|
@@ -84,6 +105,7 @@ Exit codes:
|
|
|
84
105
|
|---|---:|---|---|
|
|
85
106
|
| `--help` | `0` | Help text | Empty |
|
|
86
107
|
| `--version` | `0` | `flowsh-cli <version>` | Empty |
|
|
108
|
+
| `--schema` | `0` | Workflow schema as YAML-formatted JSON Schema | Empty |
|
|
87
109
|
| Valid generation | `0` | One `Wrote <path>` line per harness | Empty |
|
|
88
110
|
| Valid `--dry-run` | `0` | One `DRY-RUN would write <path>` line per selected workflow | Empty |
|
|
89
111
|
| Missing required CLI argument | `2` | Empty | Typer usage error |
|
|
@@ -96,7 +118,7 @@ Generated harnesses are also non-interactive. `harness.sh --dry-run` exits `0` a
|
|
|
96
118
|
|
|
97
119
|
Generated harnesses are written with owner-only executable permissions and refuse to overwrite existing paths unless `--force` is passed. Multi-workflow generation preflights overwrite conflicts before writing any harness. `--force` replaces regular harness files and harness-file symlinks, but never replaces a directory at a harness file path. The `.harness` output directory must be a real directory, not a symlink or file. Harness dry runs do not create log files or directories. Real harness logs go to `.flowsh/logs` by default with owner-private directory and file permissions. Set `FLOWSH_LOG_DIR` when running a harness to use another local relative log directory; absolute paths, `..` path segments, symlinked path components, and non-directory log paths are refused. Logging setup and write failures fail the harness instead of being silently ignored.
|
|
98
120
|
|
|
99
|
-
Generated `bash` and `vars` bodies run with `bash -euo pipefail`, so command failures stop the workflow instead of being masked by later successful commands. Captured `vars` values are exported for later `bash` steps. `agent` steps invoke only `opencode run --format json
|
|
121
|
+
Generated `bash` and `vars` bodies run with `bash -euo pipefail`, so command failures stop the workflow instead of being masked by later successful commands. Captured `vars` values are exported for later `bash` steps. `agent` prompt heredocs are quoted by default and unquoted only when `expandPrompt: true` is set. `agent` steps invoke only `opencode run --format json` with optional `--agent <agent>`, `--model <provider/model>`, `--command <command>`, and `--dangerously-skip-permissions` flags before `-- <prompt>`, so dash-prefixed prompts are message content rather than OpenCode flags. Agent steps fail with a clear error if `opencode` is not on `PATH`.
|
|
100
122
|
|
|
101
123
|
## Development
|
|
102
124
|
|
|
@@ -11,7 +11,7 @@ from typing import Annotated
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
from flowsh_cli import __version__
|
|
14
|
-
from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows
|
|
14
|
+
from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows, workflow_schema_yaml
|
|
15
15
|
from flowsh_cli.render import harness_path, render_harness
|
|
16
16
|
|
|
17
17
|
app = typer.Typer(
|
|
@@ -62,10 +62,20 @@ def generate(
|
|
|
62
62
|
is_eager=True,
|
|
63
63
|
),
|
|
64
64
|
] = False,
|
|
65
|
+
schema: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
typer.Option(
|
|
68
|
+
"--schema",
|
|
69
|
+
callback=lambda value: print_schema(value),
|
|
70
|
+
help="Show the workflow YAML schema and exit.",
|
|
71
|
+
is_eager=True,
|
|
72
|
+
),
|
|
73
|
+
] = False,
|
|
65
74
|
) -> None:
|
|
66
75
|
"""Generate Bash harnesses from workflow YAML."""
|
|
67
76
|
|
|
68
77
|
_ = version
|
|
78
|
+
_ = schema
|
|
69
79
|
|
|
70
80
|
try:
|
|
71
81
|
workflows = parse_workflows(workflow_yaml)
|
|
@@ -99,6 +109,14 @@ def print_version(value: bool) -> None:
|
|
|
99
109
|
raise typer.Exit
|
|
100
110
|
|
|
101
111
|
|
|
112
|
+
def print_schema(value: bool) -> None:
|
|
113
|
+
if not value:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
print(workflow_schema_yaml(), end="")
|
|
117
|
+
raise typer.Exit
|
|
118
|
+
|
|
119
|
+
|
|
102
120
|
def write_harnesses(workflows: list[Workflow], *, dry_run: bool, force: bool) -> None:
|
|
103
121
|
output_paths = [(workflow, harness_path(workflow)) for workflow in workflows]
|
|
104
122
|
|
|
@@ -7,7 +7,15 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Annotated, Literal
|
|
8
8
|
|
|
9
9
|
import yaml
|
|
10
|
-
from pydantic import
|
|
10
|
+
from pydantic import (
|
|
11
|
+
AliasChoices,
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
ValidationError,
|
|
16
|
+
field_validator,
|
|
17
|
+
model_validator,
|
|
18
|
+
)
|
|
11
19
|
|
|
12
20
|
MAX_WORKFLOW_YAML_BYTES = 1_048_576
|
|
13
21
|
|
|
@@ -67,8 +75,31 @@ class AgentStep(BaseStep):
|
|
|
67
75
|
type: Literal["agent"]
|
|
68
76
|
prompt: str
|
|
69
77
|
agent: str | None = None
|
|
78
|
+
model: str | None = None
|
|
79
|
+
command: str | None = None
|
|
80
|
+
dangerouslySkipPermissions: bool = Field(
|
|
81
|
+
default=False,
|
|
82
|
+
validation_alias=AliasChoices(
|
|
83
|
+
"dangerouslySkipPermissions",
|
|
84
|
+
"dangerously-skip-permissions",
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
expandPrompt: bool = False
|
|
88
|
+
|
|
89
|
+
@model_validator(mode="before")
|
|
90
|
+
@classmethod
|
|
91
|
+
def reject_ambiguous_dangerous_aliases(cls, data: object) -> object:
|
|
92
|
+
if (
|
|
93
|
+
isinstance(data, Mapping)
|
|
94
|
+
and "dangerouslySkipPermissions" in data
|
|
95
|
+
and "dangerously-skip-permissions" in data
|
|
96
|
+
):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"dangerouslySkipPermissions and dangerously-skip-permissions must not both be set"
|
|
99
|
+
)
|
|
100
|
+
return data
|
|
70
101
|
|
|
71
|
-
@field_validator("prompt", "agent")
|
|
102
|
+
@field_validator("prompt", "agent", "model", "command")
|
|
72
103
|
@classmethod
|
|
73
104
|
def validate_strings(cls, value: str | None) -> str | None:
|
|
74
105
|
if value is not None and value.strip() == "":
|
|
@@ -175,6 +206,14 @@ def parse_workflows(path: Path) -> list[Workflow]:
|
|
|
175
206
|
raise WorkflowParseError(format_validation_error(error)) from error
|
|
176
207
|
|
|
177
208
|
|
|
209
|
+
def workflow_schema_yaml() -> str:
|
|
210
|
+
return yaml.safe_dump(
|
|
211
|
+
WorkflowFile.model_json_schema(),
|
|
212
|
+
sort_keys=False,
|
|
213
|
+
allow_unicode=False,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
178
217
|
def format_validation_error(error: ValidationError) -> str:
|
|
179
218
|
messages: list[str] = []
|
|
180
219
|
for item in error.errors(include_url=False, include_input=False, include_context=False):
|
|
@@ -174,11 +174,23 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
174
174
|
"run_agent() {",
|
|
175
175
|
' local prompt="$1"',
|
|
176
176
|
' local agent="${2:-}"',
|
|
177
|
+
' local model="${3:-}"',
|
|
178
|
+
' local command="${4:-}"',
|
|
179
|
+
' local dangerously_skip_permissions="${5:-false}"',
|
|
177
180
|
"",
|
|
178
181
|
" local cmd=(opencode run --format json)",
|
|
179
182
|
' if [[ -n "$agent" ]]; then',
|
|
180
183
|
' cmd+=(--agent "$agent")',
|
|
181
184
|
" fi",
|
|
185
|
+
' if [[ -n "$model" ]]; then',
|
|
186
|
+
' cmd+=(--model "$model")',
|
|
187
|
+
" fi",
|
|
188
|
+
' if [[ -n "$command" ]]; then',
|
|
189
|
+
' cmd+=(--command "$command")',
|
|
190
|
+
" fi",
|
|
191
|
+
' if [[ "$dangerously_skip_permissions" == true ]]; then',
|
|
192
|
+
" cmd+=(--dangerously-skip-permissions)",
|
|
193
|
+
" fi",
|
|
182
194
|
"",
|
|
183
195
|
' if [[ "$DRY_RUN" == true ]]; then',
|
|
184
196
|
' log INFO "[DRY-RUN] would run: $(printf \'%q \' "${cmd[@]}") (with prompt)"',
|
|
@@ -245,20 +257,24 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
|
|
|
245
257
|
)
|
|
246
258
|
elif isinstance(step, AgentStep):
|
|
247
259
|
delimiter = heredoc_delimiter("PROMPT", step.prompt)
|
|
260
|
+
heredoc = f"<<{delimiter}" if step.expandPrompt else f"<<'{delimiter}'"
|
|
248
261
|
lines.extend(
|
|
249
262
|
[
|
|
250
263
|
" local prompt",
|
|
251
|
-
f" prompt=$(cat
|
|
264
|
+
f" prompt=$(cat {heredoc}",
|
|
252
265
|
*step.prompt.splitlines(),
|
|
253
266
|
delimiter,
|
|
254
267
|
" )",
|
|
255
268
|
]
|
|
256
269
|
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
else
|
|
261
|
-
|
|
270
|
+
lines.append(f" local agent={bash_quote(step.agent or '')}")
|
|
271
|
+
lines.append(f" local model={bash_quote(step.model or '')}")
|
|
272
|
+
lines.append(f" local command={bash_quote(step.command or '')}")
|
|
273
|
+
dangerous_skip_permissions = "true" if step.dangerouslySkipPermissions else "false"
|
|
274
|
+
lines.append(f" local dangerously_skip_permissions={dangerous_skip_permissions}")
|
|
275
|
+
lines.append(
|
|
276
|
+
' run_agent "$prompt" "$agent" "$model" "$command" "$dangerously_skip_permissions"'
|
|
277
|
+
)
|
|
262
278
|
else:
|
|
263
279
|
raise AssertionError(f"Unsupported step type: {step}")
|
|
264
280
|
|
|
File without changes
|