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.
- package/README.md +276 -32
- package/package.json +8 -4
- package/src/cli.ts +350 -416
- package/src/commands/doc.ts +31 -0
- package/src/commands/event.ts +29 -0
- package/src/commands/graph.ts +37 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/init.ts +185 -0
- package/src/commands/run.ts +124 -0
- package/src/commands/schema.ts +40 -0
- package/src/commands/utils.ts +78 -0
- package/src/commands/validate.ts +111 -0
- package/src/db/workflow-db.test.ts +314 -0
- package/src/db/workflow-db.ts +810 -210
- package/src/expression/evaluator-audit.test.ts +4 -2
- package/src/expression/evaluator.test.ts +14 -1
- package/src/expression/evaluator.ts +166 -19
- package/src/parser/config-schema.ts +18 -0
- package/src/parser/schema.ts +153 -22
- package/src/parser/test-schema.ts +6 -6
- package/src/parser/workflow-parser.test.ts +24 -0
- package/src/parser/workflow-parser.ts +65 -3
- package/src/runner/auto-heal.test.ts +5 -6
- package/src/runner/blueprint-executor.test.ts +2 -2
- package/src/runner/debug-repl.test.ts +5 -8
- package/src/runner/debug-repl.ts +59 -16
- package/src/runner/durable-timers.test.ts +11 -2
- package/src/runner/engine-executor.test.ts +1 -1
- package/src/runner/events.ts +57 -0
- package/src/runner/executors/artifact-executor.ts +166 -0
- package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
- package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
- package/src/runner/executors/file-executor.test.ts +48 -0
- package/src/runner/executors/file-executor.ts +324 -0
- package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
- package/src/runner/executors/human-executor.ts +144 -0
- package/src/runner/executors/join-executor.ts +75 -0
- package/src/runner/executors/llm-executor.ts +1266 -0
- package/src/runner/executors/memory-executor.ts +71 -0
- package/src/runner/executors/plan-executor.ts +104 -0
- package/src/runner/executors/request-executor.ts +265 -0
- package/src/runner/executors/script-executor.ts +43 -0
- package/src/runner/executors/shell-executor.ts +403 -0
- package/src/runner/executors/subworkflow-executor.ts +114 -0
- package/src/runner/executors/types.ts +69 -0
- package/src/runner/executors/wait-executor.ts +59 -0
- package/src/runner/join-scheduling.test.ts +197 -0
- package/src/runner/llm-adapter-runtime.test.ts +209 -0
- package/src/runner/llm-adapter.test.ts +419 -24
- package/src/runner/llm-adapter.ts +130 -26
- package/src/runner/llm-clarification.test.ts +2 -1
- package/src/runner/llm-executor.test.ts +532 -17
- package/src/runner/mcp-client-audit.test.ts +1 -2
- package/src/runner/mcp-client.ts +136 -46
- package/src/runner/mcp-manager.test.ts +4 -0
- package/src/runner/mcp-server.test.ts +58 -0
- package/src/runner/mcp-server.ts +26 -0
- package/src/runner/memoization.test.ts +190 -0
- package/src/runner/optimization-runner.ts +4 -9
- package/src/runner/quality-gate.test.ts +69 -0
- package/src/runner/reflexion.test.ts +6 -17
- package/src/runner/resource-pool.ts +102 -14
- package/src/runner/services/context-builder.ts +144 -0
- package/src/runner/services/secret-manager.ts +105 -0
- package/src/runner/services/workflow-validator.ts +131 -0
- package/src/runner/shell-executor.test.ts +28 -4
- package/src/runner/standard-tools-ast.test.ts +196 -0
- package/src/runner/standard-tools-execution.test.ts +27 -0
- package/src/runner/standard-tools-integration.test.ts +6 -10
- package/src/runner/standard-tools.ts +339 -102
- package/src/runner/step-executor.test.ts +216 -4
- package/src/runner/step-executor.ts +69 -941
- package/src/runner/stream-utils.ts +7 -3
- package/src/runner/test-harness.ts +20 -1
- package/src/runner/timeout.test.ts +10 -0
- package/src/runner/timeout.ts +11 -2
- package/src/runner/tool-integration.test.ts +1 -1
- package/src/runner/wait-step.test.ts +102 -0
- package/src/runner/workflow-runner.test.ts +208 -15
- package/src/runner/workflow-runner.ts +890 -818
- package/src/runner/workflow-scheduler.ts +75 -0
- package/src/runner/workflow-state.ts +269 -0
- package/src/runner/workflow-subflows.test.ts +13 -12
- package/src/scripts/generate-schemas.ts +16 -0
- package/src/templates/agents/explore.md +1 -0
- package/src/templates/agents/general.md +1 -0
- package/src/templates/agents/handoff-router.md +14 -0
- package/src/templates/agents/handoff-specialist.md +15 -0
- package/src/templates/agents/keystone-architect.md +13 -44
- package/src/templates/agents/my-agent.md +1 -0
- package/src/templates/agents/software-engineer.md +1 -0
- package/src/templates/agents/summarizer.md +1 -0
- package/src/templates/agents/test-agent.md +1 -0
- package/src/templates/agents/tester.md +1 -0
- package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
- package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +2 -1
- package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
- package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
- package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
- package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
- package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
- package/src/templates/control-flow/idempotency-example.yaml +30 -0
- package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
- package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
- package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
- package/src/templates/features/artifact-example.yaml +39 -0
- package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
- package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
- package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
- package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
- package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
- package/src/templates/features/script-example.yaml +27 -0
- package/src/templates/patterns/agent-handoff.yaml +53 -0
- package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
- package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
- package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +1 -0
- package/src/templates/{composition-parent.yaml → patterns/composition-parent.yaml} +1 -0
- package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
- package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
- package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
- package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
- package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
- package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
- package/src/templates/scaffolding/review-loop.yaml +97 -0
- package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
- package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
- package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
- package/src/templates/testing/invalid.yaml +6 -0
- package/src/ui/dashboard.tsx +191 -33
- package/src/utils/auth-manager.test.ts +337 -0
- package/src/utils/auth-manager.ts +157 -61
- package/src/utils/blueprint-utils.ts +4 -6
- package/src/utils/config-loader.test.ts +2 -0
- package/src/utils/config-loader.ts +12 -3
- package/src/utils/constants.ts +76 -0
- package/src/utils/container.ts +63 -0
- package/src/utils/context-injector.test.ts +200 -0
- package/src/utils/context-injector.ts +244 -0
- package/src/utils/doc-generator.ts +85 -0
- package/src/utils/env-filter.ts +45 -0
- package/src/utils/json-parser.test.ts +12 -0
- package/src/utils/json-parser.ts +30 -5
- package/src/utils/logger.ts +12 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.ts +52 -1
- package/src/utils/process-sandbox-worker.test.ts +46 -0
- package/src/utils/process-sandbox.ts +227 -14
- package/src/utils/redactor.test.ts +11 -6
- package/src/utils/redactor.ts +25 -9
- package/src/utils/sandbox.ts +3 -0
- package/src/runner/llm-executor.ts +0 -638
- package/src/runner/shell-executor.ts +0 -366
- 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
|
-
}
|