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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowsh-cli
3
- Version: 0.2.2
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 -- <prompt>` with optional `--agent <agent>` before `--`, so dash-prefixed prompts are message content rather than OpenCode flags. Agent steps fail with a clear error if `opencode` is not on `PATH`.
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 -- <prompt>` with optional `--agent <agent>` before `--`, so dash-prefixed prompts are message content rather than OpenCode flags. Agent steps fail with a clear error if `opencode` is not on `PATH`.
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
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "flowsh-cli"
7
- version = "0.2.2"
7
+ version = "0.4.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.2.2"
6
+ __version__ = "0.4.0"
7
7
 
8
8
  __all__ = [
9
9
  "Workflow",
@@ -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 BaseModel, ConfigDict, Field, ValidationError, field_validator
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 <<'{delimiter}'",
264
+ f" prompt=$(cat {heredoc}",
252
265
  *step.prompt.splitlines(),
253
266
  delimiter,
254
267
  " )",
255
268
  ]
256
269
  )
257
- if step.agent:
258
- lines.append(f" local agent={bash_quote(step.agent)}")
259
- lines.append(' run_agent "$prompt" "$agent"')
260
- else:
261
- lines.append(' run_agent "$prompt"')
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