keystone-cli 1.0.3 → 1.1.0

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.
Files changed (153) hide show
  1. package/README.md +276 -32
  2. package/package.json +8 -4
  3. package/src/cli.ts +350 -416
  4. package/src/commands/doc.ts +31 -0
  5. package/src/commands/event.ts +29 -0
  6. package/src/commands/graph.ts +37 -0
  7. package/src/commands/index.ts +14 -0
  8. package/src/commands/init.ts +185 -0
  9. package/src/commands/run.ts +124 -0
  10. package/src/commands/schema.ts +40 -0
  11. package/src/commands/utils.ts +78 -0
  12. package/src/commands/validate.ts +111 -0
  13. package/src/db/workflow-db.test.ts +314 -0
  14. package/src/db/workflow-db.ts +810 -210
  15. package/src/expression/evaluator-audit.test.ts +4 -2
  16. package/src/expression/evaluator.test.ts +14 -1
  17. package/src/expression/evaluator.ts +166 -19
  18. package/src/parser/config-schema.ts +18 -0
  19. package/src/parser/schema.ts +153 -22
  20. package/src/parser/test-schema.ts +6 -6
  21. package/src/parser/workflow-parser.test.ts +24 -0
  22. package/src/parser/workflow-parser.ts +65 -3
  23. package/src/runner/auto-heal.test.ts +5 -6
  24. package/src/runner/blueprint-executor.test.ts +2 -2
  25. package/src/runner/debug-repl.test.ts +5 -8
  26. package/src/runner/debug-repl.ts +59 -16
  27. package/src/runner/durable-timers.test.ts +11 -2
  28. package/src/runner/engine-executor.test.ts +1 -1
  29. package/src/runner/events.ts +57 -0
  30. package/src/runner/executors/artifact-executor.ts +166 -0
  31. package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
  32. package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
  33. package/src/runner/executors/file-executor.test.ts +48 -0
  34. package/src/runner/executors/file-executor.ts +324 -0
  35. package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
  36. package/src/runner/executors/human-executor.ts +144 -0
  37. package/src/runner/executors/join-executor.ts +75 -0
  38. package/src/runner/executors/llm-executor.ts +1266 -0
  39. package/src/runner/executors/memory-executor.ts +71 -0
  40. package/src/runner/executors/plan-executor.ts +104 -0
  41. package/src/runner/executors/request-executor.ts +265 -0
  42. package/src/runner/executors/script-executor.ts +43 -0
  43. package/src/runner/executors/shell-executor.ts +403 -0
  44. package/src/runner/executors/subworkflow-executor.ts +114 -0
  45. package/src/runner/executors/types.ts +69 -0
  46. package/src/runner/executors/wait-executor.ts +59 -0
  47. package/src/runner/join-scheduling.test.ts +197 -0
  48. package/src/runner/llm-adapter-runtime.test.ts +209 -0
  49. package/src/runner/llm-adapter.test.ts +419 -24
  50. package/src/runner/llm-adapter.ts +130 -26
  51. package/src/runner/llm-clarification.test.ts +2 -1
  52. package/src/runner/llm-executor.test.ts +532 -17
  53. package/src/runner/mcp-client-audit.test.ts +1 -2
  54. package/src/runner/mcp-client.ts +136 -46
  55. package/src/runner/mcp-manager.test.ts +4 -0
  56. package/src/runner/mcp-server.test.ts +58 -0
  57. package/src/runner/mcp-server.ts +26 -0
  58. package/src/runner/memoization.test.ts +190 -0
  59. package/src/runner/optimization-runner.ts +4 -9
  60. package/src/runner/quality-gate.test.ts +69 -0
  61. package/src/runner/reflexion.test.ts +6 -17
  62. package/src/runner/resource-pool.ts +102 -14
  63. package/src/runner/services/context-builder.ts +144 -0
  64. package/src/runner/services/secret-manager.ts +105 -0
  65. package/src/runner/services/workflow-validator.ts +131 -0
  66. package/src/runner/shell-executor.test.ts +28 -4
  67. package/src/runner/standard-tools-ast.test.ts +196 -0
  68. package/src/runner/standard-tools-execution.test.ts +27 -0
  69. package/src/runner/standard-tools-integration.test.ts +6 -10
  70. package/src/runner/standard-tools.ts +339 -102
  71. package/src/runner/step-executor.test.ts +216 -4
  72. package/src/runner/step-executor.ts +69 -941
  73. package/src/runner/stream-utils.ts +7 -3
  74. package/src/runner/test-harness.ts +20 -1
  75. package/src/runner/timeout.test.ts +10 -0
  76. package/src/runner/timeout.ts +11 -2
  77. package/src/runner/tool-integration.test.ts +1 -1
  78. package/src/runner/wait-step.test.ts +102 -0
  79. package/src/runner/workflow-runner.test.ts +208 -15
  80. package/src/runner/workflow-runner.ts +890 -818
  81. package/src/runner/workflow-scheduler.ts +75 -0
  82. package/src/runner/workflow-state.ts +269 -0
  83. package/src/runner/workflow-subflows.test.ts +13 -12
  84. package/src/scripts/generate-schemas.ts +16 -0
  85. package/src/templates/agents/explore.md +1 -0
  86. package/src/templates/agents/general.md +1 -0
  87. package/src/templates/agents/handoff-router.md +14 -0
  88. package/src/templates/agents/handoff-specialist.md +15 -0
  89. package/src/templates/agents/keystone-architect.md +13 -44
  90. package/src/templates/agents/my-agent.md +1 -0
  91. package/src/templates/agents/software-engineer.md +1 -0
  92. package/src/templates/agents/summarizer.md +1 -0
  93. package/src/templates/agents/test-agent.md +1 -0
  94. package/src/templates/agents/tester.md +1 -0
  95. package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
  96. package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +2 -1
  97. package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
  98. package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
  99. package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
  100. package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
  101. package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
  102. package/src/templates/control-flow/idempotency-example.yaml +30 -0
  103. package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
  104. package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
  105. package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
  106. package/src/templates/features/artifact-example.yaml +39 -0
  107. package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
  108. package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
  109. package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
  110. package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
  111. package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
  112. package/src/templates/features/script-example.yaml +27 -0
  113. package/src/templates/patterns/agent-handoff.yaml +53 -0
  114. package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
  115. package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
  116. package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +1 -0
  117. package/src/templates/{composition-parent.yaml → patterns/composition-parent.yaml} +1 -0
  118. package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
  119. package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
  120. package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
  121. package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
  122. package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
  123. package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
  124. package/src/templates/scaffolding/review-loop.yaml +97 -0
  125. package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
  126. package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
  127. package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
  128. package/src/templates/testing/invalid.yaml +6 -0
  129. package/src/ui/dashboard.tsx +191 -33
  130. package/src/utils/auth-manager.test.ts +337 -0
  131. package/src/utils/auth-manager.ts +157 -61
  132. package/src/utils/blueprint-utils.ts +4 -6
  133. package/src/utils/config-loader.test.ts +2 -0
  134. package/src/utils/config-loader.ts +12 -3
  135. package/src/utils/constants.ts +76 -0
  136. package/src/utils/container.ts +63 -0
  137. package/src/utils/context-injector.test.ts +200 -0
  138. package/src/utils/context-injector.ts +244 -0
  139. package/src/utils/doc-generator.ts +85 -0
  140. package/src/utils/env-filter.ts +45 -0
  141. package/src/utils/json-parser.test.ts +12 -0
  142. package/src/utils/json-parser.ts +30 -5
  143. package/src/utils/logger.ts +12 -1
  144. package/src/utils/mermaid.ts +4 -0
  145. package/src/utils/paths.ts +52 -1
  146. package/src/utils/process-sandbox-worker.test.ts +46 -0
  147. package/src/utils/process-sandbox.ts +227 -14
  148. package/src/utils/redactor.test.ts +11 -6
  149. package/src/utils/redactor.ts +25 -9
  150. package/src/utils/sandbox.ts +3 -0
  151. package/src/runner/llm-executor.ts +0 -638
  152. package/src/runner/shell-executor.ts +0 -366
  153. package/src/templates/invalid.yaml +0 -5
