command-stream 0.0.5 → 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 +1134 -91
- 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,
|
|
@@ -375,11 +493,19 @@ class ProcessRunner extends StreamEmitter {
|
|
|
375
493
|
}
|
|
376
494
|
|
|
377
495
|
async _runVirtual(cmd, args) {
|
|
496
|
+
traceFunc('ProcessRunner', '_runVirtual', 'ENTER', { cmd, args });
|
|
497
|
+
|
|
378
498
|
const handler = virtualCommands.get(cmd);
|
|
379
499
|
if (!handler) {
|
|
500
|
+
trace('ProcessRunner', 'Virtual command not found', { cmd });
|
|
380
501
|
throw new Error(`Virtual command not found: ${cmd}`);
|
|
381
502
|
}
|
|
382
503
|
|
|
504
|
+
trace('ProcessRunner', 'Found virtual command handler', {
|
|
505
|
+
cmd,
|
|
506
|
+
isGenerator: handler.constructor.name === 'AsyncGeneratorFunction'
|
|
507
|
+
});
|
|
508
|
+
|
|
383
509
|
try {
|
|
384
510
|
// Prepare stdin
|
|
385
511
|
let stdinData = '';
|
|
@@ -405,18 +531,65 @@ class ProcessRunner extends StreamEmitter {
|
|
|
405
531
|
|
|
406
532
|
// Check if handler is async generator (streaming)
|
|
407
533
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
408
|
-
// Handle streaming virtual command
|
|
534
|
+
// Handle streaming virtual command with cancellation support
|
|
409
535
|
const chunks = [];
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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;
|
|
413
555
|
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
}
|
|
416
588
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.
|
|
589
|
+
} finally {
|
|
590
|
+
// Clean up
|
|
591
|
+
this._virtualGenerator = null;
|
|
592
|
+
this._cancelResolve = null;
|
|
420
593
|
}
|
|
421
594
|
|
|
422
595
|
result = {
|
|
@@ -509,11 +682,627 @@ class ProcessRunner extends StreamEmitter {
|
|
|
509
682
|
}
|
|
510
683
|
}
|
|
511
684
|
|
|
512
|
-
async
|
|
513
|
-
|
|
514
|
-
|
|
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;
|
|
515
877
|
}
|
|
878
|
+
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
516
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
|
+
}
|
|
1280
|
+
}
|
|
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;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
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)
|
|
517
1306
|
let currentOutput = '';
|
|
518
1307
|
let currentInput = '';
|
|
519
1308
|
|
|
@@ -531,6 +1320,11 @@ class ProcessRunner extends StreamEmitter {
|
|
|
531
1320
|
|
|
532
1321
|
// Check if this is a virtual command (only if virtual commands are enabled)
|
|
533
1322
|
if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
|
|
1323
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'VIRTUAL_COMMAND', {
|
|
1324
|
+
cmd,
|
|
1325
|
+
argsCount: args.length
|
|
1326
|
+
});
|
|
1327
|
+
|
|
534
1328
|
// Run virtual command with current input
|
|
535
1329
|
const handler = virtualCommands.get(cmd);
|
|
536
1330
|
|
|
@@ -550,6 +1344,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
550
1344
|
|
|
551
1345
|
// Check if handler is async generator (streaming)
|
|
552
1346
|
if (handler.constructor.name === 'AsyncGeneratorFunction') {
|
|
1347
|
+
traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'ASYNC_GENERATOR', { cmd });
|
|
553
1348
|
const chunks = [];
|
|
554
1349
|
for await (const chunk of handler(argValues, currentInput, this.options)) {
|
|
555
1350
|
chunks.push(Buffer.from(chunk));
|
|
@@ -703,49 +1498,73 @@ class ProcessRunner extends StreamEmitter {
|
|
|
703
1498
|
console.log(commandStr);
|
|
704
1499
|
}
|
|
705
1500
|
|
|
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) => {
|
|
1501
|
+
// Execute the system command with current input as stdin (ASYNC VERSION)
|
|
1502
|
+
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
718
1503
|
const require = createRequire(import.meta.url);
|
|
719
1504
|
const cp = require('child_process');
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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();
|
|
726
1554
|
});
|
|
727
1555
|
};
|
|
728
1556
|
|
|
729
1557
|
// Execute using shell to handle complex commands
|
|
730
1558
|
const argv = ['sh', '-c', commandStr];
|
|
731
|
-
const
|
|
1559
|
+
const isLastCommand = (i === commands.length - 1);
|
|
1560
|
+
const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
|
|
732
1561
|
|
|
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
|
-
}
|
|
1562
|
+
let result = {
|
|
1563
|
+
code: proc.status || 0,
|
|
1564
|
+
stdout: proc.stdout || '',
|
|
1565
|
+
stderr: proc.stderr || '',
|
|
1566
|
+
stdin: currentInput
|
|
1567
|
+
};
|
|
749
1568
|
|
|
750
1569
|
// If command failed and pipefail is set, fail the entire pipeline
|
|
751
1570
|
if (globalShellSettings.pipefail && result.code !== 0) {
|
|
@@ -765,7 +1584,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
765
1584
|
this.errChunks.push(Buffer.from(result.stderr));
|
|
766
1585
|
}
|
|
767
1586
|
} else {
|
|
768
|
-
// This is the last command -
|
|
1587
|
+
// This is the last command - store final result (streaming already handled during execution)
|
|
769
1588
|
currentOutput = result.stdout;
|
|
770
1589
|
|
|
771
1590
|
// Collect all accumulated stderr
|
|
@@ -777,25 +1596,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
777
1596
|
allStderr += result.stderr;
|
|
778
1597
|
}
|
|
779
1598
|
|
|
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
1599
|
// Store final result using createResult helper for .text() method compatibility
|
|
800
1600
|
const finalResult = createResult({
|
|
801
1601
|
code: result.code,
|
|
@@ -860,26 +1660,65 @@ class ProcessRunner extends StreamEmitter {
|
|
|
860
1660
|
}
|
|
861
1661
|
}
|
|
862
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
|
+
|
|
863
1685
|
// Run programmatic pipeline (.pipe() method)
|
|
864
1686
|
async _runProgrammaticPipeline(source, destination) {
|
|
1687
|
+
traceFunc('ProcessRunner', '_runProgrammaticPipeline', 'ENTER', {});
|
|
1688
|
+
|
|
865
1689
|
try {
|
|
866
1690
|
// Execute the source command first
|
|
1691
|
+
trace('ProcessRunner', 'Executing source command', {});
|
|
867
1692
|
const sourceResult = await source;
|
|
868
1693
|
|
|
869
1694
|
if (sourceResult.code !== 0) {
|
|
870
1695
|
// If source failed, return its result
|
|
1696
|
+
traceBranch('ProcessRunner', '_runProgrammaticPipeline', 'SOURCE_FAILED', {
|
|
1697
|
+
code: sourceResult.code,
|
|
1698
|
+
stderr: sourceResult.stderr
|
|
1699
|
+
});
|
|
871
1700
|
return sourceResult;
|
|
872
1701
|
}
|
|
873
1702
|
|
|
874
|
-
//
|
|
875
|
-
destination.
|
|
1703
|
+
// Create a new ProcessRunner with the correct stdin for the destination
|
|
1704
|
+
const destWithStdin = new ProcessRunner(destination.spec, {
|
|
876
1705
|
...destination.options,
|
|
877
1706
|
stdin: sourceResult.stdout
|
|
878
|
-
};
|
|
1707
|
+
});
|
|
879
1708
|
|
|
880
1709
|
// Execute the destination command
|
|
881
|
-
const destResult = await
|
|
1710
|
+
const destResult = await destWithStdin;
|
|
882
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
|
+
|
|
883
1722
|
// Return the final result with combined information
|
|
884
1723
|
return createResult({
|
|
885
1724
|
code: destResult.code,
|
|
@@ -916,13 +1755,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
916
1755
|
|
|
917
1756
|
// Async iteration support
|
|
918
1757
|
async* stream() {
|
|
1758
|
+
traceFunc('ProcessRunner', 'stream', 'ENTER', {
|
|
1759
|
+
started: this.started,
|
|
1760
|
+
finished: this.finished
|
|
1761
|
+
});
|
|
1762
|
+
|
|
919
1763
|
if (!this.started) {
|
|
920
|
-
|
|
1764
|
+
trace('ProcessRunner', 'Auto-starting async process from stream()', {});
|
|
1765
|
+
this._startAsync(); // Start but don't await
|
|
921
1766
|
}
|
|
922
1767
|
|
|
923
1768
|
let buffer = [];
|
|
924
1769
|
let resolve, reject;
|
|
925
1770
|
let ended = false;
|
|
1771
|
+
let cleanedUp = false;
|
|
926
1772
|
|
|
927
1773
|
const onData = (chunk) => {
|
|
928
1774
|
buffer.push(chunk);
|
|
@@ -955,15 +1801,94 @@ class ProcessRunner extends StreamEmitter {
|
|
|
955
1801
|
}
|
|
956
1802
|
}
|
|
957
1803
|
} finally {
|
|
1804
|
+
cleanedUp = true;
|
|
958
1805
|
this.off('data', onData);
|
|
959
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
|
+
}
|
|
960
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
|
+
});
|
|
961
1880
|
}
|
|
962
1881
|
|
|
963
1882
|
// Programmatic piping support
|
|
964
1883
|
pipe(destination) {
|
|
1884
|
+
traceFunc('ProcessRunner', 'pipe', 'ENTER', {
|
|
1885
|
+
hasDestination: !!destination,
|
|
1886
|
+
destinationType: destination?.constructor?.name
|
|
1887
|
+
});
|
|
1888
|
+
|
|
965
1889
|
// If destination is a ProcessRunner, create a pipeline
|
|
966
1890
|
if (destination instanceof ProcessRunner) {
|
|
1891
|
+
traceBranch('ProcessRunner', 'pipe', 'PROCESS_RUNNER_DEST', {});
|
|
967
1892
|
// Create a new ProcessRunner that represents the piped operation
|
|
968
1893
|
const pipeSpec = {
|
|
969
1894
|
mode: 'pipeline',
|
|
@@ -971,49 +1896,64 @@ class ProcessRunner extends StreamEmitter {
|
|
|
971
1896
|
destination: destination
|
|
972
1897
|
};
|
|
973
1898
|
|
|
974
|
-
|
|
1899
|
+
const pipeRunner = new ProcessRunner(pipeSpec, {
|
|
975
1900
|
...this.options,
|
|
976
1901
|
capture: destination.options.capture ?? true
|
|
977
1902
|
});
|
|
1903
|
+
|
|
1904
|
+
traceFunc('ProcessRunner', 'pipe', 'EXIT', { mode: 'pipeline' });
|
|
1905
|
+
return pipeRunner;
|
|
978
1906
|
}
|
|
979
1907
|
|
|
980
1908
|
// If destination is a template literal result (from $`command`), use its spec
|
|
981
1909
|
if (destination && destination.spec) {
|
|
1910
|
+
traceBranch('ProcessRunner', 'pipe', 'TEMPLATE_LITERAL_DEST', {});
|
|
982
1911
|
const destRunner = new ProcessRunner(destination.spec, destination.options);
|
|
983
1912
|
return this.pipe(destRunner);
|
|
984
1913
|
}
|
|
985
1914
|
|
|
1915
|
+
traceBranch('ProcessRunner', 'pipe', 'INVALID_DEST', {});
|
|
986
1916
|
throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
|
|
987
1917
|
}
|
|
988
1918
|
|
|
989
1919
|
// Promise interface (for await)
|
|
990
1920
|
then(onFulfilled, onRejected) {
|
|
991
1921
|
if (!this.promise) {
|
|
992
|
-
this.promise = this.
|
|
1922
|
+
this.promise = this._startAsync();
|
|
993
1923
|
}
|
|
994
1924
|
return this.promise.then(onFulfilled, onRejected);
|
|
995
1925
|
}
|
|
996
1926
|
|
|
997
1927
|
catch(onRejected) {
|
|
998
1928
|
if (!this.promise) {
|
|
999
|
-
this.promise = this.
|
|
1929
|
+
this.promise = this._startAsync();
|
|
1000
1930
|
}
|
|
1001
1931
|
return this.promise.catch(onRejected);
|
|
1002
1932
|
}
|
|
1003
1933
|
|
|
1004
1934
|
finally(onFinally) {
|
|
1005
1935
|
if (!this.promise) {
|
|
1006
|
-
this.promise = this.
|
|
1936
|
+
this.promise = this._startAsync();
|
|
1007
1937
|
}
|
|
1008
1938
|
return this.promise.finally(onFinally);
|
|
1009
1939
|
}
|
|
1010
1940
|
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1941
|
+
// Internal sync execution
|
|
1942
|
+
_startSync() {
|
|
1943
|
+
traceFunc('ProcessRunner', '_startSync', 'ENTER', {
|
|
1944
|
+
started: this.started,
|
|
1945
|
+
spec: this.spec
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1013
1948
|
if (this.started) {
|
|
1949
|
+
traceBranch('ProcessRunner', '_startSync', 'ALREADY_STARTED', {});
|
|
1014
1950
|
throw new Error('Command already started - cannot run sync after async start');
|
|
1015
1951
|
}
|
|
1016
1952
|
|
|
1953
|
+
this.started = true;
|
|
1954
|
+
this._mode = 'sync';
|
|
1955
|
+
trace('ProcessRunner', 'Starting sync execution', { mode: this._mode });
|
|
1956
|
+
|
|
1017
1957
|
const { cwd, env, stdin } = this.options;
|
|
1018
1958
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1019
1959
|
|
|
@@ -1132,34 +2072,79 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1132
2072
|
|
|
1133
2073
|
// Public APIs
|
|
1134
2074
|
async function sh(commandString, options = {}) {
|
|
2075
|
+
traceFunc('API', 'sh', 'ENTER', {
|
|
2076
|
+
command: commandString,
|
|
2077
|
+
options
|
|
2078
|
+
});
|
|
2079
|
+
|
|
1135
2080
|
const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
|
|
1136
|
-
|
|
2081
|
+
const result = await runner._startAsync();
|
|
2082
|
+
|
|
2083
|
+
traceFunc('API', 'sh', 'EXIT', { code: result.code });
|
|
2084
|
+
return result;
|
|
1137
2085
|
}
|
|
1138
2086
|
|
|
1139
2087
|
async function exec(file, args = [], options = {}) {
|
|
2088
|
+
traceFunc('API', 'exec', 'ENTER', {
|
|
2089
|
+
file,
|
|
2090
|
+
argsCount: args.length,
|
|
2091
|
+
options
|
|
2092
|
+
});
|
|
2093
|
+
|
|
1140
2094
|
const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
|
|
1141
|
-
|
|
2095
|
+
const result = await runner._startAsync();
|
|
2096
|
+
|
|
2097
|
+
traceFunc('API', 'exec', 'EXIT', { code: result.code });
|
|
2098
|
+
return result;
|
|
1142
2099
|
}
|
|
1143
2100
|
|
|
1144
2101
|
async function run(commandOrTokens, options = {}) {
|
|
2102
|
+
traceFunc('API', 'run', 'ENTER', {
|
|
2103
|
+
type: typeof commandOrTokens,
|
|
2104
|
+
options
|
|
2105
|
+
});
|
|
2106
|
+
|
|
1145
2107
|
if (typeof commandOrTokens === 'string') {
|
|
2108
|
+
traceBranch('API', 'run', 'STRING_COMMAND', { command: commandOrTokens });
|
|
1146
2109
|
return sh(commandOrTokens, { ...options, mirror: false, capture: true });
|
|
1147
2110
|
}
|
|
2111
|
+
|
|
1148
2112
|
const [file, ...args] = commandOrTokens;
|
|
2113
|
+
traceBranch('API', 'run', 'TOKEN_ARRAY', { file, argsCount: args.length });
|
|
1149
2114
|
return exec(file, args, { ...options, mirror: false, capture: true });
|
|
1150
2115
|
}
|
|
1151
2116
|
|
|
1152
2117
|
// Enhanced tagged template that returns ProcessRunner
|
|
1153
2118
|
function $tagged(strings, ...values) {
|
|
2119
|
+
traceFunc('API', '$tagged', 'ENTER', {
|
|
2120
|
+
stringsLength: strings.length,
|
|
2121
|
+
valuesLength: values.length
|
|
2122
|
+
});
|
|
2123
|
+
|
|
1154
2124
|
const cmd = buildShellCommand(strings, values);
|
|
1155
|
-
|
|
2125
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
|
|
2126
|
+
|
|
2127
|
+
traceFunc('API', '$tagged', 'EXIT', { command: cmd });
|
|
2128
|
+
return runner;
|
|
1156
2129
|
}
|
|
1157
2130
|
|
|
1158
2131
|
function create(defaultOptions = {}) {
|
|
2132
|
+
traceFunc('API', 'create', 'ENTER', { defaultOptions });
|
|
2133
|
+
|
|
1159
2134
|
const tagged = (strings, ...values) => {
|
|
2135
|
+
traceFunc('API', 'create.tagged', 'ENTER', {
|
|
2136
|
+
stringsLength: strings.length,
|
|
2137
|
+
valuesLength: values.length
|
|
2138
|
+
});
|
|
2139
|
+
|
|
1160
2140
|
const cmd = buildShellCommand(strings, values);
|
|
1161
|
-
|
|
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;
|
|
1162
2145
|
};
|
|
2146
|
+
|
|
2147
|
+
traceFunc('API', 'create', 'EXIT', {});
|
|
1163
2148
|
return tagged;
|
|
1164
2149
|
}
|
|
1165
2150
|
|
|
@@ -1230,12 +2215,21 @@ const shell = {
|
|
|
1230
2215
|
|
|
1231
2216
|
// Virtual command registration API
|
|
1232
2217
|
function register(name, handler) {
|
|
2218
|
+
traceFunc('VirtualCommands', 'register', 'ENTER', { name });
|
|
2219
|
+
|
|
1233
2220
|
virtualCommands.set(name, handler);
|
|
2221
|
+
|
|
2222
|
+
traceFunc('VirtualCommands', 'register', 'EXIT', { registered: true });
|
|
1234
2223
|
return virtualCommands;
|
|
1235
2224
|
}
|
|
1236
2225
|
|
|
1237
2226
|
function unregister(name) {
|
|
1238
|
-
|
|
2227
|
+
traceFunc('VirtualCommands', 'unregister', 'ENTER', { name });
|
|
2228
|
+
|
|
2229
|
+
const deleted = virtualCommands.delete(name);
|
|
2230
|
+
|
|
2231
|
+
traceFunc('VirtualCommands', 'unregister', 'EXIT', { deleted });
|
|
2232
|
+
return deleted;
|
|
1239
2233
|
}
|
|
1240
2234
|
|
|
1241
2235
|
function listCommands() {
|
|
@@ -1257,10 +2251,15 @@ function registerBuiltins() {
|
|
|
1257
2251
|
// cd - change directory
|
|
1258
2252
|
register('cd', async (args) => {
|
|
1259
2253
|
const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
|
|
2254
|
+
trace('VirtualCommand', 'cd: changing directory', { target });
|
|
2255
|
+
|
|
1260
2256
|
try {
|
|
1261
2257
|
process.chdir(target);
|
|
1262
|
-
|
|
2258
|
+
const newDir = process.cwd();
|
|
2259
|
+
trace('VirtualCommand', 'cd: success', { newDir });
|
|
2260
|
+
return { stdout: newDir, code: 0 };
|
|
1263
2261
|
} catch (error) {
|
|
2262
|
+
trace('VirtualCommand', 'cd: failed', { error: error.message });
|
|
1264
2263
|
return { stderr: `cd: ${error.message}`, code: 1 };
|
|
1265
2264
|
}
|
|
1266
2265
|
});
|
|
@@ -1269,14 +2268,18 @@ function registerBuiltins() {
|
|
|
1269
2268
|
register('pwd', async (args, stdin, options) => {
|
|
1270
2269
|
// If cwd option is provided, return that instead of process.cwd()
|
|
1271
2270
|
const dir = options?.cwd || process.cwd();
|
|
2271
|
+
trace('VirtualCommand', 'pwd: getting directory', { dir });
|
|
1272
2272
|
return { stdout: dir, code: 0 };
|
|
1273
2273
|
});
|
|
1274
2274
|
|
|
1275
2275
|
// echo - print arguments
|
|
1276
2276
|
register('echo', async (args) => {
|
|
2277
|
+
trace('VirtualCommand', 'echo: processing', { argsCount: args.length });
|
|
2278
|
+
|
|
1277
2279
|
let output = args.join(' ');
|
|
1278
2280
|
if (args.includes('-n')) {
|
|
1279
2281
|
// Don't add newline
|
|
2282
|
+
traceBranch('VirtualCommand', 'echo', 'NO_NEWLINE', {});
|
|
1280
2283
|
output = args.filter(arg => arg !== '-n').join(' ');
|
|
1281
2284
|
} else {
|
|
1282
2285
|
output += '\n';
|
|
@@ -1287,10 +2290,15 @@ function registerBuiltins() {
|
|
|
1287
2290
|
// sleep - wait for specified time
|
|
1288
2291
|
register('sleep', async (args) => {
|
|
1289
2292
|
const seconds = parseFloat(args[0] || 0);
|
|
2293
|
+
trace('VirtualCommand', 'sleep: starting', { seconds });
|
|
2294
|
+
|
|
1290
2295
|
if (isNaN(seconds) || seconds < 0) {
|
|
2296
|
+
trace('VirtualCommand', 'sleep: invalid interval', { input: args[0] });
|
|
1291
2297
|
return { stderr: 'sleep: invalid time interval', code: 1 };
|
|
1292
2298
|
}
|
|
2299
|
+
|
|
1293
2300
|
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
2301
|
+
trace('VirtualCommand', 'sleep: completed', { seconds });
|
|
1294
2302
|
return { stdout: '', code: 0 };
|
|
1295
2303
|
});
|
|
1296
2304
|
|
|
@@ -1821,14 +2829,49 @@ function registerBuiltins() {
|
|
|
1821
2829
|
});
|
|
1822
2830
|
|
|
1823
2831
|
// yes - output a string repeatedly
|
|
1824
|
-
register('yes', async function* (args) {
|
|
2832
|
+
register('yes', async function* (args, stdin, options) {
|
|
1825
2833
|
const output = args.length > 0 ? args.join(' ') : 'y';
|
|
2834
|
+
trace('VirtualCommand', 'yes: starting infinite generator', { output });
|
|
1826
2835
|
|
|
1827
2836
|
// Generate infinite stream of the output
|
|
1828
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
|
+
|
|
1829
2850
|
yield output + '\n';
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
+
}
|
|
1832
2875
|
}
|
|
1833
2876
|
});
|
|
1834
2877
|
|