command-stream 0.0.4 → 0.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/$.mjs +1300 -85
- package/README.md +44 -3
- package/package.json +1 -1
package/$.mjs
CHANGED
|
@@ -10,6 +10,32 @@ import { fileURLToPath } from 'url';
|
|
|
10
10
|
|
|
11
11
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
12
12
|
|
|
13
|
+
// Verbose tracing for debugging (enabled in CI or when COMMAND_STREAM_VERBOSE is set)
|
|
14
|
+
const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';
|
|
15
|
+
|
|
16
|
+
// Trace function for verbose logging
|
|
17
|
+
function trace(category, message, data = {}) {
|
|
18
|
+
if (!VERBOSE) return;
|
|
19
|
+
|
|
20
|
+
const timestamp = new Date().toISOString();
|
|
21
|
+
const dataStr = Object.keys(data).length > 0 ? ' | ' + JSON.stringify(data) : '';
|
|
22
|
+
console.error(`[TRACE ${timestamp}] [${category}] ${message}${dataStr}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Trace decision branches
|
|
26
|
+
function traceBranch(category, condition, branch, data = {}) {
|
|
27
|
+
if (!VERBOSE) return;
|
|
28
|
+
|
|
29
|
+
trace(category, `BRANCH: ${condition} => ${branch}`, data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Trace function entry/exit
|
|
33
|
+
function traceFunc(category, funcName, phase, data = {}) {
|
|
34
|
+
if (!VERBOSE) return;
|
|
35
|
+
|
|
36
|
+
trace(category, `${funcName} ${phase}`, data);
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
// Global shell settings (like bash set -e / set +e)
|
|
14
40
|
let globalShellSettings = {
|
|
15
41
|
errexit: false, // set -e equivalent: exit on error
|
|
@@ -50,6 +76,9 @@ class StreamEmitter {
|
|
|
50
76
|
this.listeners.set(event, []);
|
|
51
77
|
}
|
|
52
78
|
this.listeners.get(event).push(listener);
|
|
79
|
+
|
|
80
|
+
// No auto-start - explicit start() or await will start the process
|
|
81
|
+
|
|
53
82
|
return this;
|
|
54
83
|
}
|
|
55
84
|
|
|
@@ -84,18 +113,28 @@ function quote(value) {
|
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
function buildShellCommand(strings, values) {
|
|
116
|
+
traceFunc('Utils', 'buildShellCommand', 'ENTER', {
|
|
117
|
+
stringsLength: strings.length,
|
|
118
|
+
valuesLength: values.length
|
|
119
|
+
});
|
|
120
|
+
|
|
87
121
|
let out = '';
|
|
88
122
|
for (let i = 0; i < strings.length; i++) {
|
|
89
123
|
out += strings[i];
|
|
90
124
|
if (i < values.length) {
|
|
91
125
|
const v = values[i];
|
|
92
126
|
if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
|
|
127
|
+
traceBranch('Utils', 'buildShellCommand', 'RAW_VALUE', { value: String(v.raw) });
|
|
93
128
|
out += String(v.raw);
|
|
94
129
|
} else {
|
|
95
|
-
|
|
130
|
+
const quoted = quote(v);
|
|
131
|
+
traceBranch('Utils', 'buildShellCommand', 'QUOTED_VALUE', { original: v, quoted });
|
|
132
|
+
out += quoted;
|
|
96
133
|
}
|
|
97
134
|
}
|
|
98
135
|
}
|
|
136
|
+
|
|
137
|
+
traceFunc('Utils', 'buildShellCommand', 'EXIT', { command: out });
|
|
99
138
|
return out;
|
|
100
139
|
}
|
|
101
140
|
|
|
@@ -116,6 +155,12 @@ async function pumpReadable(readable, onChunk) {
|
|
|
116
155
|
class ProcessRunner extends StreamEmitter {
|
|
117
156
|
constructor(spec, options = {}) {
|
|
118
157
|
super();
|
|
158
|
+
|
|
159
|
+
traceFunc('ProcessRunner', 'constructor', 'ENTER', {
|
|
160
|
+
spec: typeof spec === 'object' ? { ...spec, command: spec.command?.slice(0, 100) } : spec,
|
|
161
|
+
options
|
|
162
|
+
});
|
|
163
|
+
|
|
119
164
|
this.spec = spec;
|
|
120
165
|
this.options = {
|
|
121
166
|
mirror: true,
|
|
@@ -139,27 +184,92 @@ class ProcessRunner extends StreamEmitter {
|
|
|
139
184
|
|
|
140
185
|
// Promise for awaiting final result
|
|
141
186
|
this.promise = null;
|
|
187
|
+
|
|
188
|
+
// Track the execution mode
|
|
189
|
+
this._mode = null; // 'async' or 'sync'
|
|
190
|
+
|
|
191
|
+
// Cancellation support for virtual commands
|
|
192
|
+
this._cancelled = false;
|
|
193
|
+
this._virtualGenerator = null;
|
|
194
|
+
this._abortController = new AbortController();
|
|
142
195
|
}
|
|
143
196
|
|
|
144
|
-
async
|
|
145
|
-
|
|
197
|
+
// Unified start method that can work in both async and sync modes
|
|
198
|
+
start(options = {}) {
|
|
199
|
+
const mode = options.mode || 'async';
|
|
200
|
+
|
|
201
|
+
traceFunc('ProcessRunner', 'start', 'ENTER', { mode, options });
|
|
202
|
+
|
|
203
|
+
if (mode === 'sync') {
|
|
204
|
+
traceBranch('ProcessRunner', 'mode', 'sync', {});
|
|
205
|
+
return this._startSync();
|
|
206
|
+
} else {
|
|
207
|
+
traceBranch('ProcessRunner', 'mode', 'async', {});
|
|
208
|
+
return this._startAsync();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Shortcut for sync mode
|
|
213
|
+
sync() {
|
|
214
|
+
return this.start({ mode: 'sync' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Shortcut for async mode
|
|
218
|
+
async() {
|
|
219
|
+
return this.start({ mode: 'async' });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async _startAsync() {
|
|
223
|
+
if (this.started) return this.promise;
|
|
224
|
+
if (this.promise) return this.promise;
|
|
225
|
+
|
|
226
|
+
this.promise = this._doStartAsync();
|
|
227
|
+
return this.promise;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async _doStartAsync() {
|
|
231
|
+
traceFunc('ProcessRunner', '_doStartAsync', 'ENTER', {
|
|
232
|
+
mode: this.spec.mode,
|
|
233
|
+
command: this.spec.command?.slice(0, 100)
|
|
234
|
+
});
|
|
235
|
+
|
|
146
236
|
this.started = true;
|
|
237
|
+
this._mode = 'async';
|
|
147
238
|
|
|
148
239
|
const { cwd, env, stdin } = this.options;
|
|
149
240
|
|
|
150
241
|
// Handle programmatic pipeline mode
|
|
151
242
|
if (this.spec.mode === 'pipeline') {
|
|
243
|
+
traceBranch('ProcessRunner', 'spec.mode', 'pipeline', {
|
|
244
|
+
hasSource: !!this.spec.source,
|
|
245
|
+
hasDestination: !!this.spec.destination
|
|
246
|
+
});
|
|
152
247
|
return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
|
|
153
248
|
}
|
|
154
249
|
|
|
155
250
|
// Check if this is a virtual command first
|
|
156
251
|
if (this.spec.mode === 'shell') {
|
|
252
|
+
traceBranch('ProcessRunner', 'spec.mode', 'shell', {});
|
|
253
|
+
|
|
157
254
|
// Parse the command to check for virtual commands or pipelines
|
|
158
255
|
const parsed = this._parseCommand(this.spec.command);
|
|
256
|
+
trace('ProcessRunner', 'Parsed command', {
|
|
257
|
+
type: parsed?.type,
|
|
258
|
+
cmd: parsed?.cmd,
|
|
259
|
+
argsCount: parsed?.args?.length
|
|
260
|
+
});
|
|
261
|
+
|
|
159
262
|
if (parsed) {
|
|
160
263
|
if (parsed.type === 'pipeline') {
|
|
264
|
+
traceBranch('ProcessRunner', 'parsed.type', 'pipeline', {
|
|
265
|
+
commandCount: parsed.commands?.length
|
|
266
|
+
});
|
|
161
267
|
return await this._runPipeline(parsed.commands);
|
|
162
268
|
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
269
|
+
traceBranch('ProcessRunner', 'virtualCommand', parsed.cmd, {
|
|
270
|
+
isVirtual: true,
|
|
271
|
+
args: parsed.args
|
|
272
|
+
});
|
|
163
273
|
return await this._runVirtual(parsed.cmd, parsed.args);
|
|
164
274
|
}
|
|
165
275
|
}
|
|
@@ -236,8 +346,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
236
346
|
const code = await exited;
|
|
237
347
|
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
238
348
|
|
|
239
|
-
|
|
349
|
+
// Debug: Check the raw exit code
|
|
350
|
+
trace('ProcessRunner', 'Raw exit code from child', {
|
|
240
351
|
code,
|
|
352
|
+
codeType: typeof code,
|
|
353
|
+
childExitCode: this.child?.exitCode,
|
|
354
|
+
isBun
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const resultData = {
|
|
358
|
+
code: code ?? 0, // Default to 0 if exit code is null/undefined
|
|
241
359
|
stdout: this.options.capture ? Buffer.concat(this.outChunks).toString('utf8') : undefined,
|
|
242
360
|
stderr: this.options.capture ? Buffer.concat(this.errChunks).toString('utf8') : undefined,
|
|
243
361
|
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
@@ -311,12 +429,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
311
429
|
|
|
312
430
|
const cmd = parts[0];
|
|
313
431
|
const args = parts.slice(1).map(arg => {
|
|
314
|
-
//
|
|
432
|
+
// Keep track of whether the arg was quoted
|
|
315
433
|
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
316
434
|
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
317
|
-
return arg.slice(1, -1);
|
|
435
|
+
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
318
436
|
}
|
|
319
|
-
return arg;
|
|
437
|
+
return { value: arg, quoted: false };
|
|
320
438
|
});
|
|
321
439
|
|
|
322
440
|
return { cmd, args, type: 'simple' };
|
|
@@ -359,11 +477,13 @@ class ProcessRunner extends StreamEmitter {
|
|
|
359
477
|
|
|
360
478
|
const cmd = parts[0];
|
|
361
479
|
const args = parts.slice(1).map(arg => {
|
|
480
|
+
// Keep track of whether the arg was quoted
|
|
362
481
|
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
363
482
|
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
364
|
-
|
|
483
|
+
// Store the original with quotes for system commands
|
|
484
|
+
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
365
485
|
}
|
|
366
|
-
return arg;
|
|
486
|
+
return { value: arg, quoted: false };
|
|
367
487
|
});
|
|
368
488
|
|
|
369
489
|
return { cmd, args };
|
|
@@ -373,11 +493,19 @@ class ProcessRunner extends StreamEmitter {
|
|
|
373
493
|
}
|
|
374
494
|
|
|
375
495
|
async _runVirtual(cmd, args) {
|
|
496
|
+
traceFunc('ProcessRunner', '_runVirtual', 'ENTER', { cmd, args });
|
|
497
|
+
|
|
376
498
|
const handler = virtualCommands.get(cmd);
|
|
377
499
|
if (!handler) {
|
|
500
|
+
trace('ProcessRunner', 'Virtual command not found', { cmd });
|
|
378
501
|
throw new Error(`Virtual command not found: ${cmd}`);
|
|
379
502
|
}
|
|
380
503
|
|
|
504
|
+
trace('ProcessRunner', 'Found virtual command handler', {
|
|
505
|
+
cmd,
|
|
506
|
+
isGenerator: handler.constructor.name === 'AsyncGeneratorFunction'
|
|
507
|
+
});
|
|
508
|
+
|
|
381
509
|
try {
|
|
382
510
|
// Prepare stdin
|
|
383
511
|
let stdinData = '';
|
|
@@ -387,12 +515,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
387
515
|
stdinData = this.options.stdin.toString('utf8');
|
|
388
516
|
}
|
|
389
517
|
|
|
518
|
+
// Extract actual values for virtual command
|
|
519
|
+
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
520
|
+
|
|
390
521
|
// Shell tracing for virtual commands
|
|
391
522
|
if (globalShellSettings.xtrace) {
|
|
392
|
-
console.log(`+ ${cmd} ${
|
|
523
|
+
console.log(`+ ${cmd} ${argValues.join(' ')}`);
|
|
393
524
|
}
|
|
394
525
|
if (globalShellSettings.verbose) {
|
|
395
|
-
console.log(`${cmd} ${
|
|
526
|
+
console.log(`${cmd} ${argValues.join(' ')}`);
|
|
396
527
|
}
|
|
397
528
|
|
|
398
529
|
// Execute the virtual command
|
|
@@ -400,18 +531,65 @@ class ProcessRunner extends StreamEmitter {
|
|
|
400
531
|
|
|
401
532
|
// Check if handler is async generator (streaming)
|
|
402
533
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
403
|
-
// Handle streaming virtual command
|
|
534
|
+
// Handle streaming virtual command with cancellation support
|
|
404
535
|
const chunks = [];
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
536
|
+
|
|
537
|
+
// Create options with cancellation check and abort signal
|
|
538
|
+
const commandOptions = {
|
|
539
|
+
...this.options,
|
|
540
|
+
isCancelled: () => this._cancelled,
|
|
541
|
+
signal: this._abortController.signal
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const generator = handler(argValues, stdinData, commandOptions);
|
|
545
|
+
this._virtualGenerator = generator;
|
|
546
|
+
|
|
547
|
+
// Create a promise that resolves when cancelled
|
|
548
|
+
const cancelPromise = new Promise(resolve => {
|
|
549
|
+
this._cancelResolve = resolve;
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const iterator = generator[Symbol.asyncIterator]();
|
|
554
|
+
let done = false;
|
|
408
555
|
|
|
409
|
-
|
|
410
|
-
|
|
556
|
+
while (!done && !this._cancelled) {
|
|
557
|
+
// Race between getting next value and cancellation
|
|
558
|
+
const result = await Promise.race([
|
|
559
|
+
iterator.next(),
|
|
560
|
+
cancelPromise.then(() => ({ done: true, cancelled: true }))
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
if (result.cancelled || this._cancelled) {
|
|
564
|
+
// Cancelled - close the generator
|
|
565
|
+
if (iterator.return) {
|
|
566
|
+
await iterator.return();
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
done = result.done;
|
|
572
|
+
|
|
573
|
+
if (!done) {
|
|
574
|
+
const chunk = result.value;
|
|
575
|
+
const buf = Buffer.from(chunk);
|
|
576
|
+
chunks.push(buf);
|
|
577
|
+
|
|
578
|
+
// Only output if not cancelled
|
|
579
|
+
if (!this._cancelled) {
|
|
580
|
+
if (this.options.mirror) {
|
|
581
|
+
process.stdout.write(buf);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
this.emit('stdout', buf);
|
|
585
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
411
588
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.
|
|
589
|
+
} finally {
|
|
590
|
+
// Clean up
|
|
591
|
+
this._virtualGenerator = null;
|
|
592
|
+
this._cancelResolve = null;
|
|
415
593
|
}
|
|
416
594
|
|
|
417
595
|
result = {
|
|
@@ -422,7 +600,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
422
600
|
};
|
|
423
601
|
} else {
|
|
424
602
|
// Regular async function
|
|
425
|
-
result = await handler(
|
|
603
|
+
result = await handler(argValues, stdinData, this.options);
|
|
426
604
|
|
|
427
605
|
// Ensure result has required fields, respecting capture option
|
|
428
606
|
result = {
|
|
@@ -489,26 +667,642 @@ class ProcessRunner extends StreamEmitter {
|
|
|
489
667
|
if (this.options.mirror) {
|
|
490
668
|
process.stderr.write(buf);
|
|
491
669
|
}
|
|
492
|
-
this.emit('stderr', buf);
|
|
493
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
this.emit('end', result);
|
|
497
|
-
this.emit('exit', result.code);
|
|
498
|
-
|
|
499
|
-
if (globalShellSettings.errexit) {
|
|
500
|
-
throw error;
|
|
670
|
+
this.emit('stderr', buf);
|
|
671
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
this.emit('end', result);
|
|
675
|
+
this.emit('exit', result.code);
|
|
676
|
+
|
|
677
|
+
if (globalShellSettings.errexit) {
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async _runStreamingPipelineBun(commands) {
|
|
686
|
+
traceFunc('ProcessRunner', '_runStreamingPipelineBun', 'ENTER', {
|
|
687
|
+
commandsCount: commands.length
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// For true streaming, we need to handle virtual and real commands differently
|
|
691
|
+
// but make them work together seamlessly
|
|
692
|
+
|
|
693
|
+
// First, analyze the pipeline to identify virtual vs real commands
|
|
694
|
+
const pipelineInfo = commands.map(command => {
|
|
695
|
+
const { cmd, args } = command;
|
|
696
|
+
const isVirtual = virtualCommandsEnabled && virtualCommands.has(cmd);
|
|
697
|
+
return { ...command, isVirtual };
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
trace('ProcessRunner', 'Pipeline analysis', {
|
|
701
|
+
virtualCount: pipelineInfo.filter(p => p.isVirtual).length,
|
|
702
|
+
realCount: pipelineInfo.filter(p => !p.isVirtual).length
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// If pipeline contains virtual commands, use advanced streaming
|
|
706
|
+
if (pipelineInfo.some(info => info.isVirtual)) {
|
|
707
|
+
traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'MIXED_PIPELINE', {});
|
|
708
|
+
return this._runMixedStreamingPipeline(commands);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// For pipelines with commands that buffer (like jq), use tee streaming
|
|
712
|
+
const needsStreamingWorkaround = commands.some(c =>
|
|
713
|
+
c.cmd === 'jq' || c.cmd === 'grep' || c.cmd === 'sed' || c.cmd === 'cat' || c.cmd === 'awk'
|
|
714
|
+
);
|
|
715
|
+
if (needsStreamingWorkaround) {
|
|
716
|
+
traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'TEE_STREAMING', {
|
|
717
|
+
bufferedCommands: commands.filter(c =>
|
|
718
|
+
['jq', 'grep', 'sed', 'cat', 'awk'].includes(c.cmd)
|
|
719
|
+
).map(c => c.cmd)
|
|
720
|
+
});
|
|
721
|
+
return this._runTeeStreamingPipeline(commands);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// All real commands - use native pipe connections
|
|
725
|
+
const processes = [];
|
|
726
|
+
let allStderr = '';
|
|
727
|
+
|
|
728
|
+
for (let i = 0; i < commands.length; i++) {
|
|
729
|
+
const command = commands[i];
|
|
730
|
+
const { cmd, args } = command;
|
|
731
|
+
|
|
732
|
+
// Build command string
|
|
733
|
+
const commandParts = [cmd];
|
|
734
|
+
for (const arg of args) {
|
|
735
|
+
if (arg.value !== undefined) {
|
|
736
|
+
if (arg.quoted) {
|
|
737
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
738
|
+
} else if (arg.value.includes(' ')) {
|
|
739
|
+
commandParts.push(`"${arg.value}"`);
|
|
740
|
+
} else {
|
|
741
|
+
commandParts.push(arg.value);
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
745
|
+
commandParts.push(`"${arg}"`);
|
|
746
|
+
} else {
|
|
747
|
+
commandParts.push(arg);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const commandStr = commandParts.join(' ');
|
|
752
|
+
|
|
753
|
+
// Determine stdin for this process
|
|
754
|
+
let stdin;
|
|
755
|
+
let needsManualStdin = false;
|
|
756
|
+
let stdinData;
|
|
757
|
+
|
|
758
|
+
if (i === 0) {
|
|
759
|
+
// First command - use provided stdin or pipe
|
|
760
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
761
|
+
stdin = 'pipe';
|
|
762
|
+
needsManualStdin = true;
|
|
763
|
+
stdinData = Buffer.from(this.options.stdin);
|
|
764
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
765
|
+
stdin = 'pipe';
|
|
766
|
+
needsManualStdin = true;
|
|
767
|
+
stdinData = this.options.stdin;
|
|
768
|
+
} else {
|
|
769
|
+
stdin = 'ignore';
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
// Connect to previous process stdout
|
|
773
|
+
stdin = processes[i - 1].stdout;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Spawn the process directly (not through sh) for better streaming
|
|
777
|
+
// Only use sh -c for complex commands that need shell features
|
|
778
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
779
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
780
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
781
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
782
|
+
|
|
783
|
+
const spawnArgs = needsShell
|
|
784
|
+
? ['sh', '-c', commandStr]
|
|
785
|
+
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
786
|
+
|
|
787
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
788
|
+
cwd: this.options.cwd,
|
|
789
|
+
env: this.options.env,
|
|
790
|
+
stdin: stdin,
|
|
791
|
+
stdout: 'pipe',
|
|
792
|
+
stderr: 'pipe'
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Write stdin data if needed for first process
|
|
796
|
+
if (needsManualStdin && stdinData && proc.stdin) {
|
|
797
|
+
(async () => {
|
|
798
|
+
try {
|
|
799
|
+
// Bun's FileSink has write and end methods
|
|
800
|
+
await proc.stdin.write(stdinData);
|
|
801
|
+
await proc.stdin.end();
|
|
802
|
+
} catch (e) {
|
|
803
|
+
console.error('Error writing stdin:', e);
|
|
804
|
+
}
|
|
805
|
+
})();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
processes.push(proc);
|
|
809
|
+
|
|
810
|
+
// Collect stderr from all processes
|
|
811
|
+
(async () => {
|
|
812
|
+
for await (const chunk of proc.stderr) {
|
|
813
|
+
const buf = Buffer.from(chunk);
|
|
814
|
+
allStderr += buf.toString();
|
|
815
|
+
// Only emit stderr for the last command
|
|
816
|
+
if (i === commands.length - 1) {
|
|
817
|
+
if (this.options.mirror) {
|
|
818
|
+
process.stderr.write(buf);
|
|
819
|
+
}
|
|
820
|
+
this.emit('stderr', buf);
|
|
821
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
})();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Stream output from the last process
|
|
828
|
+
const lastProc = processes[processes.length - 1];
|
|
829
|
+
let finalOutput = '';
|
|
830
|
+
|
|
831
|
+
// Stream stdout from last process
|
|
832
|
+
for await (const chunk of lastProc.stdout) {
|
|
833
|
+
const buf = Buffer.from(chunk);
|
|
834
|
+
finalOutput += buf.toString();
|
|
835
|
+
if (this.options.mirror) {
|
|
836
|
+
process.stdout.write(buf);
|
|
837
|
+
}
|
|
838
|
+
this.emit('stdout', buf);
|
|
839
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Wait for all processes to complete
|
|
843
|
+
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
844
|
+
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
845
|
+
|
|
846
|
+
// Check for pipeline failures if pipefail is set
|
|
847
|
+
if (globalShellSettings.pipefail) {
|
|
848
|
+
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
849
|
+
if (failedIndex !== -1) {
|
|
850
|
+
const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`);
|
|
851
|
+
error.code = exitCodes[failedIndex];
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const result = createResult({
|
|
857
|
+
code: lastExitCode || 0,
|
|
858
|
+
stdout: finalOutput,
|
|
859
|
+
stderr: allStderr,
|
|
860
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
861
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
this.result = result;
|
|
865
|
+
this.finished = true;
|
|
866
|
+
|
|
867
|
+
this.emit('end', result);
|
|
868
|
+
this.emit('exit', result.code);
|
|
869
|
+
|
|
870
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
871
|
+
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
872
|
+
error.code = result.code;
|
|
873
|
+
error.stdout = result.stdout;
|
|
874
|
+
error.stderr = result.stderr;
|
|
875
|
+
error.result = result;
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async _runTeeStreamingPipeline(commands) {
|
|
883
|
+
traceFunc('ProcessRunner', '_runTeeStreamingPipeline', 'ENTER', {
|
|
884
|
+
commandsCount: commands.length
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// Use tee() to split streams for real-time reading
|
|
888
|
+
// This works around jq and similar commands that buffer when piped
|
|
889
|
+
|
|
890
|
+
const processes = [];
|
|
891
|
+
let allStderr = '';
|
|
892
|
+
let currentStream = null;
|
|
893
|
+
|
|
894
|
+
for (let i = 0; i < commands.length; i++) {
|
|
895
|
+
const command = commands[i];
|
|
896
|
+
const { cmd, args } = command;
|
|
897
|
+
|
|
898
|
+
// Build command string
|
|
899
|
+
const commandParts = [cmd];
|
|
900
|
+
for (const arg of args) {
|
|
901
|
+
if (arg.value !== undefined) {
|
|
902
|
+
if (arg.quoted) {
|
|
903
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
904
|
+
} else if (arg.value.includes(' ')) {
|
|
905
|
+
commandParts.push(`"${arg.value}"`);
|
|
906
|
+
} else {
|
|
907
|
+
commandParts.push(arg.value);
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
911
|
+
commandParts.push(`"${arg}"`);
|
|
912
|
+
} else {
|
|
913
|
+
commandParts.push(arg);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const commandStr = commandParts.join(' ');
|
|
918
|
+
|
|
919
|
+
// Determine stdin for this process
|
|
920
|
+
let stdin;
|
|
921
|
+
let needsManualStdin = false;
|
|
922
|
+
let stdinData;
|
|
923
|
+
|
|
924
|
+
if (i === 0) {
|
|
925
|
+
// First command - use provided stdin or ignore
|
|
926
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
927
|
+
stdin = 'pipe';
|
|
928
|
+
needsManualStdin = true;
|
|
929
|
+
stdinData = Buffer.from(this.options.stdin);
|
|
930
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
931
|
+
stdin = 'pipe';
|
|
932
|
+
needsManualStdin = true;
|
|
933
|
+
stdinData = this.options.stdin;
|
|
934
|
+
} else {
|
|
935
|
+
stdin = 'ignore';
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
// Use the stream from previous process
|
|
939
|
+
stdin = currentStream;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Spawn the process directly (not through sh) for better control
|
|
943
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
944
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
945
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
946
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
947
|
+
|
|
948
|
+
const spawnArgs = needsShell
|
|
949
|
+
? ['sh', '-c', commandStr]
|
|
950
|
+
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
951
|
+
|
|
952
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
953
|
+
cwd: this.options.cwd,
|
|
954
|
+
env: this.options.env,
|
|
955
|
+
stdin: stdin,
|
|
956
|
+
stdout: 'pipe',
|
|
957
|
+
stderr: 'pipe'
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Write stdin data if needed for first process
|
|
961
|
+
if (needsManualStdin && stdinData && proc.stdin) {
|
|
962
|
+
try {
|
|
963
|
+
await proc.stdin.write(stdinData);
|
|
964
|
+
await proc.stdin.end();
|
|
965
|
+
} catch (e) {
|
|
966
|
+
// Ignore stdin errors
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
processes.push(proc);
|
|
971
|
+
|
|
972
|
+
// For non-last processes, tee the output so we can both pipe and read
|
|
973
|
+
if (i < commands.length - 1) {
|
|
974
|
+
const [readStream, pipeStream] = proc.stdout.tee();
|
|
975
|
+
currentStream = pipeStream;
|
|
976
|
+
|
|
977
|
+
// Read from the tee'd stream for real-time updates
|
|
978
|
+
// Always read from the first process for best streaming
|
|
979
|
+
if (i === 0) {
|
|
980
|
+
(async () => {
|
|
981
|
+
for await (const chunk of readStream) {
|
|
982
|
+
// Emit from the first process for real-time updates
|
|
983
|
+
const buf = Buffer.from(chunk);
|
|
984
|
+
if (this.options.mirror) {
|
|
985
|
+
process.stdout.write(buf);
|
|
986
|
+
}
|
|
987
|
+
this.emit('stdout', buf);
|
|
988
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
989
|
+
}
|
|
990
|
+
})();
|
|
991
|
+
} else {
|
|
992
|
+
// Consume other tee'd streams to prevent blocking
|
|
993
|
+
(async () => {
|
|
994
|
+
for await (const chunk of readStream) {
|
|
995
|
+
// Just consume to keep flowing
|
|
996
|
+
}
|
|
997
|
+
})();
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
currentStream = proc.stdout;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Collect stderr from all processes
|
|
1004
|
+
(async () => {
|
|
1005
|
+
for await (const chunk of proc.stderr) {
|
|
1006
|
+
const buf = Buffer.from(chunk);
|
|
1007
|
+
allStderr += buf.toString();
|
|
1008
|
+
if (i === commands.length - 1) {
|
|
1009
|
+
if (this.options.mirror) {
|
|
1010
|
+
process.stderr.write(buf);
|
|
1011
|
+
}
|
|
1012
|
+
this.emit('stderr', buf);
|
|
1013
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
})();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Read final output from the last process
|
|
1020
|
+
const lastProc = processes[processes.length - 1];
|
|
1021
|
+
let finalOutput = '';
|
|
1022
|
+
|
|
1023
|
+
// If we haven't emitted stdout yet (no tee), emit from last process
|
|
1024
|
+
const shouldEmitFromLast = commands.length === 1;
|
|
1025
|
+
|
|
1026
|
+
for await (const chunk of lastProc.stdout) {
|
|
1027
|
+
const buf = Buffer.from(chunk);
|
|
1028
|
+
finalOutput += buf.toString();
|
|
1029
|
+
if (shouldEmitFromLast) {
|
|
1030
|
+
if (this.options.mirror) {
|
|
1031
|
+
process.stdout.write(buf);
|
|
1032
|
+
}
|
|
1033
|
+
this.emit('stdout', buf);
|
|
1034
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Wait for all processes to complete
|
|
1039
|
+
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
1040
|
+
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
1041
|
+
|
|
1042
|
+
// Check for pipeline failures if pipefail is set
|
|
1043
|
+
if (globalShellSettings.pipefail) {
|
|
1044
|
+
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
1045
|
+
if (failedIndex !== -1) {
|
|
1046
|
+
const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`);
|
|
1047
|
+
error.code = exitCodes[failedIndex];
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const result = createResult({
|
|
1053
|
+
code: lastExitCode || 0,
|
|
1054
|
+
stdout: finalOutput,
|
|
1055
|
+
stderr: allStderr,
|
|
1056
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1057
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
this.result = result;
|
|
1061
|
+
this.finished = true;
|
|
1062
|
+
|
|
1063
|
+
this.emit('end', result);
|
|
1064
|
+
this.emit('exit', result.code);
|
|
1065
|
+
|
|
1066
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1067
|
+
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
1068
|
+
error.code = result.code;
|
|
1069
|
+
error.stdout = result.stdout;
|
|
1070
|
+
error.stderr = result.stderr;
|
|
1071
|
+
error.result = result;
|
|
1072
|
+
throw error;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return result;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
async _runMixedStreamingPipeline(commands) {
|
|
1080
|
+
traceFunc('ProcessRunner', '_runMixedStreamingPipeline', 'ENTER', {
|
|
1081
|
+
commandsCount: commands.length
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// Handle pipelines with both virtual and real commands
|
|
1085
|
+
// Each stage reads from previous stage's output stream
|
|
1086
|
+
|
|
1087
|
+
let currentInputStream = null;
|
|
1088
|
+
let finalOutput = '';
|
|
1089
|
+
let allStderr = '';
|
|
1090
|
+
|
|
1091
|
+
// Set up initial input stream if provided
|
|
1092
|
+
if (this.options.stdin) {
|
|
1093
|
+
const inputData = typeof this.options.stdin === 'string'
|
|
1094
|
+
? this.options.stdin
|
|
1095
|
+
: this.options.stdin.toString('utf8');
|
|
1096
|
+
|
|
1097
|
+
// Create a readable stream from the input
|
|
1098
|
+
currentInputStream = new ReadableStream({
|
|
1099
|
+
start(controller) {
|
|
1100
|
+
controller.enqueue(new TextEncoder().encode(inputData));
|
|
1101
|
+
controller.close();
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1107
|
+
const command = commands[i];
|
|
1108
|
+
const { cmd, args } = command;
|
|
1109
|
+
const isLastCommand = i === commands.length - 1;
|
|
1110
|
+
|
|
1111
|
+
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1112
|
+
// Handle virtual command with streaming
|
|
1113
|
+
traceBranch('ProcessRunner', '_runMixedStreamingPipeline', 'VIRTUAL_COMMAND', {
|
|
1114
|
+
cmd,
|
|
1115
|
+
commandIndex: i
|
|
1116
|
+
});
|
|
1117
|
+
const handler = virtualCommands.get(cmd);
|
|
1118
|
+
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
1119
|
+
|
|
1120
|
+
// Read input from stream if available
|
|
1121
|
+
let inputData = '';
|
|
1122
|
+
if (currentInputStream) {
|
|
1123
|
+
const reader = currentInputStream.getReader();
|
|
1124
|
+
try {
|
|
1125
|
+
while (true) {
|
|
1126
|
+
const { done, value } = await reader.read();
|
|
1127
|
+
if (done) break;
|
|
1128
|
+
inputData += new TextDecoder().decode(value);
|
|
1129
|
+
}
|
|
1130
|
+
} finally {
|
|
1131
|
+
reader.releaseLock();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Check if handler is async generator (streaming)
|
|
1136
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1137
|
+
// Create output stream from generator
|
|
1138
|
+
const chunks = [];
|
|
1139
|
+
const self = this; // Capture this context
|
|
1140
|
+
currentInputStream = new ReadableStream({
|
|
1141
|
+
async start(controller) {
|
|
1142
|
+
for await (const chunk of handler(argValues, inputData, {})) {
|
|
1143
|
+
const data = Buffer.from(chunk);
|
|
1144
|
+
controller.enqueue(data);
|
|
1145
|
+
|
|
1146
|
+
// Emit for last command
|
|
1147
|
+
if (isLastCommand) {
|
|
1148
|
+
chunks.push(data);
|
|
1149
|
+
if (self.options.mirror) {
|
|
1150
|
+
process.stdout.write(data);
|
|
1151
|
+
}
|
|
1152
|
+
self.emit('stdout', data);
|
|
1153
|
+
self.emit('data', { type: 'stdout', data });
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
controller.close();
|
|
1157
|
+
|
|
1158
|
+
if (isLastCommand) {
|
|
1159
|
+
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
} else {
|
|
1164
|
+
// Regular async function
|
|
1165
|
+
const result = await handler(argValues, inputData, {});
|
|
1166
|
+
const outputData = result.stdout || '';
|
|
1167
|
+
|
|
1168
|
+
if (isLastCommand) {
|
|
1169
|
+
finalOutput = outputData;
|
|
1170
|
+
const buf = Buffer.from(outputData);
|
|
1171
|
+
if (this.options.mirror) {
|
|
1172
|
+
process.stdout.write(buf);
|
|
1173
|
+
}
|
|
1174
|
+
this.emit('stdout', buf);
|
|
1175
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Create stream from output
|
|
1179
|
+
currentInputStream = new ReadableStream({
|
|
1180
|
+
start(controller) {
|
|
1181
|
+
controller.enqueue(new TextEncoder().encode(outputData));
|
|
1182
|
+
controller.close();
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
if (result.stderr) {
|
|
1187
|
+
allStderr += result.stderr;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
// Handle real command - spawn with streaming
|
|
1192
|
+
const commandParts = [cmd];
|
|
1193
|
+
for (const arg of args) {
|
|
1194
|
+
if (arg.value !== undefined) {
|
|
1195
|
+
if (arg.quoted) {
|
|
1196
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
1197
|
+
} else if (arg.value.includes(' ')) {
|
|
1198
|
+
commandParts.push(`"${arg.value}"`);
|
|
1199
|
+
} else {
|
|
1200
|
+
commandParts.push(arg.value);
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
1204
|
+
commandParts.push(`"${arg}"`);
|
|
1205
|
+
} else {
|
|
1206
|
+
commandParts.push(arg);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const commandStr = commandParts.join(' ');
|
|
1211
|
+
|
|
1212
|
+
// Spawn the process
|
|
1213
|
+
const proc = Bun.spawn(['sh', '-c', commandStr], {
|
|
1214
|
+
cwd: this.options.cwd,
|
|
1215
|
+
env: this.options.env,
|
|
1216
|
+
stdin: currentInputStream ? 'pipe' : 'ignore',
|
|
1217
|
+
stdout: 'pipe',
|
|
1218
|
+
stderr: 'pipe'
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// Write input stream to process stdin if needed
|
|
1222
|
+
if (currentInputStream && proc.stdin) {
|
|
1223
|
+
const reader = currentInputStream.getReader();
|
|
1224
|
+
const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
|
|
1225
|
+
|
|
1226
|
+
(async () => {
|
|
1227
|
+
try {
|
|
1228
|
+
while (true) {
|
|
1229
|
+
const { done, value } = await reader.read();
|
|
1230
|
+
if (done) break;
|
|
1231
|
+
if (writer.write) {
|
|
1232
|
+
await writer.write(value);
|
|
1233
|
+
} else if (writer.getWriter) {
|
|
1234
|
+
const w = writer.getWriter();
|
|
1235
|
+
await w.write(value);
|
|
1236
|
+
w.releaseLock();
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
} finally {
|
|
1240
|
+
reader.releaseLock();
|
|
1241
|
+
if (writer.close) await writer.close();
|
|
1242
|
+
else if (writer.end) writer.end();
|
|
1243
|
+
}
|
|
1244
|
+
})();
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Set up output stream
|
|
1248
|
+
currentInputStream = proc.stdout;
|
|
1249
|
+
|
|
1250
|
+
// Handle stderr
|
|
1251
|
+
(async () => {
|
|
1252
|
+
for await (const chunk of proc.stderr) {
|
|
1253
|
+
const buf = Buffer.from(chunk);
|
|
1254
|
+
allStderr += buf.toString();
|
|
1255
|
+
if (isLastCommand) {
|
|
1256
|
+
if (this.options.mirror) {
|
|
1257
|
+
process.stderr.write(buf);
|
|
1258
|
+
}
|
|
1259
|
+
this.emit('stderr', buf);
|
|
1260
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
})();
|
|
1264
|
+
|
|
1265
|
+
// For last command, stream output
|
|
1266
|
+
if (isLastCommand) {
|
|
1267
|
+
const chunks = [];
|
|
1268
|
+
for await (const chunk of proc.stdout) {
|
|
1269
|
+
const buf = Buffer.from(chunk);
|
|
1270
|
+
chunks.push(buf);
|
|
1271
|
+
if (this.options.mirror) {
|
|
1272
|
+
process.stdout.write(buf);
|
|
1273
|
+
}
|
|
1274
|
+
this.emit('stdout', buf);
|
|
1275
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1276
|
+
}
|
|
1277
|
+
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1278
|
+
await proc.exited;
|
|
1279
|
+
}
|
|
501
1280
|
}
|
|
502
|
-
|
|
503
|
-
return result;
|
|
504
1281
|
}
|
|
1282
|
+
|
|
1283
|
+
const result = createResult({
|
|
1284
|
+
code: 0, // TODO: Track exit codes properly
|
|
1285
|
+
stdout: finalOutput,
|
|
1286
|
+
stderr: allStderr,
|
|
1287
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1288
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
this.result = result;
|
|
1292
|
+
this.finished = true;
|
|
1293
|
+
|
|
1294
|
+
this.emit('end', result);
|
|
1295
|
+
this.emit('exit', result.code);
|
|
1296
|
+
|
|
1297
|
+
return result;
|
|
505
1298
|
}
|
|
506
1299
|
|
|
507
|
-
async
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
1300
|
+
async _runPipelineNonStreaming(commands) {
|
|
1301
|
+
traceFunc('ProcessRunner', '_runPipelineNonStreaming', 'ENTER', {
|
|
1302
|
+
commandsCount: commands.length
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Original non-streaming implementation for fallback (e.g., virtual commands)
|
|
512
1306
|
let currentOutput = '';
|
|
513
1307
|
let currentInput = '';
|
|
514
1308
|
|
|
@@ -524,26 +1318,35 @@ class ProcessRunner extends StreamEmitter {
|
|
|
524
1318
|
const command = commands[i];
|
|
525
1319
|
const { cmd, args } = command;
|
|
526
1320
|
|
|
527
|
-
// Check if this is a virtual command
|
|
1321
|
+
// Check if this is a virtual command (only if virtual commands are enabled)
|
|
528
1322
|
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1323
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'VIRTUAL_COMMAND', {
|
|
1324
|
+
cmd,
|
|
1325
|
+
argsCount: args.length
|
|
1326
|
+
});
|
|
1327
|
+
|
|
529
1328
|
// Run virtual command with current input
|
|
530
1329
|
const handler = virtualCommands.get(cmd);
|
|
531
1330
|
|
|
532
1331
|
try {
|
|
1332
|
+
// Extract actual values for virtual command
|
|
1333
|
+
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
1334
|
+
|
|
533
1335
|
// Shell tracing for virtual commands
|
|
534
1336
|
if (globalShellSettings.xtrace) {
|
|
535
|
-
console.log(`+ ${cmd} ${
|
|
1337
|
+
console.log(`+ ${cmd} ${argValues.join(' ')}`);
|
|
536
1338
|
}
|
|
537
1339
|
if (globalShellSettings.verbose) {
|
|
538
|
-
console.log(`${cmd} ${
|
|
1340
|
+
console.log(`${cmd} ${argValues.join(' ')}`);
|
|
539
1341
|
}
|
|
540
1342
|
|
|
541
1343
|
let result;
|
|
542
1344
|
|
|
543
1345
|
// Check if handler is async generator (streaming)
|
|
544
1346
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1347
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'ASYNC_GENERATOR', { cmd });
|
|
545
1348
|
const chunks = [];
|
|
546
|
-
for await (const chunk of handler(
|
|
1349
|
+
for await (const chunk of handler(argValues, currentInput, this.options)) {
|
|
547
1350
|
chunks.push(Buffer.from(chunk));
|
|
548
1351
|
}
|
|
549
1352
|
result = {
|
|
@@ -554,7 +1357,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
554
1357
|
};
|
|
555
1358
|
} else {
|
|
556
1359
|
// Regular async function
|
|
557
|
-
result = await handler(
|
|
1360
|
+
result = await handler(argValues, currentInput, this.options);
|
|
558
1361
|
result = {
|
|
559
1362
|
code: result.code ?? 0,
|
|
560
1363
|
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
@@ -660,54 +1463,262 @@ class ProcessRunner extends StreamEmitter {
|
|
|
660
1463
|
return result;
|
|
661
1464
|
}
|
|
662
1465
|
} else {
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1466
|
+
// Execute system command in pipeline
|
|
1467
|
+
try {
|
|
1468
|
+
// Build command string for this part of the pipeline
|
|
1469
|
+
const commandParts = [cmd];
|
|
1470
|
+
for (const arg of args) {
|
|
1471
|
+
if (arg.value !== undefined) {
|
|
1472
|
+
// Handle our parsed arg structure
|
|
1473
|
+
if (arg.quoted) {
|
|
1474
|
+
// Preserve original quotes
|
|
1475
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
1476
|
+
} else if (arg.value.includes(' ')) {
|
|
1477
|
+
// Quote if contains spaces
|
|
1478
|
+
commandParts.push(`"${arg.value}"`);
|
|
1479
|
+
} else {
|
|
1480
|
+
commandParts.push(arg.value);
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
// Handle plain string args (backward compatibility)
|
|
1484
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
1485
|
+
commandParts.push(`"${arg}"`);
|
|
1486
|
+
} else {
|
|
1487
|
+
commandParts.push(arg);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const commandStr = commandParts.join(' ');
|
|
1492
|
+
|
|
1493
|
+
// Shell tracing for system commands
|
|
1494
|
+
if (globalShellSettings.xtrace) {
|
|
1495
|
+
console.log(`+ ${commandStr}`);
|
|
1496
|
+
}
|
|
1497
|
+
if (globalShellSettings.verbose) {
|
|
1498
|
+
console.log(commandStr);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Execute the system command with current input as stdin (ASYNC VERSION)
|
|
1502
|
+
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
1503
|
+
const require = createRequire(import.meta.url);
|
|
1504
|
+
const cp = require('child_process');
|
|
1505
|
+
|
|
1506
|
+
return new Promise((resolve, reject) => {
|
|
1507
|
+
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
1508
|
+
cwd: this.options.cwd,
|
|
1509
|
+
env: this.options.env,
|
|
1510
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
let stdout = '';
|
|
1514
|
+
let stderr = '';
|
|
1515
|
+
|
|
1516
|
+
proc.stdout.on('data', (chunk) => {
|
|
1517
|
+
stdout += chunk.toString();
|
|
1518
|
+
// If this is the last command, emit streaming data
|
|
1519
|
+
if (isLastCommand) {
|
|
1520
|
+
if (this.options.mirror) {
|
|
1521
|
+
process.stdout.write(chunk);
|
|
1522
|
+
}
|
|
1523
|
+
this.emit('stdout', chunk);
|
|
1524
|
+
this.emit('data', { type: 'stdout', data: chunk });
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
proc.stderr.on('data', (chunk) => {
|
|
1529
|
+
stderr += chunk.toString();
|
|
1530
|
+
// If this is the last command, emit streaming data
|
|
1531
|
+
if (isLastCommand) {
|
|
1532
|
+
if (this.options.mirror) {
|
|
1533
|
+
process.stderr.write(chunk);
|
|
1534
|
+
}
|
|
1535
|
+
this.emit('stderr', chunk);
|
|
1536
|
+
this.emit('data', { type: 'stderr', data: chunk });
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
proc.on('close', (code) => {
|
|
1541
|
+
resolve({
|
|
1542
|
+
status: code,
|
|
1543
|
+
stdout,
|
|
1544
|
+
stderr
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
proc.on('error', reject);
|
|
1549
|
+
|
|
1550
|
+
if (stdin) {
|
|
1551
|
+
proc.stdin.write(stdin);
|
|
1552
|
+
}
|
|
1553
|
+
proc.stdin.end();
|
|
1554
|
+
});
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
// Execute using shell to handle complex commands
|
|
1558
|
+
const argv = ['sh', '-c', commandStr];
|
|
1559
|
+
const isLastCommand = (i === commands.length - 1);
|
|
1560
|
+
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
1561
|
+
|
|
1562
|
+
let result = {
|
|
1563
|
+
code: proc.status || 0,
|
|
1564
|
+
stdout: proc.stdout || '',
|
|
1565
|
+
stderr: proc.stderr || '',
|
|
1566
|
+
stdin: currentInput
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
// If command failed and pipefail is set, fail the entire pipeline
|
|
1570
|
+
if (globalShellSettings.pipefail && result.code !== 0) {
|
|
1571
|
+
const error = new Error(`Pipeline command '${commandStr}' failed with exit code ${result.code}`);
|
|
1572
|
+
error.code = result.code;
|
|
1573
|
+
error.stdout = result.stdout;
|
|
1574
|
+
error.stderr = result.stderr;
|
|
1575
|
+
throw error;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// If this isn't the last command, pass stdout as stdin to next command
|
|
1579
|
+
if (i < commands.length - 1) {
|
|
1580
|
+
currentInput = result.stdout;
|
|
1581
|
+
// Accumulate stderr from all commands
|
|
1582
|
+
if (result.stderr && this.options.capture) {
|
|
1583
|
+
this.errChunks = this.errChunks || [];
|
|
1584
|
+
this.errChunks.push(Buffer.from(result.stderr));
|
|
1585
|
+
}
|
|
1586
|
+
} else {
|
|
1587
|
+
// This is the last command - store final result (streaming already handled during execution)
|
|
1588
|
+
currentOutput = result.stdout;
|
|
1589
|
+
|
|
1590
|
+
// Collect all accumulated stderr
|
|
1591
|
+
let allStderr = '';
|
|
1592
|
+
if (this.errChunks && this.errChunks.length > 0) {
|
|
1593
|
+
allStderr = Buffer.concat(this.errChunks).toString('utf8');
|
|
1594
|
+
}
|
|
1595
|
+
if (result.stderr) {
|
|
1596
|
+
allStderr += result.stderr;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Store final result using createResult helper for .text() method compatibility
|
|
1600
|
+
const finalResult = createResult({
|
|
1601
|
+
code: result.code,
|
|
1602
|
+
stdout: currentOutput,
|
|
1603
|
+
stderr: allStderr,
|
|
1604
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1605
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
this.result = finalResult;
|
|
1609
|
+
this.finished = true;
|
|
1610
|
+
|
|
1611
|
+
// Emit completion events
|
|
1612
|
+
this.emit('end', finalResult);
|
|
1613
|
+
this.emit('exit', finalResult.code);
|
|
1614
|
+
|
|
1615
|
+
// Handle shell settings
|
|
1616
|
+
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
1617
|
+
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
1618
|
+
error.code = finalResult.code;
|
|
1619
|
+
error.stdout = finalResult.stdout;
|
|
1620
|
+
error.stderr = finalResult.stderr;
|
|
1621
|
+
error.result = finalResult;
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
return finalResult;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
// Handle errors from system commands in pipeline
|
|
1630
|
+
const result = createResult({
|
|
1631
|
+
code: error.code ?? 1,
|
|
1632
|
+
stdout: currentOutput,
|
|
1633
|
+
stderr: error.stderr ?? error.message,
|
|
1634
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1635
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
this.result = result;
|
|
1639
|
+
this.finished = true;
|
|
1640
|
+
|
|
1641
|
+
if (result.stderr) {
|
|
1642
|
+
const buf = Buffer.from(result.stderr);
|
|
1643
|
+
if (this.options.mirror) {
|
|
1644
|
+
process.stderr.write(buf);
|
|
1645
|
+
}
|
|
1646
|
+
this.emit('stderr', buf);
|
|
1647
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
this.emit('end', result);
|
|
1651
|
+
this.emit('exit', result.code);
|
|
1652
|
+
|
|
1653
|
+
if (globalShellSettings.errexit) {
|
|
1654
|
+
throw error;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
return result;
|
|
679
1658
|
}
|
|
680
|
-
this.emit('stderr', buf);
|
|
681
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
682
|
-
|
|
683
|
-
this.emit('end', result);
|
|
684
|
-
this.emit('exit', result.code);
|
|
685
|
-
|
|
686
|
-
return result;
|
|
687
1659
|
}
|
|
688
1660
|
}
|
|
689
1661
|
}
|
|
690
1662
|
|
|
1663
|
+
async _runPipeline(commands) {
|
|
1664
|
+
traceFunc('ProcessRunner', '_runPipeline', 'ENTER', {
|
|
1665
|
+
commandsCount: commands.length
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
if (commands.length === 0) {
|
|
1669
|
+
traceBranch('ProcessRunner', '_runPipeline', 'NO_COMMANDS', {});
|
|
1670
|
+
return createResult({ code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' });
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
// For true streaming, we need to connect processes via pipes
|
|
1675
|
+
if (isBun) {
|
|
1676
|
+
traceBranch('ProcessRunner', '_runPipeline', 'BUN_STREAMING', {});
|
|
1677
|
+
return this._runStreamingPipelineBun(commands);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// For Node.js, fall back to non-streaming implementation for now
|
|
1681
|
+
traceBranch('ProcessRunner', '_runPipeline', 'NODE_NON_STREAMING', {});
|
|
1682
|
+
return this._runPipelineNonStreaming(commands);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
691
1685
|
// Run programmatic pipeline (.pipe() method)
|
|
692
1686
|
async _runProgrammaticPipeline(source, destination) {
|
|
1687
|
+
traceFunc('ProcessRunner', '_runProgrammaticPipeline', 'ENTER', {});
|
|
1688
|
+
|
|
693
1689
|
try {
|
|
694
1690
|
// Execute the source command first
|
|
1691
|
+
trace('ProcessRunner', 'Executing source command', {});
|
|
695
1692
|
const sourceResult = await source;
|
|
696
1693
|
|
|
697
1694
|
if (sourceResult.code !== 0) {
|
|
698
1695
|
// If source failed, return its result
|
|
1696
|
+
traceBranch('ProcessRunner', '_runProgrammaticPipeline', 'SOURCE_FAILED', {
|
|
1697
|
+
code: sourceResult.code,
|
|
1698
|
+
stderr: sourceResult.stderr
|
|
1699
|
+
});
|
|
699
1700
|
return sourceResult;
|
|
700
1701
|
}
|
|
701
1702
|
|
|
702
|
-
//
|
|
703
|
-
destination.
|
|
1703
|
+
// Create a new ProcessRunner with the correct stdin for the destination
|
|
1704
|
+
const destWithStdin = new ProcessRunner(destination.spec, {
|
|
704
1705
|
...destination.options,
|
|
705
1706
|
stdin: sourceResult.stdout
|
|
706
|
-
};
|
|
1707
|
+
});
|
|
707
1708
|
|
|
708
1709
|
// Execute the destination command
|
|
709
|
-
const destResult = await
|
|
1710
|
+
const destResult = await destWithStdin;
|
|
710
1711
|
|
|
1712
|
+
// Debug: Log what destResult looks like
|
|
1713
|
+
trace('ProcessRunner', 'destResult debug', {
|
|
1714
|
+
code: destResult.code,
|
|
1715
|
+
codeType: typeof destResult.code,
|
|
1716
|
+
hasCode: 'code' in destResult,
|
|
1717
|
+
keys: Object.keys(destResult),
|
|
1718
|
+
resultType: typeof destResult,
|
|
1719
|
+
fullResult: JSON.stringify(destResult, null, 2).slice(0, 200)
|
|
1720
|
+
});
|
|
1721
|
+
|
|
711
1722
|
// Return the final result with combined information
|
|
712
1723
|
return createResult({
|
|
713
1724
|
code: destResult.code,
|
|
@@ -744,13 +1755,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
744
1755
|
|
|
745
1756
|
// Async iteration support
|
|
746
1757
|
async* stream() {
|
|
1758
|
+
traceFunc('ProcessRunner', 'stream', 'ENTER', {
|
|
1759
|
+
started: this.started,
|
|
1760
|
+
finished: this.finished
|
|
1761
|
+
});
|
|
1762
|
+
|
|
747
1763
|
if (!this.started) {
|
|
748
|
-
|
|
1764
|
+
trace('ProcessRunner', 'Auto-starting async process from stream()', {});
|
|
1765
|
+
this._startAsync(); // Start but don't await
|
|
749
1766
|
}
|
|
750
1767
|
|
|
751
1768
|
let buffer = [];
|
|
752
1769
|
let resolve, reject;
|
|
753
1770
|
let ended = false;
|
|
1771
|
+
let cleanedUp = false;
|
|
754
1772
|
|
|
755
1773
|
const onData = (chunk) => {
|
|
756
1774
|
buffer.push(chunk);
|
|
@@ -783,15 +1801,94 @@ class ProcessRunner extends StreamEmitter {
|
|
|
783
1801
|
}
|
|
784
1802
|
}
|
|
785
1803
|
} finally {
|
|
1804
|
+
cleanedUp = true;
|
|
786
1805
|
this.off('data', onData);
|
|
787
1806
|
this.off('end', onEnd);
|
|
1807
|
+
|
|
1808
|
+
// Kill the process if it's still running when iteration is stopped
|
|
1809
|
+
// This happens when breaking from a for-await loop
|
|
1810
|
+
if (!this.finished) {
|
|
1811
|
+
this.kill();
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Kill the running process or cancel virtual command
|
|
1817
|
+
kill() {
|
|
1818
|
+
traceFunc('ProcessRunner', 'kill', 'ENTER', {
|
|
1819
|
+
cancelled: this._cancelled,
|
|
1820
|
+
finished: this.finished,
|
|
1821
|
+
hasChild: !!this.child,
|
|
1822
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
// Mark as cancelled for virtual commands
|
|
1826
|
+
this._cancelled = true;
|
|
1827
|
+
|
|
1828
|
+
// Resolve the cancel promise to break the race in virtual command execution
|
|
1829
|
+
if (this._cancelResolve) {
|
|
1830
|
+
trace('ProcessRunner', 'Resolving cancel promise', {});
|
|
1831
|
+
this._cancelResolve();
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Abort any async operations
|
|
1835
|
+
if (this._abortController) {
|
|
1836
|
+
trace('ProcessRunner', 'Aborting controller', {});
|
|
1837
|
+
this._abortController.abort();
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// If it's a virtual generator, try to close it
|
|
1841
|
+
if (this._virtualGenerator && this._virtualGenerator.return) {
|
|
1842
|
+
trace('ProcessRunner', 'Closing virtual generator', {});
|
|
1843
|
+
try {
|
|
1844
|
+
this._virtualGenerator.return();
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
trace('ProcessRunner', 'Error closing generator', { error: err.message });
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// Kill child process if it exists
|
|
1851
|
+
if (this.child && !this.finished) {
|
|
1852
|
+
traceBranch('ProcessRunner', 'hasChild', 'killing', { pid: this.child.pid });
|
|
1853
|
+
try {
|
|
1854
|
+
// Kill the process group to ensure all child processes are terminated
|
|
1855
|
+
if (this.child.pid) {
|
|
1856
|
+
if (isBun) {
|
|
1857
|
+
trace('ProcessRunner', 'Killing Bun process', { pid: this.child.pid });
|
|
1858
|
+
this.child.kill();
|
|
1859
|
+
} else {
|
|
1860
|
+
// In Node.js, kill the process group
|
|
1861
|
+
trace('ProcessRunner', 'Killing Node process group', { pid: this.child.pid });
|
|
1862
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
this.finished = true;
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
// Process might already be dead
|
|
1868
|
+
trace('ProcessRunner', 'Error killing process', { error: err.message });
|
|
1869
|
+
console.error('Error killing process:', err.message);
|
|
1870
|
+
}
|
|
788
1871
|
}
|
|
1872
|
+
|
|
1873
|
+
// Mark as finished
|
|
1874
|
+
this.finished = true;
|
|
1875
|
+
|
|
1876
|
+
traceFunc('ProcessRunner', 'kill', 'EXIT', {
|
|
1877
|
+
cancelled: this._cancelled,
|
|
1878
|
+
finished: this.finished
|
|
1879
|
+
});
|
|
789
1880
|
}
|
|
790
1881
|
|
|
791
1882
|
// Programmatic piping support
|
|
792
1883
|
pipe(destination) {
|
|
1884
|
+
traceFunc('ProcessRunner', 'pipe', 'ENTER', {
|
|
1885
|
+
hasDestination: !!destination,
|
|
1886
|
+
destinationType: destination?.constructor?.name
|
|
1887
|
+
});
|
|
1888
|
+
|
|
793
1889
|
// If destination is a ProcessRunner, create a pipeline
|
|
794
1890
|
if (destination instanceof ProcessRunner) {
|
|
1891
|
+
traceBranch('ProcessRunner', 'pipe', 'PROCESS_RUNNER_DEST', {});
|
|
795
1892
|
// Create a new ProcessRunner that represents the piped operation
|
|
796
1893
|
const pipeSpec = {
|
|
797
1894
|
mode: 'pipeline',
|
|
@@ -799,49 +1896,64 @@ class ProcessRunner extends StreamEmitter {
|
|
|
799
1896
|
destination: destination
|
|
800
1897
|
};
|
|
801
1898
|
|
|
802
|
-
|
|
1899
|
+
const pipeRunner = new ProcessRunner(pipeSpec, {
|
|
803
1900
|
...this.options,
|
|
804
1901
|
capture: destination.options.capture ?? true
|
|
805
1902
|
});
|
|
1903
|
+
|
|
1904
|
+
traceFunc('ProcessRunner', 'pipe', 'EXIT', { mode: 'pipeline' });
|
|
1905
|
+
return pipeRunner;
|
|
806
1906
|
}
|
|
807
1907
|
|
|
808
1908
|
// If destination is a template literal result (from $`command`), use its spec
|
|
809
1909
|
if (destination && destination.spec) {
|
|
1910
|
+
traceBranch('ProcessRunner', 'pipe', 'TEMPLATE_LITERAL_DEST', {});
|
|
810
1911
|
const destRunner = new ProcessRunner(destination.spec, destination.options);
|
|
811
1912
|
return this.pipe(destRunner);
|
|
812
1913
|
}
|
|
813
1914
|
|
|
1915
|
+
traceBranch('ProcessRunner', 'pipe', 'INVALID_DEST', {});
|
|
814
1916
|
throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
|
|
815
1917
|
}
|
|
816
1918
|
|
|
817
1919
|
// Promise interface (for await)
|
|
818
1920
|
then(onFulfilled, onRejected) {
|
|
819
1921
|
if (!this.promise) {
|
|
820
|
-
this.promise = this.
|
|
1922
|
+
this.promise = this._startAsync();
|
|
821
1923
|
}
|
|
822
1924
|
return this.promise.then(onFulfilled, onRejected);
|
|
823
1925
|
}
|
|
824
1926
|
|
|
825
1927
|
catch(onRejected) {
|
|
826
1928
|
if (!this.promise) {
|
|
827
|
-
this.promise = this.
|
|
1929
|
+
this.promise = this._startAsync();
|
|
828
1930
|
}
|
|
829
1931
|
return this.promise.catch(onRejected);
|
|
830
1932
|
}
|
|
831
1933
|
|
|
832
1934
|
finally(onFinally) {
|
|
833
1935
|
if (!this.promise) {
|
|
834
|
-
this.promise = this.
|
|
1936
|
+
this.promise = this._startAsync();
|
|
835
1937
|
}
|
|
836
1938
|
return this.promise.finally(onFinally);
|
|
837
1939
|
}
|
|
838
1940
|
|
|
839
|
-
//
|
|
840
|
-
|
|
1941
|
+
// Internal sync execution
|
|
1942
|
+
_startSync() {
|
|
1943
|
+
traceFunc('ProcessRunner', '_startSync', 'ENTER', {
|
|
1944
|
+
started: this.started,
|
|
1945
|
+
spec: this.spec
|
|
1946
|
+
});
|
|
1947
|
+
|
|
841
1948
|
if (this.started) {
|
|
1949
|
+
traceBranch('ProcessRunner', '_startSync', 'ALREADY_STARTED', {});
|
|
842
1950
|
throw new Error('Command already started - cannot run sync after async start');
|
|
843
1951
|
}
|
|
844
1952
|
|
|
1953
|
+
this.started = true;
|
|
1954
|
+
this._mode = 'sync';
|
|
1955
|
+
trace('ProcessRunner', 'Starting sync execution', { mode: this._mode });
|
|
1956
|
+
|
|
845
1957
|
const { cwd, env, stdin } = this.options;
|
|
846
1958
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
847
1959
|
|
|
@@ -960,34 +2072,79 @@ class ProcessRunner extends StreamEmitter {
|
|
|
960
2072
|
|
|
961
2073
|
// Public APIs
|
|
962
2074
|
async function sh(commandString, options = {}) {
|
|
2075
|
+
traceFunc('API', 'sh', 'ENTER', {
|
|
2076
|
+
command: commandString,
|
|
2077
|
+
options
|
|
2078
|
+
});
|
|
2079
|
+
|
|
963
2080
|
const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
|
|
964
|
-
|
|
2081
|
+
const result = await runner._startAsync();
|
|
2082
|
+
|
|
2083
|
+
traceFunc('API', 'sh', 'EXIT', { code: result.code });
|
|
2084
|
+
return result;
|
|
965
2085
|
}
|
|
966
2086
|
|
|
967
2087
|
async function exec(file, args = [], options = {}) {
|
|
2088
|
+
traceFunc('API', 'exec', 'ENTER', {
|
|
2089
|
+
file,
|
|
2090
|
+
argsCount: args.length,
|
|
2091
|
+
options
|
|
2092
|
+
});
|
|
2093
|
+
|
|
968
2094
|
const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
|
|
969
|
-
|
|
2095
|
+
const result = await runner._startAsync();
|
|
2096
|
+
|
|
2097
|
+
traceFunc('API', 'exec', 'EXIT', { code: result.code });
|
|
2098
|
+
return result;
|
|
970
2099
|
}
|
|
971
2100
|
|
|
972
2101
|
async function run(commandOrTokens, options = {}) {
|
|
2102
|
+
traceFunc('API', 'run', 'ENTER', {
|
|
2103
|
+
type: typeof commandOrTokens,
|
|
2104
|
+
options
|
|
2105
|
+
});
|
|
2106
|
+
|
|
973
2107
|
if (typeof commandOrTokens === 'string') {
|
|
2108
|
+
traceBranch('API', 'run', 'STRING_COMMAND', { command: commandOrTokens });
|
|
974
2109
|
return sh(commandOrTokens, { ...options, mirror: false, capture: true });
|
|
975
2110
|
}
|
|
2111
|
+
|
|
976
2112
|
const [file, ...args] = commandOrTokens;
|
|
2113
|
+
traceBranch('API', 'run', 'TOKEN_ARRAY', { file, argsCount: args.length });
|
|
977
2114
|
return exec(file, args, { ...options, mirror: false, capture: true });
|
|
978
2115
|
}
|
|
979
2116
|
|
|
980
2117
|
// Enhanced tagged template that returns ProcessRunner
|
|
981
2118
|
function $tagged(strings, ...values) {
|
|
2119
|
+
traceFunc('API', '$tagged', 'ENTER', {
|
|
2120
|
+
stringsLength: strings.length,
|
|
2121
|
+
valuesLength: values.length
|
|
2122
|
+
});
|
|
2123
|
+
|
|
982
2124
|
const cmd = buildShellCommand(strings, values);
|
|
983
|
-
|
|
2125
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
|
|
2126
|
+
|
|
2127
|
+
traceFunc('API', '$tagged', 'EXIT', { command: cmd });
|
|
2128
|
+
return runner;
|
|
984
2129
|
}
|
|
985
2130
|
|
|
986
2131
|
function create(defaultOptions = {}) {
|
|
2132
|
+
traceFunc('API', 'create', 'ENTER', { defaultOptions });
|
|
2133
|
+
|
|
987
2134
|
const tagged = (strings, ...values) => {
|
|
2135
|
+
traceFunc('API', 'create.tagged', 'ENTER', {
|
|
2136
|
+
stringsLength: strings.length,
|
|
2137
|
+
valuesLength: values.length
|
|
2138
|
+
});
|
|
2139
|
+
|
|
988
2140
|
const cmd = buildShellCommand(strings, values);
|
|
989
|
-
|
|
2141
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
|
|
2142
|
+
|
|
2143
|
+
traceFunc('API', 'create.tagged', 'EXIT', { command: cmd });
|
|
2144
|
+
return runner;
|
|
990
2145
|
};
|
|
2146
|
+
|
|
2147
|
+
traceFunc('API', 'create', 'EXIT', {});
|
|
991
2148
|
return tagged;
|
|
992
2149
|
}
|
|
993
2150
|
|
|
@@ -1058,12 +2215,21 @@ const shell = {
|
|
|
1058
2215
|
|
|
1059
2216
|
// Virtual command registration API
|
|
1060
2217
|
function register(name, handler) {
|
|
2218
|
+
traceFunc('VirtualCommands', 'register', 'ENTER', { name });
|
|
2219
|
+
|
|
1061
2220
|
virtualCommands.set(name, handler);
|
|
2221
|
+
|
|
2222
|
+
traceFunc('VirtualCommands', 'register', 'EXIT', { registered: true });
|
|
1062
2223
|
return virtualCommands;
|
|
1063
2224
|
}
|
|
1064
2225
|
|
|
1065
2226
|
function unregister(name) {
|
|
1066
|
-
|
|
2227
|
+
traceFunc('VirtualCommands', 'unregister', 'ENTER', { name });
|
|
2228
|
+
|
|
2229
|
+
const deleted = virtualCommands.delete(name);
|
|
2230
|
+
|
|
2231
|
+
traceFunc('VirtualCommands', 'unregister', 'EXIT', { deleted });
|
|
2232
|
+
return deleted;
|
|
1067
2233
|
}
|
|
1068
2234
|
|
|
1069
2235
|
function listCommands() {
|
|
@@ -1085,10 +2251,15 @@ function registerBuiltins() {
|
|
|
1085
2251
|
// cd - change directory
|
|
1086
2252
|
register('cd', async (args) => {
|
|
1087
2253
|
const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
|
|
2254
|
+
trace('VirtualCommand', 'cd: changing directory', { target });
|
|
2255
|
+
|
|
1088
2256
|
try {
|
|
1089
2257
|
process.chdir(target);
|
|
1090
|
-
|
|
2258
|
+
const newDir = process.cwd();
|
|
2259
|
+
trace('VirtualCommand', 'cd: success', { newDir });
|
|
2260
|
+
return { stdout: newDir, code: 0 };
|
|
1091
2261
|
} catch (error) {
|
|
2262
|
+
trace('VirtualCommand', 'cd: failed', { error: error.message });
|
|
1092
2263
|
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
1093
2264
|
}
|
|
1094
2265
|
});
|
|
@@ -1097,14 +2268,18 @@ function registerBuiltins() {
|
|
|
1097
2268
|
register('pwd', async (args, stdin, options) => {
|
|
1098
2269
|
// If cwd option is provided, return that instead of process.cwd()
|
|
1099
2270
|
const dir = options?.cwd || process.cwd();
|
|
2271
|
+
trace('VirtualCommand', 'pwd: getting directory', { dir });
|
|
1100
2272
|
return { stdout: dir, code: 0 };
|
|
1101
2273
|
});
|
|
1102
2274
|
|
|
1103
2275
|
// echo - print arguments
|
|
1104
2276
|
register('echo', async (args) => {
|
|
2277
|
+
trace('VirtualCommand', 'echo: processing', { argsCount: args.length });
|
|
2278
|
+
|
|
1105
2279
|
let output = args.join(' ');
|
|
1106
2280
|
if (args.includes('-n')) {
|
|
1107
2281
|
// Don't add newline
|
|
2282
|
+
traceBranch('VirtualCommand', 'echo', 'NO_NEWLINE', {});
|
|
1108
2283
|
output = args.filter(arg => arg !== '-n').join(' ');
|
|
1109
2284
|
} else {
|
|
1110
2285
|
output += '\n';
|
|
@@ -1115,10 +2290,15 @@ function registerBuiltins() {
|
|
|
1115
2290
|
// sleep - wait for specified time
|
|
1116
2291
|
register('sleep', async (args) => {
|
|
1117
2292
|
const seconds = parseFloat(args[0] || 0);
|
|
2293
|
+
trace('VirtualCommand', 'sleep: starting', { seconds });
|
|
2294
|
+
|
|
1118
2295
|
if (isNaN(seconds) || seconds < 0) {
|
|
2296
|
+
trace('VirtualCommand', 'sleep: invalid interval', { input: args[0] });
|
|
1119
2297
|
return { stderr: 'sleep: invalid time interval', code: 1 };
|
|
1120
2298
|
}
|
|
2299
|
+
|
|
1121
2300
|
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
2301
|
+
trace('VirtualCommand', 'sleep: completed', { seconds });
|
|
1122
2302
|
return { stdout: '', code: 0 };
|
|
1123
2303
|
});
|
|
1124
2304
|
|
|
@@ -1649,14 +2829,49 @@ function registerBuiltins() {
|
|
|
1649
2829
|
});
|
|
1650
2830
|
|
|
1651
2831
|
// yes - output a string repeatedly
|
|
1652
|
-
register('yes', async function* (args) {
|
|
2832
|
+
register('yes', async function* (args, stdin, options) {
|
|
1653
2833
|
const output = args.length > 0 ? args.join(' ') : 'y';
|
|
2834
|
+
trace('VirtualCommand', 'yes: starting infinite generator', { output });
|
|
1654
2835
|
|
|
1655
2836
|
// Generate infinite stream of the output
|
|
1656
2837
|
while (true) {
|
|
2838
|
+
// Check if cancelled via function or abort signal
|
|
2839
|
+
if (options) {
|
|
2840
|
+
if (options.isCancelled && options.isCancelled()) {
|
|
2841
|
+
trace('VirtualCommand', 'yes: cancelled via function', {});
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
if (options.signal && options.signal.aborted) {
|
|
2845
|
+
trace('VirtualCommand', 'yes: cancelled via abort signal', {});
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
1657
2850
|
yield output + '\n';
|
|
1658
|
-
|
|
1659
|
-
|
|
2851
|
+
|
|
2852
|
+
// Small delay with abort signal support
|
|
2853
|
+
try {
|
|
2854
|
+
await new Promise((resolve, reject) => {
|
|
2855
|
+
const timeout = setTimeout(resolve, 0);
|
|
2856
|
+
|
|
2857
|
+
// Listen for abort signal if available
|
|
2858
|
+
if (options && options.signal) {
|
|
2859
|
+
const abortHandler = () => {
|
|
2860
|
+
clearTimeout(timeout);
|
|
2861
|
+
reject(new Error('Aborted'));
|
|
2862
|
+
};
|
|
2863
|
+
|
|
2864
|
+
if (options.signal.aborted) {
|
|
2865
|
+
abortHandler();
|
|
2866
|
+
} else {
|
|
2867
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
// Aborted
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
1660
2875
|
}
|
|
1661
2876
|
});
|
|
1662
2877
|
|