@@ -1,366 +0,0 @@
1
- /**
2
- * Shell command executor
3
- *
4
- * ⚠️ SECURITY WARNING:
5
- * This executor runs shell commands using `sh -c`, which means:
6
- * - User inputs interpolated into commands can lead to command injection
7
- * - Malicious inputs like `foo; rm -rf /` will execute multiple commands
8
- *
9
- * IMPORTANT: Only run workflows from trusted sources.
10
- * Commands are executed with the same privileges as the Keystone process.
11
- * Expression evaluation happens before shell execution, so expressions
12
- * like ${{ inputs.filename }} are evaluated first, then passed to the shell.
13
- *
14
- * ✅ RECOMMENDED PRACTICE:
15
- * Use the escape() function to safely interpolate user inputs:
16
- *
17
- * steps:
18
- * - id: safe_echo
19
- * type: shell
20
- * run: echo ${{ escape(inputs.user_message) }}
21
- *
22
- * The escape() function wraps arguments in single quotes and escapes any
23
- * single quotes within, preventing command injection attacks.
24
- *
25
- * See SECURITY.md for more details.
26
- */
27
-
28
- import type { ExpressionContext } from '../expression/evaluator.ts';
29
- import { ExpressionEvaluator } from '../expression/evaluator.ts';
30
- import type { ShellStep } from '../parser/schema.ts';
31
- import { LIMITS } from '../utils/constants.ts';
32
- import { ConsoleLogger, type Logger } from '../utils/logger.ts';
33
-
34
- /**
35
- * Escape a shell argument for safe use in shell commands
36
- * Wraps the argument in single quotes and escapes any single quotes within
37
- *
38
- * Example usage in workflows:
39
- * ```yaml
40
- * steps:
41
- * - id: safe_echo
42
- * type: shell
43
- * # Use this pattern to safely interpolate user inputs:
44
- * run: echo ${{ escape(inputs.message) }} # Safe: explicitly escaped
45
- * # Avoid patterns like: sh -c "echo $USER_INPUT" where USER_INPUT is raw
46
- * ```
47
- */
48
- export function escapeShellArg(arg: unknown): string {
49
- const value = arg === null || arg === undefined ? '' : String(arg);
50
- // Replace single quotes with '\'' (end quote, escaped quote, start quote)
51
- return `'${value.replace(/'/g, "'\\''")}'`;
52
- }
53
-
54
- export interface ShellResult {
55
- stdout: string;
56
- stderr: string;
57
- exitCode: number;
58
- stdoutTruncated?: boolean;
59
- stderrTruncated?: boolean;
60
- }
61
-
62
- /**
63
- * Check if a command contains potentially dangerous shell metacharacters
64
- * Returns true if the command looks like it might contain unescaped user input
65
- */
66
- // Pre-compiled dangerous patterns for performance
67
- // These patterns are designed to detect likely injection attempts while minimizing false positives
68
- const DANGEROUS_PATTERNS: RegExp[] = [
69
- /;\s*(?:rm|chmod|chown|mkfs|dd)\b/, // Command chaining with destructive commands
70
- /\|\s*(?:sh|bash|zsh|ksh|dash|csh|python|python[23]?|node|ruby|perl|php|lua)\b/, // Piping to shell/interpreter (download-and-execute pattern)
71
- /\|\s*(?:sudo|su)\b/, // Piping to privilege escalation
72
- /&&\s*(?:rm|chmod|chown|mkfs|dd)\b/, // AND chaining with destructive commands
73
- /\|\|\s*(?:rm|chmod|chown|mkfs|dd)\b/, // OR chaining with destructive commands
74
- /`[^`]+`/, // Command substitution with backticks
75
- /\$\([^)]+\)/, // Command substitution with $()
76
- />\s*\/dev\/null\s*2>&1\s*&/, // Backgrounding with hidden output (often malicious)
77
- /rm\s+(-rf?|--recursive)\s+[\/~]/, // Dangerous recursive deletion
78
- />\s*\/etc\//, // Writing to /etc
79
- /curl\s+.*\|\s*(?:sh|bash)/, // Download and execute pattern
80
- /wget\s+.*\|\s*(?:sh|bash)/, // Download and execute pattern
81
- // Additional patterns for more comprehensive detection
82
- /base64\s+(-d|--decode)\s*\|/, // Base64 decode piped to another command
83
- /\beval\s+["'\$]/, // eval with variable/string (likely injection)
84
- /\bexec\s+\d+[<>]/, // exec with file descriptor redirection
85
- /python[23]?\s+-c\s*["']/, // Python one-liner with quoted code
86
- /node\s+(-e|--eval)\s*["']/, // Node.js one-liner with quoted code
87
- /perl\s+-e\s*["']/, // Perl one-liner with quoted code
88
- /ruby\s+-e\s*["']/, // Ruby one-liner with quoted code
89
- /\bdd\s+.*\bof=\//, // dd write operation to root paths
90
- /chmod\s+[0-7]{3,4}\s+\/(?!tmp)/, // chmod on root paths (except /tmp)
91
- /mkfs\./, // Filesystem formatting commands
92
- // Targeted parameter expansion patterns (not all ${} usage)
93
- /\$\{IFS[}:]/, // IFS manipulation (common injection technique)
94
- /\$\{[^}]*\$\([^}]*\}/, // Command substitution inside parameter expansion
95
- /\$\{[^}]*:-[^}]*\$\(/, // Default value with command substitution
96
- /\$\{[^}]*[`][^}]*\}/, // Backtick inside parameter expansion
97
- /\\x[0-9a-fA-F]{2}/, // Hex escaping attempts
98
- /\\[0-7]{3}/, // Octal escaping attempts
99
- /<<<\s*/, // Here-strings (can be used for injection)
100
- /\d*<&\s*\d*/, // File descriptor duplication
101
- /\d*>&-\s*/, // Closing file descriptors
102
- ];
103
-
104
- // Combine all patterns into single regex for O(m) matching instead of O(n×m)
105
- const COMBINED_DANGEROUS_PATTERN = new RegExp(DANGEROUS_PATTERNS.map((r) => r.source).join('|'));
106
-
107
- const TRUNCATED_SUFFIX = '... [truncated output]';
108
-
109
- async function readStreamWithLimit(
110
- stream: ReadableStream<Uint8Array> | null | undefined,
111
- maxBytes: number
112
- ): Promise<{ text: string; truncated: boolean }> {
113
- if (!stream) return { text: '', truncated: false };
114
- if (maxBytes <= 0) {
115
- try {
116
- await stream.cancel?.();
117
- } catch {}
118
- return { text: TRUNCATED_SUFFIX, truncated: true };
119
- }
120
-
121
- const reader = stream.getReader();
122
- const decoder = new TextDecoder();
123
- let text = '';
124
- let bytesRead = 0;
125
-
126
- while (true) {
127
- const { value, done } = await reader.read();
128
- if (done) break;
129
- if (!value) continue;
130
-
131
- if (bytesRead + value.byteLength > maxBytes) {
132
- const allowed = maxBytes - bytesRead;
133
- if (allowed > 0) {
134
- text += decoder.decode(value.slice(0, allowed), { stream: true });
135
- }
136
- text += decoder.decode();
137
- try {
138
- await reader.cancel();
139
- } catch {}
140
- return { text: `${text}${TRUNCATED_SUFFIX}`, truncated: true };
141
- }
142
-
143
- bytesRead += value.byteLength;
144
- text += decoder.decode(value, { stream: true });
145
- }
146
-
147
- text += decoder.decode();
148
- return { text, truncated: false };
149
- }
150
-
151
- export function detectShellInjectionRisk(command: string): boolean {
152
- // Use combined pattern for single-pass matching
153
- return COMBINED_DANGEROUS_PATTERN.test(command);
154
- }
155
-
156
- /**
157
- * Execute a shell command using Bun.spawn
158
- */
159
- export async function executeShell(
160
- step: ShellStep,
161
- context: ExpressionContext,
162
- logger: Logger = new ConsoleLogger(),
163
- abortSignal?: AbortSignal
164
- ): Promise<ShellResult> {
165
- if (abortSignal?.aborted) {
166
- throw new Error('Step canceled');
167
- }
168
- // Evaluate the command string
169
- const command = ExpressionEvaluator.evaluateString(step.run, context);
170
-
171
- // Check for potential shell injection risks
172
- if (!step.allowInsecure && detectShellInjectionRisk(command)) {
173
- throw new Error(
174
- `Security Error: Command contains shell metacharacters that may indicate injection risk:\n Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}\n To execute this command safely, ensure all user inputs are wrapped in \${{ escape(input) }}.\n\n If you trust this workflow and its inputs, you may need to refactor the step to avoid complex shell chains or use a stricter input validation.\n Or, if you really trust this command, you can set 'allowInsecure: true' in the step definition.`
175
- );
176
- }
177
-
178
- // Evaluate environment variables
179
- const env: Record<string, string> = context.env ? { ...context.env } : {};
180
- if (step.env) {
181
- for (const [key, value] of Object.entries(step.env)) {
182
- env[key] = ExpressionEvaluator.evaluateString(value, context);
183
- }
184
- }
185
-
186
- // Set working directory if specified
187
- const cwd = step.dir ? ExpressionEvaluator.evaluateString(step.dir, context) : undefined;
188
- const mergedEnv = Object.keys(env).length > 0 ? { ...Bun.env, ...env } : Bun.env;
189
-
190
- // Shell metacharacters that require a real shell (including newlines)
191
- const hasShellMetas = /[|&;<>`$!\n]/.test(command);
192
-
193
- // Common shell builtins that must run in a shell
194
- const firstWord = command.trim().split(/\s+/)[0];
195
- const isBuiltin = [
196
- 'exit',
197
- 'cd',
198
- 'export',
199
- 'unset',
200
- 'source',
201
- '.',
202
- 'alias',
203
- 'unalias',
204
- 'eval',
205
- 'set',
206
- 'true',
207
- 'false',
208
- ].includes(firstWord);
209
-
210
- const canUseSpawn = !hasShellMetas && !isBuiltin;
211
-
212
- try {
213
- let stdoutString = '';
214
- let stderrString = '';
215
- let exitCode = 0;
216
- let stdoutTruncated = false;
217
- let stderrTruncated = false;
218
- const maxOutputBytes = LIMITS.MAX_PROCESS_OUTPUT_BYTES;
219
-
220
- if (canUseSpawn) {
221
- // Split command into args without invoking a shell (handles quotes and escapes)
222
- const args: string[] = [];
223
- let current = '';
224
- let inQuote = false;
225
- let quoteChar = '';
226
- let escapeNext = false;
227
-
228
- for (let i = 0; i < command.length; i++) {
229
- const char = command[i];
230
- if (escapeNext) {
231
- current += char;
232
- escapeNext = false;
233
- continue;
234
- }
235
-
236
- if (char === '\\' && quoteChar !== "'") {
237
- escapeNext = true;
238
- continue;
239
- }
240
-
241
- if (char === "'" || char === '"') {
242
- if (inQuote && char === quoteChar) {
243
- inQuote = false;
244
- quoteChar = '';
245
- } else if (!inQuote) {
246
- inQuote = true;
247
- quoteChar = char;
248
- } else {
249
- current += char;
250
- }
251
- continue;
252
- }
253
-
254
- if (/\s/.test(char) && !inQuote) {
255
- if (current) {
256
- args.push(current);
257
- current = '';
258
- }
259
- continue;
260
- }
261
-
262
- current += char;
263
- }
264
- if (escapeNext) {
265
- current += '\\';
266
- }
267
- if (current) args.push(current);
268
-
269
- if (args.length === 0) throw new Error('Empty command');
270
-
271
- const proc = Bun.spawn(args, {
272
- cwd,
273
- env: mergedEnv,
274
- stdout: 'pipe',
275
- stderr: 'pipe',
276
- });
277
- const abortHandler = () => {
278
- try {
279
- proc.kill();
280
- } catch {}
281
- };
282
- if (abortSignal) {
283
- abortSignal.addEventListener('abort', abortHandler, { once: true });
284
- }
285
-
286
- const stdoutPromise = readStreamWithLimit(proc.stdout, maxOutputBytes);
287
- const stderrPromise = readStreamWithLimit(proc.stderr, maxOutputBytes);
288
-
289
- // Wait for exit
290
- exitCode = await proc.exited;
291
- const [stdoutResult, stderrResult] = await Promise.all([stdoutPromise, stderrPromise]);
292
- stdoutString = stdoutResult.text;
293
- stderrString = stderrResult.text;
294
- stdoutTruncated = stdoutResult.truncated;
295
- stderrTruncated = stderrResult.truncated;
296
- if (abortSignal) {
297
- abortSignal.removeEventListener('abort', abortHandler);
298
- }
299
- } else {
300
- // Fallback to sh -c for complex commands (pipes, redirects, quotes)
301
- const proc = Bun.spawn(['sh', '-c', command], {
302
- cwd,
303
- env: mergedEnv,
304
- stdout: 'pipe',
305
- stderr: 'pipe',
306
- });
307
- const abortHandler = () => {
308
- try {
309
- proc.kill();
310
- } catch {}
311
- };
312
- if (abortSignal) {
313
- abortSignal.addEventListener('abort', abortHandler, { once: true });
314
- }
315
-
316
- const stdoutPromise = readStreamWithLimit(proc.stdout, maxOutputBytes);
317
- const stderrPromise = readStreamWithLimit(proc.stderr, maxOutputBytes);
318
-
319
- exitCode = await proc.exited;
320
- const [stdoutResult, stderrResult] = await Promise.all([stdoutPromise, stderrPromise]);
321
- stdoutString = stdoutResult.text;
322
- stderrString = stderrResult.text;
323
- stdoutTruncated = stdoutResult.truncated;
324
- stderrTruncated = stderrResult.truncated;
325
- if (abortSignal) {
326
- abortSignal.removeEventListener('abort', abortHandler);
327
- }
328
- }
329
-
330
- return {
331
- stdout: stdoutString,
332
- stderr: stderrString,
333
- exitCode,
334
- stdoutTruncated,
335
- stderrTruncated,
336
- };
337
- } catch (error) {
338
- // Handle shell execution errors (Bun throws ShellError with exitCode, stdout, stderr)
339
- if (error && typeof error === 'object' && 'exitCode' in error) {
340
- const shellError = error as {
341
- exitCode: number;
342
- stdout?: Buffer | string;
343
- stderr?: Buffer | string;
344
- };
345
-
346
- // Convert stdout/stderr to strings if they're buffers
347
- const stdout = shellError.stdout
348
- ? Buffer.isBuffer(shellError.stdout)
349
- ? shellError.stdout.toString()
350
- : String(shellError.stdout)
351
- : '';
352
- const stderr = shellError.stderr
353
- ? Buffer.isBuffer(shellError.stderr)
354
- ? shellError.stderr.toString()
355
- : String(shellError.stderr)
356
- : '';
357
-
358
- return {
359
- stdout,
360
- stderr,
361
- exitCode: shellError.exitCode,
362
- };
363
- }
364
- throw error;
365
- }
366
- }
@@ -1,5 +0,0 @@
1
- name: invalid-workflow
2
- steps:
3
- - id: step1
4
- type: shell
5
- # Missing 'run' property