command-stream 0.0.5 → 0.2.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 +1522 -213
- package/README.md +44 -3
- package/package.json +1 -1
package/$.mjs
CHANGED
|
@@ -10,6 +10,101 @@ 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
|
+
|
|
39
|
+
// Track parent stream state for graceful shutdown
|
|
40
|
+
let parentStreamsMonitored = false;
|
|
41
|
+
const activeProcessRunners = new Set();
|
|
42
|
+
|
|
43
|
+
function monitorParentStreams() {
|
|
44
|
+
if (parentStreamsMonitored) return;
|
|
45
|
+
parentStreamsMonitored = true;
|
|
46
|
+
|
|
47
|
+
// Monitor parent stdout/stderr for closure
|
|
48
|
+
const checkParentStream = (stream, name) => {
|
|
49
|
+
if (stream && typeof stream.on === 'function') {
|
|
50
|
+
stream.on('close', () => {
|
|
51
|
+
trace('ProcessRunner', `Parent ${name} closed - triggering graceful shutdown`, {
|
|
52
|
+
activeProcesses: activeProcessRunners.size
|
|
53
|
+
});
|
|
54
|
+
// Signal all active ProcessRunners to gracefully shutdown
|
|
55
|
+
for (const runner of activeProcessRunners) {
|
|
56
|
+
runner._handleParentStreamClosure();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
checkParentStream(process.stdout, 'stdout');
|
|
63
|
+
checkParentStream(process.stderr, 'stderr');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Safe write function that checks stream state and handles parent closure
|
|
67
|
+
function safeWrite(stream, data, processRunner = null) {
|
|
68
|
+
// Ensure parent stream monitoring is active
|
|
69
|
+
monitorParentStreams();
|
|
70
|
+
|
|
71
|
+
// Check if stream is writable and not destroyed/closed
|
|
72
|
+
if (!stream || !stream.writable || stream.destroyed || stream.closed) {
|
|
73
|
+
trace('ProcessRunner', 'safeWrite skipped - stream not writable', {
|
|
74
|
+
hasStream: !!stream,
|
|
75
|
+
writable: stream?.writable,
|
|
76
|
+
destroyed: stream?.destroyed,
|
|
77
|
+
closed: stream?.closed
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// If this is a parent stream closure, signal graceful shutdown
|
|
81
|
+
if (processRunner && (stream === process.stdout || stream === process.stderr)) {
|
|
82
|
+
processRunner._handleParentStreamClosure();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return stream.write(data);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
trace('ProcessRunner', 'safeWrite error', {
|
|
92
|
+
error: error.message,
|
|
93
|
+
code: error.code,
|
|
94
|
+
writable: stream.writable,
|
|
95
|
+
destroyed: stream.destroyed
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// If this is an EPIPE on parent streams, signal graceful shutdown
|
|
99
|
+
if (error.code === 'EPIPE' && processRunner &&
|
|
100
|
+
(stream === process.stdout || stream === process.stderr)) {
|
|
101
|
+
processRunner._handleParentStreamClosure();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
13
108
|
// Global shell settings (like bash set -e / set +e)
|
|
14
109
|
let globalShellSettings = {
|
|
15
110
|
errexit: false, // set -e equivalent: exit on error
|
|
@@ -50,6 +145,9 @@ class StreamEmitter {
|
|
|
50
145
|
this.listeners.set(event, []);
|
|
51
146
|
}
|
|
52
147
|
this.listeners.get(event).push(listener);
|
|
148
|
+
|
|
149
|
+
// No auto-start - explicit start() or await will start the process
|
|
150
|
+
|
|
53
151
|
return this;
|
|
54
152
|
}
|
|
55
153
|
|
|
@@ -84,18 +182,28 @@ function quote(value) {
|
|
|
84
182
|
}
|
|
85
183
|
|
|
86
184
|
function buildShellCommand(strings, values) {
|
|
185
|
+
traceFunc('Utils', 'buildShellCommand', 'ENTER', {
|
|
186
|
+
stringsLength: strings.length,
|
|
187
|
+
valuesLength: values.length
|
|
188
|
+
});
|
|
189
|
+
|
|
87
190
|
let out = '';
|
|
88
191
|
for (let i = 0; i < strings.length; i++) {
|
|
89
192
|
out += strings[i];
|
|
90
193
|
if (i < values.length) {
|
|
91
194
|
const v = values[i];
|
|
92
195
|
if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
|
|
196
|
+
traceBranch('Utils', 'buildShellCommand', 'RAW_VALUE', { value: String(v.raw) });
|
|
93
197
|
out += String(v.raw);
|
|
94
198
|
} else {
|
|
95
|
-
|
|
199
|
+
const quoted = quote(v);
|
|
200
|
+
traceBranch('Utils', 'buildShellCommand', 'QUOTED_VALUE', { original: v, quoted });
|
|
201
|
+
out += quoted;
|
|
96
202
|
}
|
|
97
203
|
}
|
|
98
204
|
}
|
|
205
|
+
|
|
206
|
+
traceFunc('Utils', 'buildShellCommand', 'EXIT', { command: out });
|
|
99
207
|
return out;
|
|
100
208
|
}
|
|
101
209
|
|
|
@@ -116,6 +224,12 @@ async function pumpReadable(readable, onChunk) {
|
|
|
116
224
|
class ProcessRunner extends StreamEmitter {
|
|
117
225
|
constructor(spec, options = {}) {
|
|
118
226
|
super();
|
|
227
|
+
|
|
228
|
+
traceFunc('ProcessRunner', 'constructor', 'ENTER', {
|
|
229
|
+
spec: typeof spec === 'object' ? { ...spec, command: spec.command?.slice(0, 100) } : spec,
|
|
230
|
+
options
|
|
231
|
+
});
|
|
232
|
+
|
|
119
233
|
this.spec = spec;
|
|
120
234
|
this.options = {
|
|
121
235
|
mirror: true,
|
|
@@ -139,27 +253,165 @@ class ProcessRunner extends StreamEmitter {
|
|
|
139
253
|
|
|
140
254
|
// Promise for awaiting final result
|
|
141
255
|
this.promise = null;
|
|
256
|
+
|
|
257
|
+
// Track the execution mode
|
|
258
|
+
this._mode = null; // 'async' or 'sync'
|
|
259
|
+
|
|
260
|
+
// Cancellation support for virtual commands
|
|
261
|
+
this._cancelled = false;
|
|
262
|
+
this._virtualGenerator = null;
|
|
263
|
+
this._abortController = new AbortController();
|
|
264
|
+
|
|
265
|
+
// Register this ProcessRunner for parent stream monitoring
|
|
266
|
+
activeProcessRunners.add(this);
|
|
267
|
+
|
|
268
|
+
// Track finished state changes to trigger cleanup
|
|
269
|
+
this._finished = false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Override finished property to trigger cleanup when set to true
|
|
273
|
+
get finished() {
|
|
274
|
+
return this._finished;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
set finished(value) {
|
|
278
|
+
if (value === true && this._finished === false) {
|
|
279
|
+
this._finished = true;
|
|
280
|
+
this._cleanup(); // Trigger cleanup when process finishes
|
|
281
|
+
} else {
|
|
282
|
+
this._finished = value;
|
|
283
|
+
}
|
|
142
284
|
}
|
|
143
285
|
|
|
144
|
-
|
|
145
|
-
|
|
286
|
+
// Handle parent stream closure by gracefully shutting down child processes
|
|
287
|
+
_handleParentStreamClosure() {
|
|
288
|
+
if (this.finished || this._cancelled) return;
|
|
289
|
+
|
|
290
|
+
trace('ProcessRunner', 'Handling parent stream closure', {
|
|
291
|
+
started: this.started,
|
|
292
|
+
hasChild: !!this.child,
|
|
293
|
+
command: this.spec.command?.slice(0, 50) || this.spec.file
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Mark as cancelled to prevent further operations
|
|
297
|
+
this._cancelled = true;
|
|
298
|
+
|
|
299
|
+
// Cancel abort controller for virtual commands
|
|
300
|
+
if (this._abortController) {
|
|
301
|
+
this._abortController.abort();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Gracefully close child process if it exists
|
|
305
|
+
if (this.child) {
|
|
306
|
+
try {
|
|
307
|
+
// Close stdin first to signal completion
|
|
308
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') {
|
|
309
|
+
this.child.stdin.end();
|
|
310
|
+
} else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
|
|
311
|
+
const writer = this.child.stdin.getWriter();
|
|
312
|
+
writer.close().catch(() => {}); // Ignore close errors
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Give the process a moment to exit gracefully, then terminate
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
if (this.child && !this.finished) {
|
|
318
|
+
trace('ProcessRunner', 'Terminating child process after parent stream closure', {});
|
|
319
|
+
if (typeof this.child.kill === 'function') {
|
|
320
|
+
this.child.kill('SIGTERM');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}, 100);
|
|
324
|
+
|
|
325
|
+
} catch (error) {
|
|
326
|
+
trace('ProcessRunner', 'Error during graceful shutdown', { error: error.message });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Remove from active set
|
|
331
|
+
activeProcessRunners.delete(this);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Cleanup method to remove from active set when process completes normally
|
|
335
|
+
_cleanup() {
|
|
336
|
+
activeProcessRunners.delete(this);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Unified start method that can work in both async and sync modes
|
|
340
|
+
start(options = {}) {
|
|
341
|
+
const mode = options.mode || 'async';
|
|
342
|
+
|
|
343
|
+
traceFunc('ProcessRunner', 'start', 'ENTER', { mode, options });
|
|
344
|
+
|
|
345
|
+
if (mode === 'sync') {
|
|
346
|
+
traceBranch('ProcessRunner', 'mode', 'sync', {});
|
|
347
|
+
return this._startSync();
|
|
348
|
+
} else {
|
|
349
|
+
traceBranch('ProcessRunner', 'mode', 'async', {});
|
|
350
|
+
return this._startAsync();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Shortcut for sync mode
|
|
355
|
+
sync() {
|
|
356
|
+
return this.start({ mode: 'sync' });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Shortcut for async mode
|
|
360
|
+
async() {
|
|
361
|
+
return this.start({ mode: 'async' });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async _startAsync() {
|
|
365
|
+
if (this.started) return this.promise;
|
|
366
|
+
if (this.promise) return this.promise;
|
|
367
|
+
|
|
368
|
+
this.promise = this._doStartAsync();
|
|
369
|
+
return this.promise;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async _doStartAsync() {
|
|
373
|
+
traceFunc('ProcessRunner', '_doStartAsync', 'ENTER', {
|
|
374
|
+
mode: this.spec.mode,
|
|
375
|
+
command: this.spec.command?.slice(0, 100)
|
|
376
|
+
});
|
|
377
|
+
|
|
146
378
|
this.started = true;
|
|
379
|
+
this._mode = 'async';
|
|
147
380
|
|
|
148
381
|
const { cwd, env, stdin } = this.options;
|
|
149
382
|
|
|
150
383
|
// Handle programmatic pipeline mode
|
|
151
384
|
if (this.spec.mode === 'pipeline') {
|
|
385
|
+
traceBranch('ProcessRunner', 'spec.mode', 'pipeline', {
|
|
386
|
+
hasSource: !!this.spec.source,
|
|
387
|
+
hasDestination: !!this.spec.destination
|
|
388
|
+
});
|
|
152
389
|
return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
|
|
153
390
|
}
|
|
154
391
|
|
|
155
392
|
// Check if this is a virtual command first
|
|
156
393
|
if (this.spec.mode === 'shell') {
|
|
394
|
+
traceBranch('ProcessRunner', 'spec.mode', 'shell', {});
|
|
395
|
+
|
|
157
396
|
// Parse the command to check for virtual commands or pipelines
|
|
158
397
|
const parsed = this._parseCommand(this.spec.command);
|
|
398
|
+
trace('ProcessRunner', 'Parsed command', {
|
|
399
|
+
type: parsed?.type,
|
|
400
|
+
cmd: parsed?.cmd,
|
|
401
|
+
argsCount: parsed?.args?.length
|
|
402
|
+
});
|
|
403
|
+
|
|
159
404
|
if (parsed) {
|
|
160
405
|
if (parsed.type === 'pipeline') {
|
|
406
|
+
traceBranch('ProcessRunner', 'parsed.type', 'pipeline', {
|
|
407
|
+
commandCount: parsed.commands?.length
|
|
408
|
+
});
|
|
161
409
|
return await this._runPipeline(parsed.commands);
|
|
162
410
|
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
411
|
+
traceBranch('ProcessRunner', 'virtualCommand', parsed.cmd, {
|
|
412
|
+
isVirtual: true,
|
|
413
|
+
args: parsed.args
|
|
414
|
+
});
|
|
163
415
|
return await this._runVirtual(parsed.cmd, parsed.args);
|
|
164
416
|
}
|
|
165
417
|
}
|
|
@@ -194,7 +446,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
194
446
|
// Setup stdout streaming
|
|
195
447
|
const outPump = pumpReadable(this.child.stdout, async (buf) => {
|
|
196
448
|
if (this.options.capture) this.outChunks.push(buf);
|
|
197
|
-
if (this.options.mirror) process.stdout
|
|
449
|
+
if (this.options.mirror) safeWrite(process.stdout, buf);
|
|
198
450
|
|
|
199
451
|
// Emit chunk events
|
|
200
452
|
this.emit('stdout', buf);
|
|
@@ -204,7 +456,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
204
456
|
// Setup stderr streaming
|
|
205
457
|
const errPump = pumpReadable(this.child.stderr, async (buf) => {
|
|
206
458
|
if (this.options.capture) this.errChunks.push(buf);
|
|
207
|
-
if (this.options.mirror) process.stderr
|
|
459
|
+
if (this.options.mirror) safeWrite(process.stderr, buf);
|
|
208
460
|
|
|
209
461
|
// Emit chunk events
|
|
210
462
|
this.emit('stderr', buf);
|
|
@@ -236,8 +488,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
236
488
|
const code = await exited;
|
|
237
489
|
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
238
490
|
|
|
239
|
-
|
|
491
|
+
// Debug: Check the raw exit code
|
|
492
|
+
trace('ProcessRunner', 'Raw exit code from child', {
|
|
240
493
|
code,
|
|
494
|
+
codeType: typeof code,
|
|
495
|
+
childExitCode: this.child?.exitCode,
|
|
496
|
+
isBun
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const resultData = {
|
|
500
|
+
code: code ?? 0, // Default to 0 if exit code is null/undefined
|
|
241
501
|
stdout: this.options.capture ? Buffer.concat(this.outChunks).toString('utf8') : undefined,
|
|
242
502
|
stderr: this.options.capture ? Buffer.concat(this.errChunks).toString('utf8') : undefined,
|
|
243
503
|
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
@@ -276,7 +536,27 @@ class ProcessRunner extends StreamEmitter {
|
|
|
276
536
|
const buf = asBuffer(chunk);
|
|
277
537
|
captureChunks && captureChunks.push(buf);
|
|
278
538
|
if (bunWriter) await bunWriter.write(buf);
|
|
279
|
-
else if (typeof child.stdin.write === 'function')
|
|
539
|
+
else if (typeof child.stdin.write === 'function') {
|
|
540
|
+
// Add error handler to prevent unhandled error events
|
|
541
|
+
if (child.stdin && typeof child.stdin.on === 'function') {
|
|
542
|
+
child.stdin.on('error', (error) => {
|
|
543
|
+
if (error.code !== 'EPIPE') {
|
|
544
|
+
trace('ProcessRunner', 'child stdin buffer error event', { error: error.message, code: error.code });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Safe write to handle EPIPE errors
|
|
550
|
+
if (child.stdin && child.stdin.writable && !child.stdin.destroyed && !child.stdin.closed) {
|
|
551
|
+
try {
|
|
552
|
+
child.stdin.write(buf);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error.code !== 'EPIPE') {
|
|
555
|
+
trace('ProcessRunner', 'Error writing stdin buffer', { error: error.message, code: error.code });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
280
560
|
else if (isBun && typeof Bun.write === 'function') await Bun.write(child.stdin, buf);
|
|
281
561
|
}
|
|
282
562
|
if (bunWriter) await bunWriter.close();
|
|
@@ -287,8 +567,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
287
567
|
if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
|
|
288
568
|
const w = this.child.stdin.getWriter();
|
|
289
569
|
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
290
|
-
|
|
291
|
-
|
|
570
|
+
try {
|
|
571
|
+
await w.write(bytes);
|
|
572
|
+
await w.close();
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error.code !== 'EPIPE') {
|
|
575
|
+
trace('ProcessRunner', 'Error writing to Bun writer', { error: error.message, code: error.code });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
292
578
|
} else if (this.child.stdin && typeof this.child.stdin.write === 'function') {
|
|
293
579
|
this.child.stdin.end(buf);
|
|
294
580
|
} else if (isBun && typeof Bun.write === 'function') {
|
|
@@ -375,11 +661,19 @@ class ProcessRunner extends StreamEmitter {
|
|
|
375
661
|
}
|
|
376
662
|
|
|
377
663
|
async _runVirtual(cmd, args) {
|
|
664
|
+
traceFunc('ProcessRunner', '_runVirtual', 'ENTER', { cmd, args });
|
|
665
|
+
|
|
378
666
|
const handler = virtualCommands.get(cmd);
|
|
379
667
|
if (!handler) {
|
|
668
|
+
trace('ProcessRunner', 'Virtual command not found', { cmd });
|
|
380
669
|
throw new Error(`Virtual command not found: ${cmd}`);
|
|
381
670
|
}
|
|
382
671
|
|
|
672
|
+
trace('ProcessRunner', 'Found virtual command handler', {
|
|
673
|
+
cmd,
|
|
674
|
+
isGenerator: handler.constructor.name === 'AsyncGeneratorFunction'
|
|
675
|
+
});
|
|
676
|
+
|
|
383
677
|
try {
|
|
384
678
|
// Prepare stdin
|
|
385
679
|
let stdinData = '';
|
|
@@ -405,115 +699,820 @@ class ProcessRunner extends StreamEmitter {
|
|
|
405
699
|
|
|
406
700
|
// Check if handler is async generator (streaming)
|
|
407
701
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
408
|
-
// Handle streaming virtual command
|
|
702
|
+
// Handle streaming virtual command with cancellation support
|
|
409
703
|
const chunks = [];
|
|
410
|
-
|
|
704
|
+
|
|
705
|
+
// Create options with cancellation check and abort signal
|
|
706
|
+
const commandOptions = {
|
|
707
|
+
...this.options,
|
|
708
|
+
isCancelled: () => this._cancelled,
|
|
709
|
+
signal: this._abortController.signal
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const generator = handler({ args: argValues, stdin: stdinData, ...commandOptions });
|
|
713
|
+
this._virtualGenerator = generator;
|
|
714
|
+
|
|
715
|
+
// Create a promise that resolves when cancelled
|
|
716
|
+
const cancelPromise = new Promise(resolve => {
|
|
717
|
+
this._cancelResolve = resolve;
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
const iterator = generator[Symbol.asyncIterator]();
|
|
722
|
+
let done = false;
|
|
723
|
+
|
|
724
|
+
while (!done && !this._cancelled) {
|
|
725
|
+
// Race between getting next value and cancellation
|
|
726
|
+
const result = await Promise.race([
|
|
727
|
+
iterator.next(),
|
|
728
|
+
cancelPromise.then(() => ({ done: true, cancelled: true }))
|
|
729
|
+
]);
|
|
730
|
+
|
|
731
|
+
if (result.cancelled || this._cancelled) {
|
|
732
|
+
// Cancelled - close the generator
|
|
733
|
+
if (iterator.return) {
|
|
734
|
+
await iterator.return();
|
|
735
|
+
}
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
done = result.done;
|
|
740
|
+
|
|
741
|
+
if (!done) {
|
|
742
|
+
const chunk = result.value;
|
|
743
|
+
const buf = Buffer.from(chunk);
|
|
744
|
+
chunks.push(buf);
|
|
745
|
+
|
|
746
|
+
// Only output if not cancelled
|
|
747
|
+
if (!this._cancelled) {
|
|
748
|
+
if (this.options.mirror) {
|
|
749
|
+
safeWrite(process.stdout, buf);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
this.emit('stdout', buf);
|
|
753
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
// Clean up
|
|
759
|
+
this._virtualGenerator = null;
|
|
760
|
+
this._cancelResolve = null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
result = {
|
|
764
|
+
code: 0,
|
|
765
|
+
stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
|
|
766
|
+
stderr: this.options.capture ? '' : undefined,
|
|
767
|
+
stdin: this.options.capture ? stdinData : undefined
|
|
768
|
+
};
|
|
769
|
+
} else {
|
|
770
|
+
// Regular async function
|
|
771
|
+
result = await handler({ args: argValues, stdin: stdinData, ...this.options });
|
|
772
|
+
|
|
773
|
+
// Ensure result has required fields, respecting capture option
|
|
774
|
+
result = {
|
|
775
|
+
code: result.code ?? 0,
|
|
776
|
+
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
777
|
+
stderr: this.options.capture ? (result.stderr ?? '') : undefined,
|
|
778
|
+
stdin: this.options.capture ? stdinData : undefined,
|
|
779
|
+
...result
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// Mirror and emit output
|
|
783
|
+
if (result.stdout) {
|
|
784
|
+
const buf = Buffer.from(result.stdout);
|
|
785
|
+
if (this.options.mirror) {
|
|
786
|
+
safeWrite(process.stdout, buf);
|
|
787
|
+
}
|
|
788
|
+
this.emit('stdout', buf);
|
|
789
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (result.stderr) {
|
|
793
|
+
const buf = Buffer.from(result.stderr);
|
|
794
|
+
if (this.options.mirror) {
|
|
795
|
+
safeWrite(process.stderr, buf);
|
|
796
|
+
}
|
|
797
|
+
this.emit('stderr', buf);
|
|
798
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Store result
|
|
803
|
+
this.result = result;
|
|
804
|
+
this.finished = true;
|
|
805
|
+
|
|
806
|
+
// Emit completion events
|
|
807
|
+
this.emit('end', result);
|
|
808
|
+
this.emit('exit', result.code);
|
|
809
|
+
|
|
810
|
+
// Handle shell settings
|
|
811
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
812
|
+
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
813
|
+
error.code = result.code;
|
|
814
|
+
error.stdout = result.stdout;
|
|
815
|
+
error.stderr = result.stderr;
|
|
816
|
+
error.result = result;
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return result;
|
|
821
|
+
} catch (error) {
|
|
822
|
+
// Handle errors from virtual commands
|
|
823
|
+
const result = {
|
|
824
|
+
code: error.code ?? 1,
|
|
825
|
+
stdout: error.stdout ?? '',
|
|
826
|
+
stderr: error.stderr ?? error.message,
|
|
827
|
+
stdin: ''
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
this.result = result;
|
|
831
|
+
this.finished = true;
|
|
832
|
+
|
|
833
|
+
if (result.stderr) {
|
|
834
|
+
const buf = Buffer.from(result.stderr);
|
|
835
|
+
if (this.options.mirror) {
|
|
836
|
+
safeWrite(process.stderr, buf);
|
|
837
|
+
}
|
|
838
|
+
this.emit('stderr', buf);
|
|
839
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
this.emit('end', result);
|
|
843
|
+
this.emit('exit', result.code);
|
|
844
|
+
|
|
845
|
+
if (globalShellSettings.errexit) {
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async _runStreamingPipelineBun(commands) {
|
|
854
|
+
traceFunc('ProcessRunner', '_runStreamingPipelineBun', 'ENTER', {
|
|
855
|
+
commandsCount: commands.length
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// For true streaming, we need to handle virtual and real commands differently
|
|
859
|
+
// but make them work together seamlessly
|
|
860
|
+
|
|
861
|
+
// First, analyze the pipeline to identify virtual vs real commands
|
|
862
|
+
const pipelineInfo = commands.map(command => {
|
|
863
|
+
const { cmd, args } = command;
|
|
864
|
+
const isVirtual = virtualCommandsEnabled && virtualCommands.has(cmd);
|
|
865
|
+
return { ...command, isVirtual };
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
trace('ProcessRunner', 'Pipeline analysis', {
|
|
869
|
+
virtualCount: pipelineInfo.filter(p => p.isVirtual).length,
|
|
870
|
+
realCount: pipelineInfo.filter(p => !p.isVirtual).length
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// If pipeline contains virtual commands, use advanced streaming
|
|
874
|
+
if (pipelineInfo.some(info => info.isVirtual)) {
|
|
875
|
+
traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'MIXED_PIPELINE', {});
|
|
876
|
+
return this._runMixedStreamingPipeline(commands);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// For pipelines with commands that buffer (like jq), use tee streaming
|
|
880
|
+
const needsStreamingWorkaround = commands.some(c =>
|
|
881
|
+
c.cmd === 'jq' || c.cmd === 'grep' || c.cmd === 'sed' || c.cmd === 'cat' || c.cmd === 'awk'
|
|
882
|
+
);
|
|
883
|
+
if (needsStreamingWorkaround) {
|
|
884
|
+
traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'TEE_STREAMING', {
|
|
885
|
+
bufferedCommands: commands.filter(c =>
|
|
886
|
+
['jq', 'grep', 'sed', 'cat', 'awk'].includes(c.cmd)
|
|
887
|
+
).map(c => c.cmd)
|
|
888
|
+
});
|
|
889
|
+
return this._runTeeStreamingPipeline(commands);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// All real commands - use native pipe connections
|
|
893
|
+
const processes = [];
|
|
894
|
+
let allStderr = '';
|
|
895
|
+
|
|
896
|
+
for (let i = 0; i < commands.length; i++) {
|
|
897
|
+
const command = commands[i];
|
|
898
|
+
const { cmd, args } = command;
|
|
899
|
+
|
|
900
|
+
// Build command string
|
|
901
|
+
const commandParts = [cmd];
|
|
902
|
+
for (const arg of args) {
|
|
903
|
+
if (arg.value !== undefined) {
|
|
904
|
+
if (arg.quoted) {
|
|
905
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
906
|
+
} else if (arg.value.includes(' ')) {
|
|
907
|
+
commandParts.push(`"${arg.value}"`);
|
|
908
|
+
} else {
|
|
909
|
+
commandParts.push(arg.value);
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
913
|
+
commandParts.push(`"${arg}"`);
|
|
914
|
+
} else {
|
|
915
|
+
commandParts.push(arg);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const commandStr = commandParts.join(' ');
|
|
920
|
+
|
|
921
|
+
// Determine stdin for this process
|
|
922
|
+
let stdin;
|
|
923
|
+
let needsManualStdin = false;
|
|
924
|
+
let stdinData;
|
|
925
|
+
|
|
926
|
+
if (i === 0) {
|
|
927
|
+
// First command - use provided stdin or pipe
|
|
928
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
929
|
+
stdin = 'pipe';
|
|
930
|
+
needsManualStdin = true;
|
|
931
|
+
stdinData = Buffer.from(this.options.stdin);
|
|
932
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
933
|
+
stdin = 'pipe';
|
|
934
|
+
needsManualStdin = true;
|
|
935
|
+
stdinData = this.options.stdin;
|
|
936
|
+
} else {
|
|
937
|
+
stdin = 'ignore';
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
// Connect to previous process stdout
|
|
941
|
+
stdin = processes[i - 1].stdout;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Spawn the process directly (not through sh) for better streaming
|
|
945
|
+
// Only use sh -c for complex commands that need shell features
|
|
946
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
947
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
948
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
949
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
950
|
+
|
|
951
|
+
const spawnArgs = needsShell
|
|
952
|
+
? ['sh', '-c', commandStr]
|
|
953
|
+
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
954
|
+
|
|
955
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
956
|
+
cwd: this.options.cwd,
|
|
957
|
+
env: this.options.env,
|
|
958
|
+
stdin: stdin,
|
|
959
|
+
stdout: 'pipe',
|
|
960
|
+
stderr: 'pipe'
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Write stdin data if needed for first process
|
|
964
|
+
if (needsManualStdin && stdinData && proc.stdin) {
|
|
965
|
+
// Add error handler for Bun stdin
|
|
966
|
+
if (proc.stdin && typeof proc.stdin.on === 'function') {
|
|
967
|
+
proc.stdin.on('error', (error) => {
|
|
968
|
+
if (error.code !== 'EPIPE') {
|
|
969
|
+
trace('ProcessRunner', 'Bun stdin error event', { error: error.message, code: error.code });
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
(async () => {
|
|
975
|
+
try {
|
|
976
|
+
// Bun's FileSink has write and end methods
|
|
977
|
+
if (proc.stdin && proc.stdin.writable && !proc.stdin.destroyed && !proc.stdin.closed) {
|
|
978
|
+
await proc.stdin.write(stdinData);
|
|
979
|
+
await proc.stdin.end();
|
|
980
|
+
}
|
|
981
|
+
} catch (e) {
|
|
982
|
+
if (e.code !== 'EPIPE') {
|
|
983
|
+
trace('ProcessRunner', 'Error writing stdin (Bun)', { error: e.message, code: e.code });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
})();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
processes.push(proc);
|
|
990
|
+
|
|
991
|
+
// Collect stderr from all processes
|
|
992
|
+
(async () => {
|
|
993
|
+
for await (const chunk of proc.stderr) {
|
|
411
994
|
const buf = Buffer.from(chunk);
|
|
412
|
-
|
|
995
|
+
allStderr += buf.toString();
|
|
996
|
+
// Only emit stderr for the last command
|
|
997
|
+
if (i === commands.length - 1) {
|
|
998
|
+
if (this.options.mirror) {
|
|
999
|
+
safeWrite(process.stderr, buf);
|
|
1000
|
+
}
|
|
1001
|
+
this.emit('stderr', buf);
|
|
1002
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
})();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Stream output from the last process
|
|
1009
|
+
const lastProc = processes[processes.length - 1];
|
|
1010
|
+
let finalOutput = '';
|
|
1011
|
+
|
|
1012
|
+
// Stream stdout from last process
|
|
1013
|
+
for await (const chunk of lastProc.stdout) {
|
|
1014
|
+
const buf = Buffer.from(chunk);
|
|
1015
|
+
finalOutput += buf.toString();
|
|
1016
|
+
if (this.options.mirror) {
|
|
1017
|
+
safeWrite(process.stdout, buf);
|
|
1018
|
+
}
|
|
1019
|
+
this.emit('stdout', buf);
|
|
1020
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Wait for all processes to complete
|
|
1024
|
+
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
1025
|
+
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
1026
|
+
|
|
1027
|
+
// Check for pipeline failures if pipefail is set
|
|
1028
|
+
if (globalShellSettings.pipefail) {
|
|
1029
|
+
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
1030
|
+
if (failedIndex !== -1) {
|
|
1031
|
+
const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`);
|
|
1032
|
+
error.code = exitCodes[failedIndex];
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const result = createResult({
|
|
1038
|
+
code: lastExitCode || 0,
|
|
1039
|
+
stdout: finalOutput,
|
|
1040
|
+
stderr: allStderr,
|
|
1041
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1042
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
this.result = result;
|
|
1046
|
+
this.finished = true;
|
|
1047
|
+
|
|
1048
|
+
this.emit('end', result);
|
|
1049
|
+
this.emit('exit', result.code);
|
|
1050
|
+
|
|
1051
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1052
|
+
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
1053
|
+
error.code = result.code;
|
|
1054
|
+
error.stdout = result.stdout;
|
|
1055
|
+
error.stderr = result.stderr;
|
|
1056
|
+
error.result = result;
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async _runTeeStreamingPipeline(commands) {
|
|
1064
|
+
traceFunc('ProcessRunner', '_runTeeStreamingPipeline', 'ENTER', {
|
|
1065
|
+
commandsCount: commands.length
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Use tee() to split streams for real-time reading
|
|
1069
|
+
// This works around jq and similar commands that buffer when piped
|
|
1070
|
+
|
|
1071
|
+
const processes = [];
|
|
1072
|
+
let allStderr = '';
|
|
1073
|
+
let currentStream = null;
|
|
1074
|
+
|
|
1075
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1076
|
+
const command = commands[i];
|
|
1077
|
+
const { cmd, args } = command;
|
|
1078
|
+
|
|
1079
|
+
// Build command string
|
|
1080
|
+
const commandParts = [cmd];
|
|
1081
|
+
for (const arg of args) {
|
|
1082
|
+
if (arg.value !== undefined) {
|
|
1083
|
+
if (arg.quoted) {
|
|
1084
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
1085
|
+
} else if (arg.value.includes(' ')) {
|
|
1086
|
+
commandParts.push(`"${arg.value}"`);
|
|
1087
|
+
} else {
|
|
1088
|
+
commandParts.push(arg.value);
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
1092
|
+
commandParts.push(`"${arg}"`);
|
|
1093
|
+
} else {
|
|
1094
|
+
commandParts.push(arg);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const commandStr = commandParts.join(' ');
|
|
1099
|
+
|
|
1100
|
+
// Determine stdin for this process
|
|
1101
|
+
let stdin;
|
|
1102
|
+
let needsManualStdin = false;
|
|
1103
|
+
let stdinData;
|
|
1104
|
+
|
|
1105
|
+
if (i === 0) {
|
|
1106
|
+
// First command - use provided stdin or ignore
|
|
1107
|
+
if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
1108
|
+
stdin = 'pipe';
|
|
1109
|
+
needsManualStdin = true;
|
|
1110
|
+
stdinData = Buffer.from(this.options.stdin);
|
|
1111
|
+
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
1112
|
+
stdin = 'pipe';
|
|
1113
|
+
needsManualStdin = true;
|
|
1114
|
+
stdinData = this.options.stdin;
|
|
1115
|
+
} else {
|
|
1116
|
+
stdin = 'ignore';
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
// Use the stream from previous process
|
|
1120
|
+
stdin = currentStream;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Spawn the process directly (not through sh) for better control
|
|
1124
|
+
const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
|
|
1125
|
+
commandStr.includes('>') || commandStr.includes('<') ||
|
|
1126
|
+
commandStr.includes('&&') || commandStr.includes('||') ||
|
|
1127
|
+
commandStr.includes(';') || commandStr.includes('`');
|
|
1128
|
+
|
|
1129
|
+
const spawnArgs = needsShell
|
|
1130
|
+
? ['sh', '-c', commandStr]
|
|
1131
|
+
: [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
|
|
1132
|
+
|
|
1133
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
1134
|
+
cwd: this.options.cwd,
|
|
1135
|
+
env: this.options.env,
|
|
1136
|
+
stdin: stdin,
|
|
1137
|
+
stdout: 'pipe',
|
|
1138
|
+
stderr: 'pipe'
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// Write stdin data if needed for first process
|
|
1142
|
+
if (needsManualStdin && stdinData && proc.stdin) {
|
|
1143
|
+
// Add error handler for Node stdin
|
|
1144
|
+
if (proc.stdin && typeof proc.stdin.on === 'function') {
|
|
1145
|
+
proc.stdin.on('error', (error) => {
|
|
1146
|
+
if (error.code !== 'EPIPE') {
|
|
1147
|
+
trace('ProcessRunner', 'Node stdin error event', { error: error.message, code: error.code });
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
try {
|
|
1153
|
+
if (proc.stdin && proc.stdin.writable && !proc.stdin.destroyed && !proc.stdin.closed) {
|
|
1154
|
+
await proc.stdin.write(stdinData);
|
|
1155
|
+
await proc.stdin.end();
|
|
1156
|
+
}
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
if (e.code !== 'EPIPE') {
|
|
1159
|
+
trace('ProcessRunner', 'Error writing stdin (Node stream)', { error: e.message, code: e.code });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
processes.push(proc);
|
|
1165
|
+
|
|
1166
|
+
// For non-last processes, tee the output so we can both pipe and read
|
|
1167
|
+
if (i < commands.length - 1) {
|
|
1168
|
+
const [readStream, pipeStream] = proc.stdout.tee();
|
|
1169
|
+
currentStream = pipeStream;
|
|
1170
|
+
|
|
1171
|
+
// Read from the tee'd stream for real-time updates
|
|
1172
|
+
// Always read from the first process for best streaming
|
|
1173
|
+
if (i === 0) {
|
|
1174
|
+
(async () => {
|
|
1175
|
+
for await (const chunk of readStream) {
|
|
1176
|
+
// Emit from the first process for real-time updates
|
|
1177
|
+
const buf = Buffer.from(chunk);
|
|
1178
|
+
if (this.options.mirror) {
|
|
1179
|
+
safeWrite(process.stdout, buf);
|
|
1180
|
+
}
|
|
1181
|
+
this.emit('stdout', buf);
|
|
1182
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1183
|
+
}
|
|
1184
|
+
})();
|
|
1185
|
+
} else {
|
|
1186
|
+
// Consume other tee'd streams to prevent blocking
|
|
1187
|
+
(async () => {
|
|
1188
|
+
for await (const chunk of readStream) {
|
|
1189
|
+
// Just consume to keep flowing
|
|
1190
|
+
}
|
|
1191
|
+
})();
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
currentStream = proc.stdout;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Collect stderr from all processes
|
|
1198
|
+
(async () => {
|
|
1199
|
+
for await (const chunk of proc.stderr) {
|
|
1200
|
+
const buf = Buffer.from(chunk);
|
|
1201
|
+
allStderr += buf.toString();
|
|
1202
|
+
if (i === commands.length - 1) {
|
|
1203
|
+
if (this.options.mirror) {
|
|
1204
|
+
safeWrite(process.stderr, buf);
|
|
1205
|
+
}
|
|
1206
|
+
this.emit('stderr', buf);
|
|
1207
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
})();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Read final output from the last process
|
|
1214
|
+
const lastProc = processes[processes.length - 1];
|
|
1215
|
+
let finalOutput = '';
|
|
1216
|
+
|
|
1217
|
+
// If we haven't emitted stdout yet (no tee), emit from last process
|
|
1218
|
+
const shouldEmitFromLast = commands.length === 1;
|
|
1219
|
+
|
|
1220
|
+
for await (const chunk of lastProc.stdout) {
|
|
1221
|
+
const buf = Buffer.from(chunk);
|
|
1222
|
+
finalOutput += buf.toString();
|
|
1223
|
+
if (shouldEmitFromLast) {
|
|
1224
|
+
if (this.options.mirror) {
|
|
1225
|
+
safeWrite(process.stdout, buf);
|
|
1226
|
+
}
|
|
1227
|
+
this.emit('stdout', buf);
|
|
1228
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Wait for all processes to complete
|
|
1233
|
+
const exitCodes = await Promise.all(processes.map(p => p.exited));
|
|
1234
|
+
const lastExitCode = exitCodes[exitCodes.length - 1];
|
|
1235
|
+
|
|
1236
|
+
// Check for pipeline failures if pipefail is set
|
|
1237
|
+
if (globalShellSettings.pipefail) {
|
|
1238
|
+
const failedIndex = exitCodes.findIndex(code => code !== 0);
|
|
1239
|
+
if (failedIndex !== -1) {
|
|
1240
|
+
const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`);
|
|
1241
|
+
error.code = exitCodes[failedIndex];
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const result = createResult({
|
|
1247
|
+
code: lastExitCode || 0,
|
|
1248
|
+
stdout: finalOutput,
|
|
1249
|
+
stderr: allStderr,
|
|
1250
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1251
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
this.result = result;
|
|
1255
|
+
this.finished = true;
|
|
1256
|
+
|
|
1257
|
+
this.emit('end', result);
|
|
1258
|
+
this.emit('exit', result.code);
|
|
1259
|
+
|
|
1260
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1261
|
+
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
1262
|
+
error.code = result.code;
|
|
1263
|
+
error.stdout = result.stdout;
|
|
1264
|
+
error.stderr = result.stderr;
|
|
1265
|
+
error.result = result;
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
async _runMixedStreamingPipeline(commands) {
|
|
1274
|
+
traceFunc('ProcessRunner', '_runMixedStreamingPipeline', 'ENTER', {
|
|
1275
|
+
commandsCount: commands.length
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Handle pipelines with both virtual and real commands
|
|
1279
|
+
// Each stage reads from previous stage's output stream
|
|
1280
|
+
|
|
1281
|
+
let currentInputStream = null;
|
|
1282
|
+
let finalOutput = '';
|
|
1283
|
+
let allStderr = '';
|
|
1284
|
+
|
|
1285
|
+
// Set up initial input stream if provided
|
|
1286
|
+
if (this.options.stdin) {
|
|
1287
|
+
const inputData = typeof this.options.stdin === 'string'
|
|
1288
|
+
? this.options.stdin
|
|
1289
|
+
: this.options.stdin.toString('utf8');
|
|
1290
|
+
|
|
1291
|
+
// Create a readable stream from the input
|
|
1292
|
+
currentInputStream = new ReadableStream({
|
|
1293
|
+
start(controller) {
|
|
1294
|
+
controller.enqueue(new TextEncoder().encode(inputData));
|
|
1295
|
+
controller.close();
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1301
|
+
const command = commands[i];
|
|
1302
|
+
const { cmd, args } = command;
|
|
1303
|
+
const isLastCommand = i === commands.length - 1;
|
|
1304
|
+
|
|
1305
|
+
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1306
|
+
// Handle virtual command with streaming
|
|
1307
|
+
traceBranch('ProcessRunner', '_runMixedStreamingPipeline', 'VIRTUAL_COMMAND', {
|
|
1308
|
+
cmd,
|
|
1309
|
+
commandIndex: i
|
|
1310
|
+
});
|
|
1311
|
+
const handler = virtualCommands.get(cmd);
|
|
1312
|
+
const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
|
|
1313
|
+
|
|
1314
|
+
// Read input from stream if available
|
|
1315
|
+
let inputData = '';
|
|
1316
|
+
if (currentInputStream) {
|
|
1317
|
+
const reader = currentInputStream.getReader();
|
|
1318
|
+
try {
|
|
1319
|
+
while (true) {
|
|
1320
|
+
const { done, value } = await reader.read();
|
|
1321
|
+
if (done) break;
|
|
1322
|
+
inputData += new TextDecoder().decode(value);
|
|
1323
|
+
}
|
|
1324
|
+
} finally {
|
|
1325
|
+
reader.releaseLock();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Check if handler is async generator (streaming)
|
|
1330
|
+
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1331
|
+
// Create output stream from generator
|
|
1332
|
+
const chunks = [];
|
|
1333
|
+
const self = this; // Capture this context
|
|
1334
|
+
currentInputStream = new ReadableStream({
|
|
1335
|
+
async start(controller) {
|
|
1336
|
+
const { stdin: _, ...optionsWithoutStdin } = self.options;
|
|
1337
|
+
for await (const chunk of handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin })) {
|
|
1338
|
+
const data = Buffer.from(chunk);
|
|
1339
|
+
controller.enqueue(data);
|
|
1340
|
+
|
|
1341
|
+
// Emit for last command
|
|
1342
|
+
if (isLastCommand) {
|
|
1343
|
+
chunks.push(data);
|
|
1344
|
+
if (self.options.mirror) {
|
|
1345
|
+
safeWrite(process.stdout, data);
|
|
1346
|
+
}
|
|
1347
|
+
self.emit('stdout', data);
|
|
1348
|
+
self.emit('data', { type: 'stdout', data });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
controller.close();
|
|
1352
|
+
|
|
1353
|
+
if (isLastCommand) {
|
|
1354
|
+
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
} else {
|
|
1359
|
+
// Regular async function
|
|
1360
|
+
const { stdin: _, ...optionsWithoutStdin } = this.options;
|
|
1361
|
+
const result = await handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin });
|
|
1362
|
+
const outputData = result.stdout || '';
|
|
413
1363
|
|
|
414
|
-
if (
|
|
415
|
-
|
|
1364
|
+
if (isLastCommand) {
|
|
1365
|
+
finalOutput = outputData;
|
|
1366
|
+
const buf = Buffer.from(outputData);
|
|
1367
|
+
if (this.options.mirror) {
|
|
1368
|
+
safeWrite(process.stdout, buf);
|
|
1369
|
+
}
|
|
1370
|
+
this.emit('stdout', buf);
|
|
1371
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
416
1372
|
}
|
|
417
1373
|
|
|
418
|
-
|
|
419
|
-
|
|
1374
|
+
// Create stream from output
|
|
1375
|
+
currentInputStream = new ReadableStream({
|
|
1376
|
+
start(controller) {
|
|
1377
|
+
controller.enqueue(new TextEncoder().encode(outputData));
|
|
1378
|
+
controller.close();
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
if (result.stderr) {
|
|
1383
|
+
allStderr += result.stderr;
|
|
1384
|
+
}
|
|
420
1385
|
}
|
|
421
|
-
|
|
422
|
-
result = {
|
|
423
|
-
code: 0,
|
|
424
|
-
stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
|
|
425
|
-
stderr: this.options.capture ? '' : undefined,
|
|
426
|
-
stdin: this.options.capture ? stdinData : undefined
|
|
427
|
-
};
|
|
428
1386
|
} else {
|
|
429
|
-
//
|
|
430
|
-
|
|
1387
|
+
// Handle real command - spawn with streaming
|
|
1388
|
+
const commandParts = [cmd];
|
|
1389
|
+
for (const arg of args) {
|
|
1390
|
+
if (arg.value !== undefined) {
|
|
1391
|
+
if (arg.quoted) {
|
|
1392
|
+
commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
|
|
1393
|
+
} else if (arg.value.includes(' ')) {
|
|
1394
|
+
commandParts.push(`"${arg.value}"`);
|
|
1395
|
+
} else {
|
|
1396
|
+
commandParts.push(arg.value);
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
|
1400
|
+
commandParts.push(`"${arg}"`);
|
|
1401
|
+
} else {
|
|
1402
|
+
commandParts.push(arg);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const commandStr = commandParts.join(' ');
|
|
431
1407
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
};
|
|
1408
|
+
// Spawn the process
|
|
1409
|
+
const proc = Bun.spawn(['sh', '-c', commandStr], {
|
|
1410
|
+
cwd: this.options.cwd,
|
|
1411
|
+
env: this.options.env,
|
|
1412
|
+
stdin: currentInputStream ? 'pipe' : 'ignore',
|
|
1413
|
+
stdout: 'pipe',
|
|
1414
|
+
stderr: 'pipe'
|
|
1415
|
+
});
|
|
440
1416
|
|
|
441
|
-
//
|
|
442
|
-
if (
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1417
|
+
// Write input stream to process stdin if needed
|
|
1418
|
+
if (currentInputStream && proc.stdin) {
|
|
1419
|
+
const reader = currentInputStream.getReader();
|
|
1420
|
+
const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
|
|
1421
|
+
|
|
1422
|
+
(async () => {
|
|
1423
|
+
try {
|
|
1424
|
+
while (true) {
|
|
1425
|
+
const { done, value } = await reader.read();
|
|
1426
|
+
if (done) break;
|
|
1427
|
+
if (writer.write) {
|
|
1428
|
+
try {
|
|
1429
|
+
await writer.write(value);
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
if (error.code !== 'EPIPE') {
|
|
1432
|
+
trace('ProcessRunner', 'Error writing to stream writer', { error: error.message, code: error.code });
|
|
1433
|
+
}
|
|
1434
|
+
break; // Stop streaming if write fails
|
|
1435
|
+
}
|
|
1436
|
+
} else if (writer.getWriter) {
|
|
1437
|
+
try {
|
|
1438
|
+
const w = writer.getWriter();
|
|
1439
|
+
await w.write(value);
|
|
1440
|
+
w.releaseLock();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
if (error.code !== 'EPIPE') {
|
|
1443
|
+
trace('ProcessRunner', 'Error writing to stream writer (getWriter)', { error: error.message, code: error.code });
|
|
1444
|
+
}
|
|
1445
|
+
break; // Stop streaming if write fails
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} finally {
|
|
1450
|
+
reader.releaseLock();
|
|
1451
|
+
if (writer.close) await writer.close();
|
|
1452
|
+
else if (writer.end) writer.end();
|
|
1453
|
+
}
|
|
1454
|
+
})();
|
|
449
1455
|
}
|
|
450
1456
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1457
|
+
// Set up output stream
|
|
1458
|
+
currentInputStream = proc.stdout;
|
|
1459
|
+
|
|
1460
|
+
// Handle stderr
|
|
1461
|
+
(async () => {
|
|
1462
|
+
for await (const chunk of proc.stderr) {
|
|
1463
|
+
const buf = Buffer.from(chunk);
|
|
1464
|
+
allStderr += buf.toString();
|
|
1465
|
+
if (isLastCommand) {
|
|
1466
|
+
if (this.options.mirror) {
|
|
1467
|
+
safeWrite(process.stderr, buf);
|
|
1468
|
+
}
|
|
1469
|
+
this.emit('stderr', buf);
|
|
1470
|
+
this.emit('data', { type: 'stderr', data: buf });
|
|
1471
|
+
}
|
|
455
1472
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
error.code = result.code;
|
|
473
|
-
error.stdout = result.stdout;
|
|
474
|
-
error.stderr = result.stderr;
|
|
475
|
-
error.result = result;
|
|
476
|
-
throw error;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return result;
|
|
480
|
-
} catch (error) {
|
|
481
|
-
// Handle errors from virtual commands
|
|
482
|
-
const result = {
|
|
483
|
-
code: error.code ?? 1,
|
|
484
|
-
stdout: error.stdout ?? '',
|
|
485
|
-
stderr: error.stderr ?? error.message,
|
|
486
|
-
stdin: ''
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
this.result = result;
|
|
490
|
-
this.finished = true;
|
|
491
|
-
|
|
492
|
-
if (result.stderr) {
|
|
493
|
-
const buf = Buffer.from(result.stderr);
|
|
494
|
-
if (this.options.mirror) {
|
|
495
|
-
process.stderr.write(buf);
|
|
1473
|
+
})();
|
|
1474
|
+
|
|
1475
|
+
// For last command, stream output
|
|
1476
|
+
if (isLastCommand) {
|
|
1477
|
+
const chunks = [];
|
|
1478
|
+
for await (const chunk of proc.stdout) {
|
|
1479
|
+
const buf = Buffer.from(chunk);
|
|
1480
|
+
chunks.push(buf);
|
|
1481
|
+
if (this.options.mirror) {
|
|
1482
|
+
safeWrite(process.stdout, buf);
|
|
1483
|
+
}
|
|
1484
|
+
this.emit('stdout', buf);
|
|
1485
|
+
this.emit('data', { type: 'stdout', data: buf });
|
|
1486
|
+
}
|
|
1487
|
+
finalOutput = Buffer.concat(chunks).toString('utf8');
|
|
1488
|
+
await proc.exited;
|
|
496
1489
|
}
|
|
497
|
-
this.emit('stderr', buf);
|
|
498
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
499
1490
|
}
|
|
500
|
-
|
|
501
|
-
this.emit('end', result);
|
|
502
|
-
this.emit('exit', result.code);
|
|
503
|
-
|
|
504
|
-
if (globalShellSettings.errexit) {
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return result;
|
|
509
1491
|
}
|
|
1492
|
+
|
|
1493
|
+
const result = createResult({
|
|
1494
|
+
code: 0, // TODO: Track exit codes properly
|
|
1495
|
+
stdout: finalOutput,
|
|
1496
|
+
stderr: allStderr,
|
|
1497
|
+
stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
|
|
1498
|
+
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
this.result = result;
|
|
1502
|
+
this.finished = true;
|
|
1503
|
+
|
|
1504
|
+
this.emit('end', result);
|
|
1505
|
+
this.emit('exit', result.code);
|
|
1506
|
+
|
|
1507
|
+
return result;
|
|
510
1508
|
}
|
|
511
1509
|
|
|
512
|
-
async
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
1510
|
+
async _runPipelineNonStreaming(commands) {
|
|
1511
|
+
traceFunc('ProcessRunner', '_runPipelineNonStreaming', 'ENTER', {
|
|
1512
|
+
commandsCount: commands.length
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// Original non-streaming implementation for fallback (e.g., virtual commands)
|
|
517
1516
|
let currentOutput = '';
|
|
518
1517
|
let currentInput = '';
|
|
519
1518
|
|
|
@@ -531,6 +1530,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
531
1530
|
|
|
532
1531
|
// Check if this is a virtual command (only if virtual commands are enabled)
|
|
533
1532
|
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1533
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'VIRTUAL_COMMAND', {
|
|
1534
|
+
cmd,
|
|
1535
|
+
argsCount: args.length
|
|
1536
|
+
});
|
|
1537
|
+
|
|
534
1538
|
// Run virtual command with current input
|
|
535
1539
|
const handler = virtualCommands.get(cmd);
|
|
536
1540
|
|
|
@@ -550,8 +1554,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
550
1554
|
|
|
551
1555
|
// Check if handler is async generator (streaming)
|
|
552
1556
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1557
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'ASYNC_GENERATOR', { cmd });
|
|
553
1558
|
const chunks = [];
|
|
554
|
-
for await (const chunk of handler(argValues, currentInput, this.options)) {
|
|
1559
|
+
for await (const chunk of handler({ args: argValues, stdin: currentInput, ...this.options })) {
|
|
555
1560
|
chunks.push(Buffer.from(chunk));
|
|
556
1561
|
}
|
|
557
1562
|
result = {
|
|
@@ -562,7 +1567,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
562
1567
|
};
|
|
563
1568
|
} else {
|
|
564
1569
|
// Regular async function
|
|
565
|
-
result = await handler(argValues, currentInput, this.options);
|
|
1570
|
+
result = await handler({ args: argValues, stdin: currentInput, ...this.options });
|
|
566
1571
|
result = {
|
|
567
1572
|
code: result.code ?? 0,
|
|
568
1573
|
stdout: this.options.capture ? (result.stdout ?? '') : undefined,
|
|
@@ -583,7 +1588,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
583
1588
|
if (result.stdout) {
|
|
584
1589
|
const buf = Buffer.from(result.stdout);
|
|
585
1590
|
if (this.options.mirror) {
|
|
586
|
-
process.stdout
|
|
1591
|
+
safeWrite(process.stdout, buf);
|
|
587
1592
|
}
|
|
588
1593
|
this.emit('stdout', buf);
|
|
589
1594
|
this.emit('data', { type: 'stdout', data: buf });
|
|
@@ -592,7 +1597,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
592
1597
|
if (result.stderr) {
|
|
593
1598
|
const buf = Buffer.from(result.stderr);
|
|
594
1599
|
if (this.options.mirror) {
|
|
595
|
-
process.stderr
|
|
1600
|
+
safeWrite(process.stderr, buf);
|
|
596
1601
|
}
|
|
597
1602
|
this.emit('stderr', buf);
|
|
598
1603
|
this.emit('data', { type: 'stderr', data: buf });
|
|
@@ -652,7 +1657,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
652
1657
|
if (result.stderr) {
|
|
653
1658
|
const buf = Buffer.from(result.stderr);
|
|
654
1659
|
if (this.options.mirror) {
|
|
655
|
-
process.stderr
|
|
1660
|
+
safeWrite(process.stderr, buf);
|
|
656
1661
|
}
|
|
657
1662
|
this.emit('stderr', buf);
|
|
658
1663
|
this.emit('data', { type: 'stderr', data: buf });
|
|
@@ -703,49 +1708,131 @@ class ProcessRunner extends StreamEmitter {
|
|
|
703
1708
|
console.log(commandStr);
|
|
704
1709
|
}
|
|
705
1710
|
|
|
706
|
-
// Execute the system command with current input as stdin
|
|
707
|
-
const
|
|
708
|
-
return Bun.spawnSync(argv, {
|
|
709
|
-
cwd: this.options.cwd,
|
|
710
|
-
env: this.options.env,
|
|
711
|
-
stdin: stdin ? Buffer.from(stdin) : undefined,
|
|
712
|
-
stdout: 'pipe',
|
|
713
|
-
stderr: 'pipe'
|
|
714
|
-
});
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
const spawnNode = (argv, stdin) => {
|
|
1711
|
+
// Execute the system command with current input as stdin (ASYNC VERSION)
|
|
1712
|
+
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
718
1713
|
const require = createRequire(import.meta.url);
|
|
719
1714
|
const cp = require('child_process');
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1715
|
+
|
|
1716
|
+
return new Promise((resolve, reject) => {
|
|
1717
|
+
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
1718
|
+
cwd: this.options.cwd,
|
|
1719
|
+
env: this.options.env,
|
|
1720
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
let stdout = '';
|
|
1724
|
+
let stderr = '';
|
|
1725
|
+
|
|
1726
|
+
proc.stdout.on('data', (chunk) => {
|
|
1727
|
+
stdout += chunk.toString();
|
|
1728
|
+
// If this is the last command, emit streaming data
|
|
1729
|
+
if (isLastCommand) {
|
|
1730
|
+
if (this.options.mirror) {
|
|
1731
|
+
safeWrite(process.stdout, chunk);
|
|
1732
|
+
}
|
|
1733
|
+
this.emit('stdout', chunk);
|
|
1734
|
+
this.emit('data', { type: 'stdout', data: chunk });
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
proc.stderr.on('data', (chunk) => {
|
|
1739
|
+
stderr += chunk.toString();
|
|
1740
|
+
// If this is the last command, emit streaming data
|
|
1741
|
+
if (isLastCommand) {
|
|
1742
|
+
if (this.options.mirror) {
|
|
1743
|
+
safeWrite(process.stderr, chunk);
|
|
1744
|
+
}
|
|
1745
|
+
this.emit('stderr', chunk);
|
|
1746
|
+
this.emit('data', { type: 'stderr', data: chunk });
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
proc.on('close', (code) => {
|
|
1751
|
+
resolve({
|
|
1752
|
+
status: code,
|
|
1753
|
+
stdout,
|
|
1754
|
+
stderr
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
proc.on('error', reject);
|
|
1759
|
+
|
|
1760
|
+
// Add error handler to stdin to prevent unhandled error events
|
|
1761
|
+
if (proc.stdin) {
|
|
1762
|
+
proc.stdin.on('error', (error) => {
|
|
1763
|
+
trace('ProcessRunner', 'stdin error event', {
|
|
1764
|
+
error: error.message,
|
|
1765
|
+
code: error.code,
|
|
1766
|
+
isEPIPE: error.code === 'EPIPE'
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
// Only reject on non-EPIPE errors
|
|
1770
|
+
if (error.code !== 'EPIPE') {
|
|
1771
|
+
reject(error);
|
|
1772
|
+
}
|
|
1773
|
+
// EPIPE errors are expected when pipe is closed, so we ignore them
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (stdin) {
|
|
1778
|
+
// Use safe write to handle potential EPIPE errors
|
|
1779
|
+
trace('ProcessRunner', 'Attempting to write stdin', {
|
|
1780
|
+
hasStdin: !!proc.stdin,
|
|
1781
|
+
writable: proc.stdin?.writable,
|
|
1782
|
+
destroyed: proc.stdin?.destroyed,
|
|
1783
|
+
closed: proc.stdin?.closed,
|
|
1784
|
+
stdinLength: stdin.length
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
if (proc.stdin && proc.stdin.writable && !proc.stdin.destroyed && !proc.stdin.closed) {
|
|
1788
|
+
try {
|
|
1789
|
+
proc.stdin.write(stdin);
|
|
1790
|
+
trace('ProcessRunner', 'Successfully wrote to stdin', { stdinLength: stdin.length });
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
trace('ProcessRunner', 'Error writing to stdin', {
|
|
1793
|
+
error: error.message,
|
|
1794
|
+
code: error.code,
|
|
1795
|
+
isEPIPE: error.code === 'EPIPE'
|
|
1796
|
+
});
|
|
1797
|
+
if (error.code !== 'EPIPE') {
|
|
1798
|
+
throw error; // Re-throw non-EPIPE errors
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
} else {
|
|
1802
|
+
trace('ProcessRunner', 'Skipped writing to stdin - stream not writable', {
|
|
1803
|
+
hasStdin: !!proc.stdin,
|
|
1804
|
+
writable: proc.stdin?.writable,
|
|
1805
|
+
destroyed: proc.stdin?.destroyed,
|
|
1806
|
+
closed: proc.stdin?.closed
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Safely end the stdin stream
|
|
1812
|
+
if (proc.stdin && typeof proc.stdin.end === 'function' &&
|
|
1813
|
+
proc.stdin.writable && !proc.stdin.destroyed && !proc.stdin.closed) {
|
|
1814
|
+
try {
|
|
1815
|
+
proc.stdin.end();
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
if (error.code !== 'EPIPE') {
|
|
1818
|
+
trace('ProcessRunner', 'Error ending stdin', { error: error.message });
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
726
1822
|
});
|
|
727
1823
|
};
|
|
728
1824
|
|
|
729
1825
|
// Execute using shell to handle complex commands
|
|
730
1826
|
const argv = ['sh', '-c', commandStr];
|
|
731
|
-
const
|
|
1827
|
+
const isLastCommand = (i === commands.length - 1);
|
|
1828
|
+
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
732
1829
|
|
|
733
|
-
let result
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
stdin: currentInput
|
|
740
|
-
};
|
|
741
|
-
} else {
|
|
742
|
-
result = {
|
|
743
|
-
code: proc.status || 0,
|
|
744
|
-
stdout: proc.stdout || '',
|
|
745
|
-
stderr: proc.stderr || '',
|
|
746
|
-
stdin: currentInput
|
|
747
|
-
};
|
|
748
|
-
}
|
|
1830
|
+
let result = {
|
|
1831
|
+
code: proc.status || 0,
|
|
1832
|
+
stdout: proc.stdout || '',
|
|
1833
|
+
stderr: proc.stderr || '',
|
|
1834
|
+
stdin: currentInput
|
|
1835
|
+
};
|
|
749
1836
|
|
|
750
1837
|
// If command failed and pipefail is set, fail the entire pipeline
|
|
751
1838
|
if (globalShellSettings.pipefail && result.code !== 0) {
|
|
@@ -765,7 +1852,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
765
1852
|
this.errChunks.push(Buffer.from(result.stderr));
|
|
766
1853
|
}
|
|
767
1854
|
} else {
|
|
768
|
-
// This is the last command -
|
|
1855
|
+
// This is the last command - store final result (streaming already handled during execution)
|
|
769
1856
|
currentOutput = result.stdout;
|
|
770
1857
|
|
|
771
1858
|
// Collect all accumulated stderr
|
|
@@ -777,25 +1864,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
777
1864
|
allStderr += result.stderr;
|
|
778
1865
|
}
|
|
779
1866
|
|
|
780
|
-
// Mirror and emit output for final command
|
|
781
|
-
if (result.stdout) {
|
|
782
|
-
const buf = Buffer.from(result.stdout);
|
|
783
|
-
if (this.options.mirror) {
|
|
784
|
-
process.stdout.write(buf);
|
|
785
|
-
}
|
|
786
|
-
this.emit('stdout', buf);
|
|
787
|
-
this.emit('data', { type: 'stdout', data: buf });
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (allStderr) {
|
|
791
|
-
const buf = Buffer.from(allStderr);
|
|
792
|
-
if (this.options.mirror) {
|
|
793
|
-
process.stderr.write(buf);
|
|
794
|
-
}
|
|
795
|
-
this.emit('stderr', buf);
|
|
796
|
-
this.emit('data', { type: 'stderr', data: buf });
|
|
797
|
-
}
|
|
798
|
-
|
|
799
1867
|
// Store final result using createResult helper for .text() method compatibility
|
|
800
1868
|
const finalResult = createResult({
|
|
801
1869
|
code: result.code,
|
|
@@ -841,7 +1909,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
841
1909
|
if (result.stderr) {
|
|
842
1910
|
const buf = Buffer.from(result.stderr);
|
|
843
1911
|
if (this.options.mirror) {
|
|
844
|
-
process.stderr
|
|
1912
|
+
safeWrite(process.stderr, buf);
|
|
845
1913
|
}
|
|
846
1914
|
this.emit('stderr', buf);
|
|
847
1915
|
this.emit('data', { type: 'stderr', data: buf });
|
|
@@ -860,26 +1928,65 @@ class ProcessRunner extends StreamEmitter {
|
|
|
860
1928
|
}
|
|
861
1929
|
}
|
|
862
1930
|
|
|
1931
|
+
async _runPipeline(commands) {
|
|
1932
|
+
traceFunc('ProcessRunner', '_runPipeline', 'ENTER', {
|
|
1933
|
+
commandsCount: commands.length
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
if (commands.length === 0) {
|
|
1937
|
+
traceBranch('ProcessRunner', '_runPipeline', 'NO_COMMANDS', {});
|
|
1938
|
+
return createResult({ code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' });
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
// For true streaming, we need to connect processes via pipes
|
|
1943
|
+
if (isBun) {
|
|
1944
|
+
traceBranch('ProcessRunner', '_runPipeline', 'BUN_STREAMING', {});
|
|
1945
|
+
return this._runStreamingPipelineBun(commands);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// For Node.js, fall back to non-streaming implementation for now
|
|
1949
|
+
traceBranch('ProcessRunner', '_runPipeline', 'NODE_NON_STREAMING', {});
|
|
1950
|
+
return this._runPipelineNonStreaming(commands);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
863
1953
|
// Run programmatic pipeline (.pipe() method)
|
|
864
1954
|
async _runProgrammaticPipeline(source, destination) {
|
|
1955
|
+
traceFunc('ProcessRunner', '_runProgrammaticPipeline', 'ENTER', {});
|
|
1956
|
+
|
|
865
1957
|
try {
|
|
866
1958
|
// Execute the source command first
|
|
1959
|
+
trace('ProcessRunner', 'Executing source command', {});
|
|
867
1960
|
const sourceResult = await source;
|
|
868
1961
|
|
|
869
1962
|
if (sourceResult.code !== 0) {
|
|
870
1963
|
// If source failed, return its result
|
|
1964
|
+
traceBranch('ProcessRunner', '_runProgrammaticPipeline', 'SOURCE_FAILED', {
|
|
1965
|
+
code: sourceResult.code,
|
|
1966
|
+
stderr: sourceResult.stderr
|
|
1967
|
+
});
|
|
871
1968
|
return sourceResult;
|
|
872
1969
|
}
|
|
873
1970
|
|
|
874
|
-
//
|
|
875
|
-
destination.
|
|
1971
|
+
// Create a new ProcessRunner with the correct stdin for the destination
|
|
1972
|
+
const destWithStdin = new ProcessRunner(destination.spec, {
|
|
876
1973
|
...destination.options,
|
|
877
1974
|
stdin: sourceResult.stdout
|
|
878
|
-
};
|
|
1975
|
+
});
|
|
879
1976
|
|
|
880
1977
|
// Execute the destination command
|
|
881
|
-
const destResult = await
|
|
1978
|
+
const destResult = await destWithStdin;
|
|
882
1979
|
|
|
1980
|
+
// Debug: Log what destResult looks like
|
|
1981
|
+
trace('ProcessRunner', 'destResult debug', {
|
|
1982
|
+
code: destResult.code,
|
|
1983
|
+
codeType: typeof destResult.code,
|
|
1984
|
+
hasCode: 'code' in destResult,
|
|
1985
|
+
keys: Object.keys(destResult),
|
|
1986
|
+
resultType: typeof destResult,
|
|
1987
|
+
fullResult: JSON.stringify(destResult, null, 2).slice(0, 200)
|
|
1988
|
+
});
|
|
1989
|
+
|
|
883
1990
|
// Return the final result with combined information
|
|
884
1991
|
return createResult({
|
|
885
1992
|
code: destResult.code,
|
|
@@ -902,7 +2009,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
902
2009
|
|
|
903
2010
|
const buf = Buffer.from(result.stderr);
|
|
904
2011
|
if (this.options.mirror) {
|
|
905
|
-
process.stderr
|
|
2012
|
+
safeWrite(process.stderr, buf);
|
|
906
2013
|
}
|
|
907
2014
|
this.emit('stderr', buf);
|
|
908
2015
|
this.emit('data', { type: 'stderr', data: buf });
|
|
@@ -916,13 +2023,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
916
2023
|
|
|
917
2024
|
// Async iteration support
|
|
918
2025
|
async* stream() {
|
|
2026
|
+
traceFunc('ProcessRunner', 'stream', 'ENTER', {
|
|
2027
|
+
started: this.started,
|
|
2028
|
+
finished: this.finished
|
|
2029
|
+
});
|
|
2030
|
+
|
|
919
2031
|
if (!this.started) {
|
|
920
|
-
|
|
2032
|
+
trace('ProcessRunner', 'Auto-starting async process from stream()', {});
|
|
2033
|
+
this._startAsync(); // Start but don't await
|
|
921
2034
|
}
|
|
922
2035
|
|
|
923
2036
|
let buffer = [];
|
|
924
2037
|
let resolve, reject;
|
|
925
2038
|
let ended = false;
|
|
2039
|
+
let cleanedUp = false;
|
|
926
2040
|
|
|
927
2041
|
const onData = (chunk) => {
|
|
928
2042
|
buffer.push(chunk);
|
|
@@ -955,15 +2069,94 @@ class ProcessRunner extends StreamEmitter {
|
|
|
955
2069
|
}
|
|
956
2070
|
}
|
|
957
2071
|
} finally {
|
|
2072
|
+
cleanedUp = true;
|
|
958
2073
|
this.off('data', onData);
|
|
959
2074
|
this.off('end', onEnd);
|
|
2075
|
+
|
|
2076
|
+
// Kill the process if it's still running when iteration is stopped
|
|
2077
|
+
// This happens when breaking from a for-await loop
|
|
2078
|
+
if (!this.finished) {
|
|
2079
|
+
this.kill();
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Kill the running process or cancel virtual command
|
|
2085
|
+
kill() {
|
|
2086
|
+
traceFunc('ProcessRunner', 'kill', 'ENTER', {
|
|
2087
|
+
cancelled: this._cancelled,
|
|
2088
|
+
finished: this.finished,
|
|
2089
|
+
hasChild: !!this.child,
|
|
2090
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
// Mark as cancelled for virtual commands
|
|
2094
|
+
this._cancelled = true;
|
|
2095
|
+
|
|
2096
|
+
// Resolve the cancel promise to break the race in virtual command execution
|
|
2097
|
+
if (this._cancelResolve) {
|
|
2098
|
+
trace('ProcessRunner', 'Resolving cancel promise', {});
|
|
2099
|
+
this._cancelResolve();
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Abort any async operations
|
|
2103
|
+
if (this._abortController) {
|
|
2104
|
+
trace('ProcessRunner', 'Aborting controller', {});
|
|
2105
|
+
this._abortController.abort();
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// If it's a virtual generator, try to close it
|
|
2109
|
+
if (this._virtualGenerator && this._virtualGenerator.return) {
|
|
2110
|
+
trace('ProcessRunner', 'Closing virtual generator', {});
|
|
2111
|
+
try {
|
|
2112
|
+
this._virtualGenerator.return();
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
trace('ProcessRunner', 'Error closing generator', { error: err.message });
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Kill child process if it exists
|
|
2119
|
+
if (this.child && !this.finished) {
|
|
2120
|
+
traceBranch('ProcessRunner', 'hasChild', 'killing', { pid: this.child.pid });
|
|
2121
|
+
try {
|
|
2122
|
+
// Kill the process group to ensure all child processes are terminated
|
|
2123
|
+
if (this.child.pid) {
|
|
2124
|
+
if (isBun) {
|
|
2125
|
+
trace('ProcessRunner', 'Killing Bun process', { pid: this.child.pid });
|
|
2126
|
+
this.child.kill();
|
|
2127
|
+
} else {
|
|
2128
|
+
// In Node.js, kill the process group
|
|
2129
|
+
trace('ProcessRunner', 'Killing Node process group', { pid: this.child.pid });
|
|
2130
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
this.finished = true;
|
|
2134
|
+
} catch (err) {
|
|
2135
|
+
// Process might already be dead
|
|
2136
|
+
trace('ProcessRunner', 'Error killing process', { error: err.message });
|
|
2137
|
+
console.error('Error killing process:', err.message);
|
|
2138
|
+
}
|
|
960
2139
|
}
|
|
2140
|
+
|
|
2141
|
+
// Mark as finished
|
|
2142
|
+
this.finished = true;
|
|
2143
|
+
|
|
2144
|
+
traceFunc('ProcessRunner', 'kill', 'EXIT', {
|
|
2145
|
+
cancelled: this._cancelled,
|
|
2146
|
+
finished: this.finished
|
|
2147
|
+
});
|
|
961
2148
|
}
|
|
962
2149
|
|
|
963
2150
|
// Programmatic piping support
|
|
964
2151
|
pipe(destination) {
|
|
2152
|
+
traceFunc('ProcessRunner', 'pipe', 'ENTER', {
|
|
2153
|
+
hasDestination: !!destination,
|
|
2154
|
+
destinationType: destination?.constructor?.name
|
|
2155
|
+
});
|
|
2156
|
+
|
|
965
2157
|
// If destination is a ProcessRunner, create a pipeline
|
|
966
2158
|
if (destination instanceof ProcessRunner) {
|
|
2159
|
+
traceBranch('ProcessRunner', 'pipe', 'PROCESS_RUNNER_DEST', {});
|
|
967
2160
|
// Create a new ProcessRunner that represents the piped operation
|
|
968
2161
|
const pipeSpec = {
|
|
969
2162
|
mode: 'pipeline',
|
|
@@ -971,49 +2164,64 @@ class ProcessRunner extends StreamEmitter {
|
|
|
971
2164
|
destination: destination
|
|
972
2165
|
};
|
|
973
2166
|
|
|
974
|
-
|
|
2167
|
+
const pipeRunner = new ProcessRunner(pipeSpec, {
|
|
975
2168
|
...this.options,
|
|
976
2169
|
capture: destination.options.capture ?? true
|
|
977
2170
|
});
|
|
2171
|
+
|
|
2172
|
+
traceFunc('ProcessRunner', 'pipe', 'EXIT', { mode: 'pipeline' });
|
|
2173
|
+
return pipeRunner;
|
|
978
2174
|
}
|
|
979
2175
|
|
|
980
2176
|
// If destination is a template literal result (from $`command`), use its spec
|
|
981
2177
|
if (destination && destination.spec) {
|
|
2178
|
+
traceBranch('ProcessRunner', 'pipe', 'TEMPLATE_LITERAL_DEST', {});
|
|
982
2179
|
const destRunner = new ProcessRunner(destination.spec, destination.options);
|
|
983
2180
|
return this.pipe(destRunner);
|
|
984
2181
|
}
|
|
985
2182
|
|
|
2183
|
+
traceBranch('ProcessRunner', 'pipe', 'INVALID_DEST', {});
|
|
986
2184
|
throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
|
|
987
2185
|
}
|
|
988
2186
|
|
|
989
2187
|
// Promise interface (for await)
|
|
990
2188
|
then(onFulfilled, onRejected) {
|
|
991
2189
|
if (!this.promise) {
|
|
992
|
-
this.promise = this.
|
|
2190
|
+
this.promise = this._startAsync();
|
|
993
2191
|
}
|
|
994
2192
|
return this.promise.then(onFulfilled, onRejected);
|
|
995
2193
|
}
|
|
996
2194
|
|
|
997
2195
|
catch(onRejected) {
|
|
998
2196
|
if (!this.promise) {
|
|
999
|
-
this.promise = this.
|
|
2197
|
+
this.promise = this._startAsync();
|
|
1000
2198
|
}
|
|
1001
2199
|
return this.promise.catch(onRejected);
|
|
1002
2200
|
}
|
|
1003
2201
|
|
|
1004
2202
|
finally(onFinally) {
|
|
1005
2203
|
if (!this.promise) {
|
|
1006
|
-
this.promise = this.
|
|
2204
|
+
this.promise = this._startAsync();
|
|
1007
2205
|
}
|
|
1008
2206
|
return this.promise.finally(onFinally);
|
|
1009
2207
|
}
|
|
1010
2208
|
|
|
1011
|
-
//
|
|
1012
|
-
|
|
2209
|
+
// Internal sync execution
|
|
2210
|
+
_startSync() {
|
|
2211
|
+
traceFunc('ProcessRunner', '_startSync', 'ENTER', {
|
|
2212
|
+
started: this.started,
|
|
2213
|
+
spec: this.spec
|
|
2214
|
+
});
|
|
2215
|
+
|
|
1013
2216
|
if (this.started) {
|
|
2217
|
+
traceBranch('ProcessRunner', '_startSync', 'ALREADY_STARTED', {});
|
|
1014
2218
|
throw new Error('Command already started - cannot run sync after async start');
|
|
1015
2219
|
}
|
|
1016
2220
|
|
|
2221
|
+
this.started = true;
|
|
2222
|
+
this._mode = 'sync';
|
|
2223
|
+
trace('ProcessRunner', 'Starting sync execution', { mode: this._mode });
|
|
2224
|
+
|
|
1017
2225
|
const { cwd, env, stdin } = this.options;
|
|
1018
2226
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1019
2227
|
|
|
@@ -1076,8 +2284,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1076
2284
|
|
|
1077
2285
|
// Mirror output if requested (but always capture for result)
|
|
1078
2286
|
if (this.options.mirror) {
|
|
1079
|
-
if (result.stdout) process.stdout
|
|
1080
|
-
if (result.stderr) process.stderr
|
|
2287
|
+
if (result.stdout) safeWrite(process.stdout, result.stdout);
|
|
2288
|
+
if (result.stderr) safeWrite(process.stderr, result.stderr);
|
|
1081
2289
|
}
|
|
1082
2290
|
|
|
1083
2291
|
// Store chunks for events (batched after completion)
|
|
@@ -1132,34 +2340,79 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1132
2340
|
|
|
1133
2341
|
// Public APIs
|
|
1134
2342
|
async function sh(commandString, options = {}) {
|
|
2343
|
+
traceFunc('API', 'sh', 'ENTER', {
|
|
2344
|
+
command: commandString,
|
|
2345
|
+
options
|
|
2346
|
+
});
|
|
2347
|
+
|
|
1135
2348
|
const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
|
|
1136
|
-
|
|
2349
|
+
const result = await runner._startAsync();
|
|
2350
|
+
|
|
2351
|
+
traceFunc('API', 'sh', 'EXIT', { code: result.code });
|
|
2352
|
+
return result;
|
|
1137
2353
|
}
|
|
1138
2354
|
|
|
1139
2355
|
async function exec(file, args = [], options = {}) {
|
|
2356
|
+
traceFunc('API', 'exec', 'ENTER', {
|
|
2357
|
+
file,
|
|
2358
|
+
argsCount: args.length,
|
|
2359
|
+
options
|
|
2360
|
+
});
|
|
2361
|
+
|
|
1140
2362
|
const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
|
|
1141
|
-
|
|
2363
|
+
const result = await runner._startAsync();
|
|
2364
|
+
|
|
2365
|
+
traceFunc('API', 'exec', 'EXIT', { code: result.code });
|
|
2366
|
+
return result;
|
|
1142
2367
|
}
|
|
1143
2368
|
|
|
1144
2369
|
async function run(commandOrTokens, options = {}) {
|
|
2370
|
+
traceFunc('API', 'run', 'ENTER', {
|
|
2371
|
+
type: typeof commandOrTokens,
|
|
2372
|
+
options
|
|
2373
|
+
});
|
|
2374
|
+
|
|
1145
2375
|
if (typeof commandOrTokens === 'string') {
|
|
2376
|
+
traceBranch('API', 'run', 'STRING_COMMAND', { command: commandOrTokens });
|
|
1146
2377
|
return sh(commandOrTokens, { ...options, mirror: false, capture: true });
|
|
1147
2378
|
}
|
|
2379
|
+
|
|
1148
2380
|
const [file, ...args] = commandOrTokens;
|
|
2381
|
+
traceBranch('API', 'run', 'TOKEN_ARRAY', { file, argsCount: args.length });
|
|
1149
2382
|
return exec(file, args, { ...options, mirror: false, capture: true });
|
|
1150
2383
|
}
|
|
1151
2384
|
|
|
1152
2385
|
// Enhanced tagged template that returns ProcessRunner
|
|
1153
2386
|
function $tagged(strings, ...values) {
|
|
2387
|
+
traceFunc('API', '$tagged', 'ENTER', {
|
|
2388
|
+
stringsLength: strings.length,
|
|
2389
|
+
valuesLength: values.length
|
|
2390
|
+
});
|
|
2391
|
+
|
|
1154
2392
|
const cmd = buildShellCommand(strings, values);
|
|
1155
|
-
|
|
2393
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
|
|
2394
|
+
|
|
2395
|
+
traceFunc('API', '$tagged', 'EXIT', { command: cmd });
|
|
2396
|
+
return runner;
|
|
1156
2397
|
}
|
|
1157
2398
|
|
|
1158
2399
|
function create(defaultOptions = {}) {
|
|
2400
|
+
traceFunc('API', 'create', 'ENTER', { defaultOptions });
|
|
2401
|
+
|
|
1159
2402
|
const tagged = (strings, ...values) => {
|
|
2403
|
+
traceFunc('API', 'create.tagged', 'ENTER', {
|
|
2404
|
+
stringsLength: strings.length,
|
|
2405
|
+
valuesLength: values.length
|
|
2406
|
+
});
|
|
2407
|
+
|
|
1160
2408
|
const cmd = buildShellCommand(strings, values);
|
|
1161
|
-
|
|
2409
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
|
|
2410
|
+
|
|
2411
|
+
traceFunc('API', 'create.tagged', 'EXIT', { command: cmd });
|
|
2412
|
+
return runner;
|
|
1162
2413
|
};
|
|
2414
|
+
|
|
2415
|
+
traceFunc('API', 'create', 'EXIT', {});
|
|
1163
2416
|
return tagged;
|
|
1164
2417
|
}
|
|
1165
2418
|
|
|
@@ -1230,12 +2483,21 @@ const shell = {
|
|
|
1230
2483
|
|
|
1231
2484
|
// Virtual command registration API
|
|
1232
2485
|
function register(name, handler) {
|
|
2486
|
+
traceFunc('VirtualCommands', 'register', 'ENTER', { name });
|
|
2487
|
+
|
|
1233
2488
|
virtualCommands.set(name, handler);
|
|
2489
|
+
|
|
2490
|
+
traceFunc('VirtualCommands', 'register', 'EXIT', { registered: true });
|
|
1234
2491
|
return virtualCommands;
|
|
1235
2492
|
}
|
|
1236
2493
|
|
|
1237
2494
|
function unregister(name) {
|
|
1238
|
-
|
|
2495
|
+
traceFunc('VirtualCommands', 'unregister', 'ENTER', { name });
|
|
2496
|
+
|
|
2497
|
+
const deleted = virtualCommands.delete(name);
|
|
2498
|
+
|
|
2499
|
+
traceFunc('VirtualCommands', 'unregister', 'EXIT', { deleted });
|
|
2500
|
+
return deleted;
|
|
1239
2501
|
}
|
|
1240
2502
|
|
|
1241
2503
|
function listCommands() {
|
|
@@ -1255,28 +2517,37 @@ function disableVirtualCommands() {
|
|
|
1255
2517
|
// Built-in commands that match Bun.$ functionality
|
|
1256
2518
|
function registerBuiltins() {
|
|
1257
2519
|
// cd - change directory
|
|
1258
|
-
register('cd', async (args) => {
|
|
2520
|
+
register('cd', async ({ args }) => {
|
|
1259
2521
|
const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
|
|
2522
|
+
trace('VirtualCommand', 'cd: changing directory', { target });
|
|
2523
|
+
|
|
1260
2524
|
try {
|
|
1261
2525
|
process.chdir(target);
|
|
1262
|
-
|
|
2526
|
+
const newDir = process.cwd();
|
|
2527
|
+
trace('VirtualCommand', 'cd: success', { newDir });
|
|
2528
|
+
return { stdout: newDir, code: 0 };
|
|
1263
2529
|
} catch (error) {
|
|
2530
|
+
trace('VirtualCommand', 'cd: failed', { error: error.message });
|
|
1264
2531
|
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
1265
2532
|
}
|
|
1266
2533
|
});
|
|
1267
2534
|
|
|
1268
2535
|
// pwd - print working directory
|
|
1269
|
-
register('pwd', async (args, stdin,
|
|
2536
|
+
register('pwd', async ({ args, stdin, cwd }) => {
|
|
1270
2537
|
// If cwd option is provided, return that instead of process.cwd()
|
|
1271
|
-
const dir =
|
|
2538
|
+
const dir = cwd || process.cwd();
|
|
2539
|
+
trace('VirtualCommand', 'pwd: getting directory', { dir });
|
|
1272
2540
|
return { stdout: dir, code: 0 };
|
|
1273
2541
|
});
|
|
1274
2542
|
|
|
1275
2543
|
// echo - print arguments
|
|
1276
|
-
register('echo', async (args) => {
|
|
2544
|
+
register('echo', async ({ args }) => {
|
|
2545
|
+
trace('VirtualCommand', 'echo: processing', { argsCount: args.length });
|
|
2546
|
+
|
|
1277
2547
|
let output = args.join(' ');
|
|
1278
2548
|
if (args.includes('-n')) {
|
|
1279
2549
|
// Don't add newline
|
|
2550
|
+
traceBranch('VirtualCommand', 'echo', 'NO_NEWLINE', {});
|
|
1280
2551
|
output = args.filter(arg => arg !== '-n').join(' ');
|
|
1281
2552
|
} else {
|
|
1282
2553
|
output += '\n';
|
|
@@ -1285,12 +2556,17 @@ function registerBuiltins() {
|
|
|
1285
2556
|
});
|
|
1286
2557
|
|
|
1287
2558
|
// sleep - wait for specified time
|
|
1288
|
-
register('sleep', async (args) => {
|
|
2559
|
+
register('sleep', async ({ args }) => {
|
|
1289
2560
|
const seconds = parseFloat(args[0] || 0);
|
|
2561
|
+
trace('VirtualCommand', 'sleep: starting', { seconds });
|
|
2562
|
+
|
|
1290
2563
|
if (isNaN(seconds) || seconds < 0) {
|
|
2564
|
+
trace('VirtualCommand', 'sleep: invalid interval', { input: args[0] });
|
|
1291
2565
|
return { stderr: 'sleep: invalid time interval', code: 1 };
|
|
1292
2566
|
}
|
|
2567
|
+
|
|
1293
2568
|
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
2569
|
+
trace('VirtualCommand', 'sleep: completed', { seconds });
|
|
1294
2570
|
return { stdout: '', code: 0 };
|
|
1295
2571
|
});
|
|
1296
2572
|
|
|
@@ -1305,7 +2581,7 @@ function registerBuiltins() {
|
|
|
1305
2581
|
});
|
|
1306
2582
|
|
|
1307
2583
|
// which - locate command
|
|
1308
|
-
register('which', async (args) => {
|
|
2584
|
+
register('which', async ({ args }) => {
|
|
1309
2585
|
if (args.length === 0) {
|
|
1310
2586
|
return { stderr: 'which: missing operand', code: 1 };
|
|
1311
2587
|
}
|
|
@@ -1336,7 +2612,7 @@ function registerBuiltins() {
|
|
|
1336
2612
|
});
|
|
1337
2613
|
|
|
1338
2614
|
// exit - exit with code
|
|
1339
|
-
register('exit', async (args) => {
|
|
2615
|
+
register('exit', async ({ args }) => {
|
|
1340
2616
|
const code = parseInt(args[0] || 0);
|
|
1341
2617
|
if (globalShellSettings.errexit || code !== 0) {
|
|
1342
2618
|
// For virtual commands, we simulate exit by returning the code
|
|
@@ -1346,11 +2622,11 @@ function registerBuiltins() {
|
|
|
1346
2622
|
});
|
|
1347
2623
|
|
|
1348
2624
|
// env - print environment variables
|
|
1349
|
-
register('env', async (args, stdin,
|
|
2625
|
+
register('env', async ({ args, stdin, env }) => {
|
|
1350
2626
|
if (args.length === 0) {
|
|
1351
2627
|
// Use custom env if provided, otherwise use process.env
|
|
1352
|
-
const
|
|
1353
|
-
const output = Object.entries(
|
|
2628
|
+
const envVars = env || process.env;
|
|
2629
|
+
const output = Object.entries(envVars)
|
|
1354
2630
|
.map(([key, value]) => `${key}=${value}`)
|
|
1355
2631
|
.join('\n') + '\n';
|
|
1356
2632
|
return { stdout: output, code: 0 };
|
|
@@ -1361,7 +2637,7 @@ function registerBuiltins() {
|
|
|
1361
2637
|
});
|
|
1362
2638
|
|
|
1363
2639
|
// cat - read and display file contents
|
|
1364
|
-
register('cat', async (args, stdin,
|
|
2640
|
+
register('cat', async ({ args, stdin, cwd }) => {
|
|
1365
2641
|
if (args.length === 0) {
|
|
1366
2642
|
// Read from stdin if no files specified
|
|
1367
2643
|
return { stdout: stdin || '', code: 0 };
|
|
@@ -1378,7 +2654,7 @@ function registerBuiltins() {
|
|
|
1378
2654
|
|
|
1379
2655
|
try {
|
|
1380
2656
|
// Resolve path relative to cwd if provided
|
|
1381
|
-
const basePath =
|
|
2657
|
+
const basePath = cwd || process.cwd();
|
|
1382
2658
|
const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
|
|
1383
2659
|
|
|
1384
2660
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
@@ -1401,7 +2677,7 @@ function registerBuiltins() {
|
|
|
1401
2677
|
});
|
|
1402
2678
|
|
|
1403
2679
|
// ls - list directory contents
|
|
1404
|
-
register('ls', async (args, stdin,
|
|
2680
|
+
register('ls', async ({ args, stdin, cwd }) => {
|
|
1405
2681
|
try {
|
|
1406
2682
|
const fs = await import('fs');
|
|
1407
2683
|
const path = await import('path');
|
|
@@ -1420,7 +2696,7 @@ function registerBuiltins() {
|
|
|
1420
2696
|
|
|
1421
2697
|
for (const targetPath of targetPaths) {
|
|
1422
2698
|
// Resolve path relative to cwd if provided
|
|
1423
|
-
const basePath =
|
|
2699
|
+
const basePath = cwd || process.cwd();
|
|
1424
2700
|
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
|
|
1425
2701
|
|
|
1426
2702
|
try {
|
|
@@ -1475,7 +2751,7 @@ function registerBuiltins() {
|
|
|
1475
2751
|
});
|
|
1476
2752
|
|
|
1477
2753
|
// mkdir - create directories
|
|
1478
|
-
register('mkdir', async (args, stdin,
|
|
2754
|
+
register('mkdir', async ({ args, stdin, cwd }) => {
|
|
1479
2755
|
if (args.length === 0) {
|
|
1480
2756
|
return { stderr: 'mkdir: missing operand', code: 1 };
|
|
1481
2757
|
}
|
|
@@ -1490,7 +2766,7 @@ function registerBuiltins() {
|
|
|
1490
2766
|
|
|
1491
2767
|
for (const dir of dirs) {
|
|
1492
2768
|
try {
|
|
1493
|
-
const basePath =
|
|
2769
|
+
const basePath = cwd || process.cwd();
|
|
1494
2770
|
const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
|
|
1495
2771
|
|
|
1496
2772
|
if (recursive) {
|
|
@@ -1513,7 +2789,7 @@ function registerBuiltins() {
|
|
|
1513
2789
|
});
|
|
1514
2790
|
|
|
1515
2791
|
// rm - remove files and directories
|
|
1516
|
-
register('rm', async (args, stdin,
|
|
2792
|
+
register('rm', async ({ args, stdin, cwd }) => {
|
|
1517
2793
|
if (args.length === 0) {
|
|
1518
2794
|
return { stderr: 'rm: missing operand', code: 1 };
|
|
1519
2795
|
}
|
|
@@ -1529,7 +2805,7 @@ function registerBuiltins() {
|
|
|
1529
2805
|
|
|
1530
2806
|
for (const target of targets) {
|
|
1531
2807
|
try {
|
|
1532
|
-
const basePath =
|
|
2808
|
+
const basePath = cwd || process.cwd();
|
|
1533
2809
|
const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
|
|
1534
2810
|
|
|
1535
2811
|
const stat = fs.statSync(fullPath);
|
|
@@ -1562,7 +2838,7 @@ function registerBuiltins() {
|
|
|
1562
2838
|
});
|
|
1563
2839
|
|
|
1564
2840
|
// mv - move/rename files and directories
|
|
1565
|
-
register('mv', async (args, stdin,
|
|
2841
|
+
register('mv', async ({ args, stdin, cwd }) => {
|
|
1566
2842
|
if (args.length < 2) {
|
|
1567
2843
|
return { stderr: 'mv: missing destination file operand', code: 1 };
|
|
1568
2844
|
}
|
|
@@ -1571,7 +2847,7 @@ function registerBuiltins() {
|
|
|
1571
2847
|
const fs = await import('fs');
|
|
1572
2848
|
const path = await import('path');
|
|
1573
2849
|
|
|
1574
|
-
const basePath =
|
|
2850
|
+
const basePath = cwd || process.cwd();
|
|
1575
2851
|
|
|
1576
2852
|
if (args.length === 2) {
|
|
1577
2853
|
// Simple rename/move
|
|
@@ -1643,7 +2919,7 @@ function registerBuiltins() {
|
|
|
1643
2919
|
});
|
|
1644
2920
|
|
|
1645
2921
|
// cp - copy files and directories
|
|
1646
|
-
register('cp', async (args, stdin,
|
|
2922
|
+
register('cp', async ({ args, stdin, cwd }) => {
|
|
1647
2923
|
if (args.length < 2) {
|
|
1648
2924
|
return { stderr: 'cp: missing destination file operand', code: 1 };
|
|
1649
2925
|
}
|
|
@@ -1656,7 +2932,7 @@ function registerBuiltins() {
|
|
|
1656
2932
|
const paths = args.filter(arg => !arg.startsWith('-'));
|
|
1657
2933
|
const recursive = flags.includes('-r') || flags.includes('-R');
|
|
1658
2934
|
|
|
1659
|
-
const basePath =
|
|
2935
|
+
const basePath = cwd || process.cwd();
|
|
1660
2936
|
|
|
1661
2937
|
if (paths.length === 2) {
|
|
1662
2938
|
// Simple copy
|
|
@@ -1740,7 +3016,7 @@ function registerBuiltins() {
|
|
|
1740
3016
|
});
|
|
1741
3017
|
|
|
1742
3018
|
// touch - create or update file timestamps
|
|
1743
|
-
register('touch', async (args, stdin,
|
|
3019
|
+
register('touch', async ({ args, stdin, cwd }) => {
|
|
1744
3020
|
if (args.length === 0) {
|
|
1745
3021
|
return { stderr: 'touch: missing file operand', code: 1 };
|
|
1746
3022
|
}
|
|
@@ -1749,7 +3025,7 @@ function registerBuiltins() {
|
|
|
1749
3025
|
const fs = await import('fs');
|
|
1750
3026
|
const path = await import('path');
|
|
1751
3027
|
|
|
1752
|
-
const basePath =
|
|
3028
|
+
const basePath = cwd || process.cwd();
|
|
1753
3029
|
|
|
1754
3030
|
for (const file of args) {
|
|
1755
3031
|
try {
|
|
@@ -1778,7 +3054,7 @@ function registerBuiltins() {
|
|
|
1778
3054
|
});
|
|
1779
3055
|
|
|
1780
3056
|
// basename - extract filename from path
|
|
1781
|
-
register('basename', async (args) => {
|
|
3057
|
+
register('basename', async ({ args }) => {
|
|
1782
3058
|
if (args.length === 0) {
|
|
1783
3059
|
return { stderr: 'basename: missing operand', code: 1 };
|
|
1784
3060
|
}
|
|
@@ -1803,7 +3079,7 @@ function registerBuiltins() {
|
|
|
1803
3079
|
});
|
|
1804
3080
|
|
|
1805
3081
|
// dirname - extract directory from path
|
|
1806
|
-
register('dirname', async (args) => {
|
|
3082
|
+
register('dirname', async ({ args }) => {
|
|
1807
3083
|
if (args.length === 0) {
|
|
1808
3084
|
return { stderr: 'dirname: missing operand', code: 1 };
|
|
1809
3085
|
}
|
|
@@ -1821,19 +3097,52 @@ function registerBuiltins() {
|
|
|
1821
3097
|
});
|
|
1822
3098
|
|
|
1823
3099
|
// yes - output a string repeatedly
|
|
1824
|
-
register('yes', async function* (args) {
|
|
3100
|
+
register('yes', async function* ({ args, stdin, isCancelled, signal, ...rest }) {
|
|
1825
3101
|
const output = args.length > 0 ? args.join(' ') : 'y';
|
|
3102
|
+
trace('VirtualCommand', 'yes: starting infinite generator', { output });
|
|
1826
3103
|
|
|
1827
3104
|
// Generate infinite stream of the output
|
|
1828
3105
|
while (true) {
|
|
3106
|
+
// Check if cancelled via function or abort signal
|
|
3107
|
+
if (isCancelled && isCancelled()) {
|
|
3108
|
+
trace('VirtualCommand', 'yes: cancelled via function', {});
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
if (signal && signal.aborted) {
|
|
3112
|
+
trace('VirtualCommand', 'yes: cancelled via abort signal', {});
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
1829
3116
|
yield output + '\n';
|
|
1830
|
-
|
|
1831
|
-
|
|
3117
|
+
|
|
3118
|
+
// Small delay with abort signal support
|
|
3119
|
+
try {
|
|
3120
|
+
await new Promise((resolve, reject) => {
|
|
3121
|
+
const timeout = setTimeout(resolve, 0);
|
|
3122
|
+
|
|
3123
|
+
// Listen for abort signal if available
|
|
3124
|
+
if (signal) {
|
|
3125
|
+
const abortHandler = () => {
|
|
3126
|
+
clearTimeout(timeout);
|
|
3127
|
+
reject(new Error('Aborted'));
|
|
3128
|
+
};
|
|
3129
|
+
|
|
3130
|
+
if (signal.aborted) {
|
|
3131
|
+
abortHandler();
|
|
3132
|
+
} else {
|
|
3133
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
// Aborted
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
1832
3141
|
}
|
|
1833
3142
|
});
|
|
1834
3143
|
|
|
1835
3144
|
// seq - generate sequence of numbers
|
|
1836
|
-
register('seq', async (args) => {
|
|
3145
|
+
register('seq', async ({ args }) => {
|
|
1837
3146
|
if (args.length === 0) {
|
|
1838
3147
|
return { stderr: 'seq: missing operand', code: 1 };
|
|
1839
3148
|
}
|
|
@@ -1881,7 +3190,7 @@ function registerBuiltins() {
|
|
|
1881
3190
|
});
|
|
1882
3191
|
|
|
1883
3192
|
// test - test file conditions (basic implementation)
|
|
1884
|
-
register('test', async (args) => {
|
|
3193
|
+
register('test', async ({ args }) => {
|
|
1885
3194
|
if (args.length === 0) {
|
|
1886
3195
|
return { stdout: '', code: 1 };
|
|
1887
3196
|
}
|