command-stream 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/js/src/$.ansi.mjs +147 -0
  2. package/js/src/$.mjs +49 -6382
  3. package/js/src/$.process-runner-base.mjs +563 -0
  4. package/js/src/$.process-runner-execution.mjs +1497 -0
  5. package/js/src/$.process-runner-orchestration.mjs +250 -0
  6. package/js/src/$.process-runner-pipeline.mjs +1162 -0
  7. package/js/src/$.process-runner-stream-kill.mjs +312 -0
  8. package/js/src/$.process-runner-virtual.mjs +297 -0
  9. package/js/src/$.quote.mjs +161 -0
  10. package/js/src/$.result.mjs +23 -0
  11. package/js/src/$.shell-settings.mjs +84 -0
  12. package/js/src/$.shell.mjs +157 -0
  13. package/js/src/$.state.mjs +401 -0
  14. package/js/src/$.stream-emitter.mjs +111 -0
  15. package/js/src/$.stream-utils.mjs +390 -0
  16. package/js/src/$.trace.mjs +36 -0
  17. package/js/src/$.utils.mjs +2 -23
  18. package/js/src/$.virtual-commands.mjs +113 -0
  19. package/js/src/commands/$.which.mjs +3 -1
  20. package/js/src/commands/index.mjs +24 -0
  21. package/js/src/shell-parser.mjs +125 -83
  22. package/js/tests/resource-cleanup-internals.test.mjs +22 -24
  23. package/js/tests/sigint-cleanup.test.mjs +3 -0
  24. package/package.json +1 -1
  25. package/rust/src/ansi.rs +194 -0
  26. package/rust/src/events.rs +305 -0
  27. package/rust/src/lib.rs +71 -60
  28. package/rust/src/macros.rs +165 -0
  29. package/rust/src/pipeline.rs +411 -0
  30. package/rust/src/quote.rs +161 -0
  31. package/rust/src/state.rs +333 -0
  32. package/rust/src/stream.rs +369 -0
  33. package/rust/src/trace.rs +152 -0
  34. package/rust/src/utils.rs +53 -158
  35. package/rust/tests/events.rs +207 -0
  36. package/rust/tests/macros.rs +77 -0
  37. package/rust/tests/pipeline.rs +93 -0
  38. package/rust/tests/state.rs +207 -0
  39. package/rust/tests/stream.rs +102 -0
@@ -0,0 +1,1162 @@
1
+ // ProcessRunner pipeline methods - all pipeline execution strategies
2
+ // Part of the modular ProcessRunner architecture
3
+
4
+ import cp from 'child_process';
5
+ import { trace } from './$.trace.mjs';
6
+ import { findAvailableShell } from './$.shell.mjs';
7
+ import { StreamUtils, safeWrite } from './$.stream-utils.mjs';
8
+ import { createResult } from './$.result.mjs';
9
+
10
+ const isBun = typeof globalThis.Bun !== 'undefined';
11
+
12
+ /**
13
+ * Commands that need streaming workaround
14
+ */
15
+ const STREAMING_COMMANDS = ['jq', 'grep', 'sed', 'cat', 'awk'];
16
+
17
+ /**
18
+ * Check if command needs streaming workaround
19
+ * @param {object} command - Command object
20
+ * @returns {boolean}
21
+ */
22
+ function needsStreamingWorkaround(command) {
23
+ return STREAMING_COMMANDS.includes(command.cmd);
24
+ }
25
+
26
+ /**
27
+ * Analyze pipeline for virtual commands
28
+ * @param {Array} commands - Pipeline commands
29
+ * @param {Function} isVirtualCommandsEnabled - Check if virtual commands enabled
30
+ * @param {Map} virtualCommands - Virtual commands registry
31
+ * @returns {object} Analysis result
32
+ */
33
+ function analyzePipeline(commands, isVirtualCommandsEnabled, virtualCommands) {
34
+ const pipelineInfo = commands.map((command) => ({
35
+ ...command,
36
+ isVirtual: isVirtualCommandsEnabled() && virtualCommands.has(command.cmd),
37
+ }));
38
+ return {
39
+ pipelineInfo,
40
+ hasVirtual: pipelineInfo.some((info) => info.isVirtual),
41
+ virtualCount: pipelineInfo.filter((p) => p.isVirtual).length,
42
+ realCount: pipelineInfo.filter((p) => !p.isVirtual).length,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Read stream to string
48
+ * @param {ReadableStream} stream - Stream to read
49
+ * @returns {Promise<string>}
50
+ */
51
+ async function readStreamToString(stream) {
52
+ const reader = stream.getReader();
53
+ let result = '';
54
+ try {
55
+ let done = false;
56
+ while (!done) {
57
+ const readResult = await reader.read();
58
+ done = readResult.done;
59
+ if (!done && readResult.value) {
60
+ result += new TextDecoder().decode(readResult.value);
61
+ }
62
+ }
63
+ } finally {
64
+ reader.releaseLock();
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Build command parts from command object
71
+ * @param {object} command - Command with cmd and args
72
+ * @returns {string[]} Command parts array
73
+ */
74
+ function buildCommandParts(command) {
75
+ const { cmd, args } = command;
76
+ const parts = [cmd];
77
+ for (const arg of args) {
78
+ if (arg.value !== undefined) {
79
+ if (arg.quoted) {
80
+ parts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
81
+ } else if (arg.value.includes(' ')) {
82
+ parts.push(`"${arg.value}"`);
83
+ } else {
84
+ parts.push(arg.value);
85
+ }
86
+ } else if (
87
+ typeof arg === 'string' &&
88
+ arg.includes(' ') &&
89
+ !arg.startsWith('"') &&
90
+ !arg.startsWith("'")
91
+ ) {
92
+ parts.push(`"${arg}"`);
93
+ } else {
94
+ parts.push(arg);
95
+ }
96
+ }
97
+ return parts;
98
+ }
99
+
100
+ /**
101
+ * Check if command string needs shell execution
102
+ * @param {string} commandStr - Command string
103
+ * @returns {boolean}
104
+ */
105
+ function needsShellExecution(commandStr) {
106
+ return (
107
+ commandStr.includes('*') ||
108
+ commandStr.includes('$') ||
109
+ commandStr.includes('>') ||
110
+ commandStr.includes('<') ||
111
+ commandStr.includes('&&') ||
112
+ commandStr.includes('||') ||
113
+ commandStr.includes(';') ||
114
+ commandStr.includes('`')
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Get spawn args based on shell need
120
+ * @param {boolean} needsShell - Whether shell is needed
121
+ * @param {string} cmd - Command name
122
+ * @param {Array} args - Command args
123
+ * @param {string} commandStr - Full command string
124
+ * @returns {string[]} Spawn arguments
125
+ */
126
+ function getSpawnArgs(needsShell, cmd, args, commandStr) {
127
+ if (needsShell) {
128
+ const shell = findAvailableShell();
129
+ return [shell.cmd, ...shell.args.filter((arg) => arg !== '-l'), commandStr];
130
+ }
131
+ return [cmd, ...args.map((a) => (a.value !== undefined ? a.value : a))];
132
+ }
133
+
134
+ /**
135
+ * Determine stdin configuration for first command
136
+ * @param {object} options - Runner options
137
+ * @returns {object} Stdin config with stdin, needsManualStdin, stdinData
138
+ */
139
+ function getFirstCommandStdin(options) {
140
+ if (options.stdin && typeof options.stdin === 'string') {
141
+ return {
142
+ stdin: 'pipe',
143
+ needsManualStdin: true,
144
+ stdinData: Buffer.from(options.stdin),
145
+ };
146
+ }
147
+ if (options.stdin && Buffer.isBuffer(options.stdin)) {
148
+ return { stdin: 'pipe', needsManualStdin: true, stdinData: options.stdin };
149
+ }
150
+ return { stdin: 'ignore', needsManualStdin: false, stdinData: null };
151
+ }
152
+
153
+ /**
154
+ * Get stdin string from options
155
+ * @param {object} options - Runner options
156
+ * @returns {string}
157
+ */
158
+ function getStdinString(options) {
159
+ if (options.stdin && typeof options.stdin === 'string') {
160
+ return options.stdin;
161
+ }
162
+ if (options.stdin && Buffer.isBuffer(options.stdin)) {
163
+ return options.stdin.toString('utf8');
164
+ }
165
+ return '';
166
+ }
167
+
168
+ /**
169
+ * Handle pipefail check
170
+ * @param {number[]} exitCodes - Exit codes from pipeline
171
+ * @param {object} shellSettings - Shell settings
172
+ */
173
+ function checkPipefail(exitCodes, shellSettings) {
174
+ if (shellSettings.pipefail) {
175
+ const failedIndex = exitCodes.findIndex((code) => code !== 0);
176
+ if (failedIndex !== -1) {
177
+ const error = new Error(
178
+ `Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`
179
+ );
180
+ error.code = exitCodes[failedIndex];
181
+ throw error;
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Create and throw errexit error
188
+ * @param {object} result - Result object
189
+ * @param {object} shellSettings - Shell settings
190
+ */
191
+ function throwErrexitError(result, shellSettings) {
192
+ if (shellSettings.errexit && result.code !== 0) {
193
+ const error = new Error(`Pipeline failed with exit code ${result.code}`);
194
+ error.code = result.code;
195
+ error.stdout = result.stdout;
196
+ error.stderr = result.stderr;
197
+ error.result = result;
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Write stdin to Bun process
204
+ * @param {object} proc - Process with stdin
205
+ * @param {Buffer} stdinData - Data to write
206
+ */
207
+ async function writeBunStdin(proc, stdinData) {
208
+ if (!proc.stdin) {
209
+ return;
210
+ }
211
+ const stdinHandler = StreamUtils.setupStdinHandling(
212
+ proc.stdin,
213
+ 'Bun process stdin'
214
+ );
215
+ try {
216
+ if (stdinHandler.isWritable()) {
217
+ await proc.stdin.write(stdinData);
218
+ await proc.stdin.end();
219
+ }
220
+ } catch (e) {
221
+ if (e.code !== 'EPIPE') {
222
+ trace('ProcessRunner', () => `stdin write error | ${e.message}`);
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Collect stderr from process async
229
+ * @param {object} runner - ProcessRunner
230
+ * @param {object} proc - Process
231
+ * @param {boolean} isLast - Is last command
232
+ * @param {object} collector - Object to collect stderr
233
+ */
234
+ function collectStderrAsync(runner, proc, isLast, collector) {
235
+ (async () => {
236
+ for await (const chunk of proc.stderr) {
237
+ const buf = Buffer.from(chunk);
238
+ collector.stderr += buf.toString();
239
+ if (isLast) {
240
+ if (runner.options.mirror) {
241
+ safeWrite(process.stderr, buf);
242
+ }
243
+ runner._emitProcessedData('stderr', buf);
244
+ }
245
+ }
246
+ })();
247
+ }
248
+
249
+ /**
250
+ * Create initial input stream from stdin option
251
+ * @param {object} options - Runner options
252
+ * @returns {ReadableStream|null}
253
+ */
254
+ function createInitialInputStream(options) {
255
+ if (!options.stdin) {
256
+ return null;
257
+ }
258
+ const inputData =
259
+ typeof options.stdin === 'string'
260
+ ? options.stdin
261
+ : options.stdin.toString('utf8');
262
+ return new ReadableStream({
263
+ start(controller) {
264
+ controller.enqueue(new TextEncoder().encode(inputData));
265
+ controller.close();
266
+ },
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Get argument values from args array
272
+ * @param {Array} args - Args array
273
+ * @returns {Array} Argument values
274
+ */
275
+ function getArgValues(args) {
276
+ return args.map((arg) => (arg.value !== undefined ? arg.value : arg));
277
+ }
278
+
279
+ /**
280
+ * Create readable stream from string
281
+ * @param {string} data - String data
282
+ * @returns {ReadableStream}
283
+ */
284
+ function createStringStream(data) {
285
+ return new ReadableStream({
286
+ start(controller) {
287
+ controller.enqueue(new TextEncoder().encode(data));
288
+ controller.close();
289
+ },
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Pipe stream to process stdin
295
+ * @param {ReadableStream} stream - Input stream
296
+ * @param {object} proc - Process
297
+ */
298
+ function pipeStreamToProcess(stream, proc) {
299
+ if (!stream || !proc.stdin) {
300
+ return;
301
+ }
302
+ const reader = stream.getReader();
303
+ const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
304
+
305
+ (async () => {
306
+ try {
307
+ while (true) {
308
+ const { done, value } = await reader.read();
309
+ if (done) {
310
+ break;
311
+ }
312
+ if (writer.write) {
313
+ try {
314
+ await writer.write(value);
315
+ } catch (error) {
316
+ StreamUtils.handleStreamError(error, 'stream writer', false);
317
+ break;
318
+ }
319
+ } else if (writer.getWriter) {
320
+ try {
321
+ const w = writer.getWriter();
322
+ await w.write(value);
323
+ w.releaseLock();
324
+ } catch (error) {
325
+ StreamUtils.handleStreamError(
326
+ error,
327
+ 'stream writer (getWriter)',
328
+ false
329
+ );
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ } finally {
335
+ reader.releaseLock();
336
+ if (writer.close) {
337
+ await writer.close();
338
+ } else if (writer.end) {
339
+ writer.end();
340
+ }
341
+ }
342
+ })();
343
+ }
344
+
345
+ /**
346
+ * Spawn shell command in Bun
347
+ * @param {string} commandStr - Command string
348
+ * @param {object} options - Options (cwd, env, stdin)
349
+ * @returns {object} Process
350
+ */
351
+ function spawnShellCommand(commandStr, options) {
352
+ const shell = findAvailableShell();
353
+ return Bun.spawn(
354
+ [shell.cmd, ...shell.args.filter((arg) => arg !== '-l'), commandStr],
355
+ {
356
+ cwd: options.cwd,
357
+ env: options.env,
358
+ stdin: options.stdin,
359
+ stdout: 'pipe',
360
+ stderr: 'pipe',
361
+ }
362
+ );
363
+ }
364
+
365
+ /**
366
+ * Collect last command stdout
367
+ * @param {object} runner - ProcessRunner
368
+ * @param {object} proc - Process
369
+ * @returns {Promise<string>} Output string
370
+ */
371
+ async function collectFinalStdout(runner, proc) {
372
+ const chunks = [];
373
+ for await (const chunk of proc.stdout) {
374
+ const buf = Buffer.from(chunk);
375
+ chunks.push(buf);
376
+ if (runner.options.mirror) {
377
+ safeWrite(process.stdout, buf);
378
+ }
379
+ runner._emitProcessedData('stdout', buf);
380
+ }
381
+ return Buffer.concat(chunks).toString('utf8');
382
+ }
383
+
384
+ /**
385
+ * Spawn async node process for pipeline
386
+ * @param {object} runner - ProcessRunner instance
387
+ * @param {string[]} argv - Command arguments
388
+ * @param {string} stdin - Stdin input
389
+ * @param {boolean} isLastCommand - Is this the last command
390
+ * @returns {Promise<object>} Result with status, stdout, stderr
391
+ */
392
+ function spawnNodeAsync(runner, argv, stdin, isLastCommand) {
393
+ return new Promise((resolve, reject) => {
394
+ const proc = cp.spawn(argv[0], argv.slice(1), {
395
+ cwd: runner.options.cwd,
396
+ env: runner.options.env,
397
+ stdio: ['pipe', 'pipe', 'pipe'],
398
+ });
399
+
400
+ let stdout = '';
401
+ let stderr = '';
402
+
403
+ proc.stdout.on('data', (chunk) => {
404
+ stdout += chunk.toString();
405
+ if (isLastCommand) {
406
+ if (runner.options.mirror) {
407
+ safeWrite(process.stdout, chunk);
408
+ }
409
+ runner._emitProcessedData('stdout', chunk);
410
+ }
411
+ });
412
+
413
+ proc.stderr.on('data', (chunk) => {
414
+ stderr += chunk.toString();
415
+ if (isLastCommand) {
416
+ if (runner.options.mirror) {
417
+ safeWrite(process.stderr, chunk);
418
+ }
419
+ runner._emitProcessedData('stderr', chunk);
420
+ }
421
+ });
422
+
423
+ proc.on('close', (code) => resolve({ status: code, stdout, stderr }));
424
+ proc.on('error', reject);
425
+
426
+ if (proc.stdin) {
427
+ StreamUtils.addStdinErrorHandler(
428
+ proc.stdin,
429
+ 'spawnNodeAsync stdin',
430
+ reject
431
+ );
432
+ }
433
+ if (stdin) {
434
+ StreamUtils.safeStreamWrite(proc.stdin, stdin, 'spawnNodeAsync stdin');
435
+ }
436
+ StreamUtils.safeStreamEnd(proc.stdin, 'spawnNodeAsync stdin');
437
+ });
438
+ }
439
+
440
+ /**
441
+ * Log shell trace/verbose
442
+ * @param {object} settings - Shell settings
443
+ * @param {string} cmd - Command
444
+ * @param {string[]} argValues - Argument values
445
+ */
446
+ function logShellTrace(settings, cmd, argValues) {
447
+ const cmdStr = `${cmd} ${argValues.join(' ')}`;
448
+ if (settings.xtrace) {
449
+ console.log(`+ ${cmdStr}`);
450
+ }
451
+ if (settings.verbose) {
452
+ console.log(cmdStr);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Handle virtual command in non-streaming pipeline
458
+ * @param {object} runner - ProcessRunner instance
459
+ * @param {Function} handler - Handler function
460
+ * @param {string[]} argValues - Argument values
461
+ * @param {string} currentInput - Current input
462
+ * @param {object} options - Runner options
463
+ * @returns {Promise<object>} Result
464
+ */
465
+ async function runVirtualHandler(
466
+ runner,
467
+ handler,
468
+ argValues,
469
+ currentInput,
470
+ options
471
+ ) {
472
+ if (handler.constructor.name === 'AsyncGeneratorFunction') {
473
+ const chunks = [];
474
+ for await (const chunk of handler({
475
+ args: argValues,
476
+ stdin: currentInput,
477
+ ...options,
478
+ })) {
479
+ chunks.push(Buffer.from(chunk));
480
+ }
481
+ return {
482
+ code: 0,
483
+ stdout: options.capture
484
+ ? Buffer.concat(chunks).toString('utf8')
485
+ : undefined,
486
+ stderr: options.capture ? '' : undefined,
487
+ stdin: options.capture ? currentInput : undefined,
488
+ };
489
+ }
490
+ const result = await handler({
491
+ args: argValues,
492
+ stdin: currentInput,
493
+ ...options,
494
+ });
495
+ return {
496
+ ...result,
497
+ code: result.code ?? 0,
498
+ stdout: options.capture ? (result.stdout ?? '') : undefined,
499
+ stderr: options.capture ? (result.stderr ?? '') : undefined,
500
+ stdin: options.capture ? currentInput : undefined,
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Emit final result output
506
+ * @param {object} runner - ProcessRunner instance
507
+ * @param {object} result - Result object
508
+ */
509
+ function emitFinalOutput(runner, result) {
510
+ if (result.stdout) {
511
+ const buf = Buffer.from(result.stdout);
512
+ if (runner.options.mirror) {
513
+ safeWrite(process.stdout, buf);
514
+ }
515
+ runner._emitProcessedData('stdout', buf);
516
+ }
517
+ if (result.stderr) {
518
+ const buf = Buffer.from(result.stderr);
519
+ if (runner.options.mirror) {
520
+ safeWrite(process.stderr, buf);
521
+ }
522
+ runner._emitProcessedData('stderr', buf);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Create final result for pipeline
528
+ * @param {object} runner - ProcessRunner instance
529
+ * @param {object} result - Current result
530
+ * @param {string} currentOutput - Current output
531
+ * @param {object} shellSettings - Shell settings
532
+ * @returns {object} Final result
533
+ */
534
+ function createFinalPipelineResult(
535
+ runner,
536
+ result,
537
+ currentOutput,
538
+ shellSettings
539
+ ) {
540
+ const finalResult = createResult({
541
+ code: result.code,
542
+ stdout: currentOutput,
543
+ stderr: result.stderr,
544
+ stdin: getStdinString(runner.options),
545
+ });
546
+ runner.finish(finalResult);
547
+ throwErrexitError(finalResult, shellSettings);
548
+ return finalResult;
549
+ }
550
+
551
+ /**
552
+ * Handle pipeline error
553
+ * @param {object} runner - ProcessRunner instance
554
+ * @param {Error} error - Error
555
+ * @param {string} currentOutput - Current output
556
+ * @param {object} shellSettings - Shell settings
557
+ * @returns {object} Error result
558
+ */
559
+ function handlePipelineError(runner, error, currentOutput, shellSettings) {
560
+ const result = createResult({
561
+ code: error.code ?? 1,
562
+ stdout: currentOutput,
563
+ stderr: error.stderr ?? error.message,
564
+ stdin: getStdinString(runner.options),
565
+ });
566
+ if (result.stderr) {
567
+ const buf = Buffer.from(result.stderr);
568
+ if (runner.options.mirror) {
569
+ safeWrite(process.stderr, buf);
570
+ }
571
+ runner._emitProcessedData('stderr', buf);
572
+ }
573
+ runner.finish(result);
574
+ if (shellSettings.errexit) {
575
+ throw error;
576
+ }
577
+ return result;
578
+ }
579
+
580
+ /**
581
+ * Handle virtual command in non-streaming pipeline iteration
582
+ * @param {object} runner - ProcessRunner instance
583
+ * @param {object} command - Command object
584
+ * @param {string} currentInput - Current input
585
+ * @param {boolean} isLastCommand - Is last command
586
+ * @param {object} deps - Dependencies
587
+ * @returns {Promise<object>} { output, input, finalResult }
588
+ */
589
+ async function handleVirtualPipelineCommand(
590
+ runner,
591
+ command,
592
+ currentInput,
593
+ isLastCommand,
594
+ deps
595
+ ) {
596
+ const { virtualCommands, globalShellSettings } = deps;
597
+ const handler = virtualCommands.get(command.cmd);
598
+ const argValues = getArgValues(command.args);
599
+ logShellTrace(globalShellSettings, command.cmd, argValues);
600
+
601
+ const result = await runVirtualHandler(
602
+ runner,
603
+ handler,
604
+ argValues,
605
+ currentInput,
606
+ runner.options
607
+ );
608
+
609
+ if (isLastCommand) {
610
+ emitFinalOutput(runner, result);
611
+ return {
612
+ finalResult: createFinalPipelineResult(
613
+ runner,
614
+ result,
615
+ result.stdout,
616
+ globalShellSettings
617
+ ),
618
+ };
619
+ }
620
+
621
+ if (globalShellSettings.errexit && result.code !== 0) {
622
+ const error = new Error(
623
+ `Pipeline command failed with exit code ${result.code}`
624
+ );
625
+ error.code = result.code;
626
+ error.result = result;
627
+ throw error;
628
+ }
629
+
630
+ return { input: result.stdout };
631
+ }
632
+
633
+ /**
634
+ * Handle shell command in non-streaming pipeline iteration
635
+ * @param {object} runner - ProcessRunner instance
636
+ * @param {object} command - Command object
637
+ * @param {string} currentInput - Current input
638
+ * @param {boolean} isLastCommand - Is last command
639
+ * @param {object} deps - Dependencies
640
+ * @returns {Promise<object>} { output, input, finalResult }
641
+ */
642
+ async function handleShellPipelineCommand(
643
+ runner,
644
+ command,
645
+ currentInput,
646
+ isLastCommand,
647
+ deps
648
+ ) {
649
+ const { globalShellSettings } = deps;
650
+ const commandStr = buildCommandParts(command).join(' ');
651
+ logShellTrace(globalShellSettings, commandStr, []);
652
+
653
+ const shell = findAvailableShell();
654
+ const argv = [shell.cmd, ...shell.args.filter((a) => a !== '-l'), commandStr];
655
+ const proc = await spawnNodeAsync(runner, argv, currentInput, isLastCommand);
656
+ const result = {
657
+ code: proc.status || 0,
658
+ stdout: proc.stdout || '',
659
+ stderr: proc.stderr || '',
660
+ };
661
+
662
+ if (globalShellSettings.pipefail && result.code !== 0) {
663
+ const error = new Error(
664
+ `Pipeline command '${commandStr}' failed with exit code ${result.code}`
665
+ );
666
+ error.code = result.code;
667
+ throw error;
668
+ }
669
+
670
+ if (isLastCommand) {
671
+ let allStderr = '';
672
+ if (runner.errChunks?.length > 0) {
673
+ allStderr = Buffer.concat(runner.errChunks).toString('utf8');
674
+ }
675
+ if (result.stderr) {
676
+ allStderr += result.stderr;
677
+ }
678
+ const finalResult = createResult({
679
+ code: result.code,
680
+ stdout: result.stdout,
681
+ stderr: allStderr,
682
+ stdin: getStdinString(runner.options),
683
+ });
684
+ runner.finish(finalResult);
685
+ throwErrexitError(finalResult, globalShellSettings);
686
+ return { finalResult };
687
+ }
688
+
689
+ if (result.stderr && runner.options.capture) {
690
+ runner.errChunks = runner.errChunks || [];
691
+ runner.errChunks.push(Buffer.from(result.stderr));
692
+ }
693
+
694
+ return { input: result.stdout };
695
+ }
696
+
697
+ /**
698
+ * Attach pipeline methods to ProcessRunner prototype
699
+ * @param {Function} ProcessRunner - The ProcessRunner class
700
+ * @param {Object} deps - Dependencies
701
+ */
702
+ export function attachPipelineMethods(ProcessRunner, deps) {
703
+ const { virtualCommands, globalShellSettings, isVirtualCommandsEnabled } =
704
+ deps;
705
+
706
+ // Use module-level helper
707
+ ProcessRunner.prototype._readStreamToString = readStreamToString;
708
+
709
+ ProcessRunner.prototype._runStreamingPipelineBun = async function (commands) {
710
+ trace(
711
+ 'ProcessRunner',
712
+ () => `_runStreamingPipelineBun | cmds=${commands.length}`
713
+ );
714
+
715
+ const analysis = analyzePipeline(
716
+ commands,
717
+ isVirtualCommandsEnabled,
718
+ virtualCommands
719
+ );
720
+ if (analysis.hasVirtual) {
721
+ return this._runMixedStreamingPipeline(commands);
722
+ }
723
+ if (commands.some(needsStreamingWorkaround)) {
724
+ return this._runTeeStreamingPipeline(commands);
725
+ }
726
+
727
+ const processes = [];
728
+ const collector = { stderr: '' };
729
+
730
+ for (let i = 0; i < commands.length; i++) {
731
+ const command = commands[i];
732
+ const commandStr = buildCommandParts(command).join(' ');
733
+ const needsShell = needsShellExecution(commandStr);
734
+ const spawnArgs = getSpawnArgs(
735
+ needsShell,
736
+ command.cmd,
737
+ command.args,
738
+ commandStr
739
+ );
740
+
741
+ let stdin;
742
+ let stdinConfig = null;
743
+ if (i === 0) {
744
+ stdinConfig = getFirstCommandStdin(this.options);
745
+ stdin = stdinConfig.stdin;
746
+ } else {
747
+ stdin = processes[i - 1].stdout;
748
+ }
749
+
750
+ const proc = Bun.spawn(spawnArgs, {
751
+ cwd: this.options.cwd,
752
+ env: this.options.env,
753
+ stdin,
754
+ stdout: 'pipe',
755
+ stderr: 'pipe',
756
+ });
757
+
758
+ if (stdinConfig?.needsManualStdin && stdinConfig.stdinData) {
759
+ writeBunStdin(proc, stdinConfig.stdinData);
760
+ }
761
+
762
+ processes.push(proc);
763
+ collectStderrAsync(this, proc, i === commands.length - 1, collector);
764
+ }
765
+
766
+ const lastProc = processes[processes.length - 1];
767
+ let finalOutput = '';
768
+
769
+ for await (const chunk of lastProc.stdout) {
770
+ const buf = Buffer.from(chunk);
771
+ finalOutput += buf.toString();
772
+ if (this.options.mirror) {
773
+ safeWrite(process.stdout, buf);
774
+ }
775
+ this._emitProcessedData('stdout', buf);
776
+ }
777
+
778
+ const exitCodes = await Promise.all(processes.map((p) => p.exited));
779
+ checkPipefail(exitCodes, globalShellSettings);
780
+
781
+ const result = createResult({
782
+ code: exitCodes[exitCodes.length - 1] || 0,
783
+ stdout: finalOutput,
784
+ stderr: collector.stderr,
785
+ stdin: getStdinString(this.options),
786
+ });
787
+
788
+ this.finish(result);
789
+ throwErrexitError(result, globalShellSettings);
790
+ return result;
791
+ };
792
+
793
+ ProcessRunner.prototype._runTeeStreamingPipeline = async function (commands) {
794
+ trace(
795
+ 'ProcessRunner',
796
+ () => `_runTeeStreamingPipeline | cmds=${commands.length}`
797
+ );
798
+
799
+ const processes = [];
800
+ const collector = { stderr: '' };
801
+ let currentStream = null;
802
+
803
+ for (let i = 0; i < commands.length; i++) {
804
+ const command = commands[i];
805
+ const commandStr = buildCommandParts(command).join(' ');
806
+ const needsShell = needsShellExecution(commandStr);
807
+ const spawnArgs = getSpawnArgs(
808
+ needsShell,
809
+ command.cmd,
810
+ command.args,
811
+ commandStr
812
+ );
813
+
814
+ let stdin;
815
+ let stdinConfig = null;
816
+ if (i === 0) {
817
+ stdinConfig = getFirstCommandStdin(this.options);
818
+ stdin = stdinConfig.stdin;
819
+ } else {
820
+ stdin = currentStream;
821
+ }
822
+
823
+ const proc = Bun.spawn(spawnArgs, {
824
+ cwd: this.options.cwd,
825
+ env: this.options.env,
826
+ stdin,
827
+ stdout: 'pipe',
828
+ stderr: 'pipe',
829
+ });
830
+
831
+ if (
832
+ stdinConfig?.needsManualStdin &&
833
+ stdinConfig.stdinData &&
834
+ proc.stdin
835
+ ) {
836
+ await writeBunStdin(proc, stdinConfig.stdinData);
837
+ }
838
+
839
+ processes.push(proc);
840
+
841
+ if (i < commands.length - 1) {
842
+ const [readStream, pipeStream] = proc.stdout.tee();
843
+ currentStream = pipeStream;
844
+ (async () => {
845
+ for await (const _chunk of readStream) {
846
+ /* consume */
847
+ }
848
+ })();
849
+ } else {
850
+ currentStream = proc.stdout;
851
+ }
852
+
853
+ collectStderrAsync(this, proc, i === commands.length - 1, collector);
854
+ }
855
+
856
+ const lastProc = processes[processes.length - 1];
857
+ let finalOutput = '';
858
+
859
+ for await (const chunk of lastProc.stdout) {
860
+ const buf = Buffer.from(chunk);
861
+ finalOutput += buf.toString();
862
+ if (this.options.mirror) {
863
+ safeWrite(process.stdout, buf);
864
+ }
865
+ this._emitProcessedData('stdout', buf);
866
+ }
867
+
868
+ const exitCodes = await Promise.all(processes.map((p) => p.exited));
869
+ checkPipefail(exitCodes, globalShellSettings);
870
+
871
+ const result = createResult({
872
+ code: exitCodes[exitCodes.length - 1] || 0,
873
+ stdout: finalOutput,
874
+ stderr: collector.stderr,
875
+ stdin: getStdinString(this.options),
876
+ });
877
+
878
+ this.finish(result);
879
+ throwErrexitError(result, globalShellSettings);
880
+ return result;
881
+ };
882
+
883
+ ProcessRunner.prototype._runMixedStreamingPipeline = async function (
884
+ commands
885
+ ) {
886
+ trace(
887
+ 'ProcessRunner',
888
+ () => `_runMixedStreamingPipeline | cmds=${commands.length}`
889
+ );
890
+
891
+ let currentInputStream = createInitialInputStream(this.options);
892
+ let finalOutput = '';
893
+ const collector = { stderr: '' };
894
+
895
+ for (let i = 0; i < commands.length; i++) {
896
+ const command = commands[i];
897
+ const { cmd, args } = command;
898
+ const isLastCommand = i === commands.length - 1;
899
+
900
+ if (isVirtualCommandsEnabled() && virtualCommands.has(cmd)) {
901
+ const handler = virtualCommands.get(cmd);
902
+ const argValues = getArgValues(args);
903
+ const inputData = currentInputStream
904
+ ? await this._readStreamToString(currentInputStream)
905
+ : '';
906
+
907
+ if (handler.constructor.name === 'AsyncGeneratorFunction') {
908
+ const chunks = [];
909
+ const self = this;
910
+ currentInputStream = new ReadableStream({
911
+ async start(controller) {
912
+ const { stdin: _, ...opts } = self.options;
913
+ for await (const chunk of handler({
914
+ args: argValues,
915
+ stdin: inputData,
916
+ ...opts,
917
+ })) {
918
+ const data = Buffer.from(chunk);
919
+ controller.enqueue(data);
920
+ if (isLastCommand) {
921
+ chunks.push(data);
922
+ if (self.options.mirror) {
923
+ safeWrite(process.stdout, data);
924
+ }
925
+ self.emit('stdout', data);
926
+ self.emit('data', { type: 'stdout', data });
927
+ }
928
+ }
929
+ controller.close();
930
+ if (isLastCommand) {
931
+ finalOutput = Buffer.concat(chunks).toString('utf8');
932
+ }
933
+ },
934
+ });
935
+ } else {
936
+ const { stdin: _, ...opts } = this.options;
937
+ const result = await handler({
938
+ args: argValues,
939
+ stdin: inputData,
940
+ ...opts,
941
+ });
942
+ const outputData = result.stdout || '';
943
+ if (isLastCommand) {
944
+ finalOutput = outputData;
945
+ const buf = Buffer.from(outputData);
946
+ if (this.options.mirror) {
947
+ safeWrite(process.stdout, buf);
948
+ }
949
+ this._emitProcessedData('stdout', buf);
950
+ }
951
+ currentInputStream = createStringStream(outputData);
952
+ if (result.stderr) {
953
+ collector.stderr += result.stderr;
954
+ }
955
+ }
956
+ } else {
957
+ const commandStr = buildCommandParts(command).join(' ');
958
+ const proc = spawnShellCommand(commandStr, {
959
+ cwd: this.options.cwd,
960
+ env: this.options.env,
961
+ stdin: currentInputStream ? 'pipe' : 'ignore',
962
+ });
963
+
964
+ pipeStreamToProcess(currentInputStream, proc);
965
+ currentInputStream = proc.stdout;
966
+ collectStderrAsync(this, proc, isLastCommand, collector);
967
+
968
+ if (isLastCommand) {
969
+ finalOutput = await collectFinalStdout(this, proc);
970
+ await proc.exited;
971
+ }
972
+ }
973
+ }
974
+
975
+ const result = createResult({
976
+ code: 0,
977
+ stdout: finalOutput,
978
+ stderr: collector.stderr,
979
+ stdin: getStdinString(this.options),
980
+ });
981
+
982
+ this.finish(result);
983
+ return result;
984
+ };
985
+
986
+ ProcessRunner.prototype._runPipelineNonStreaming = async function (commands) {
987
+ trace(
988
+ 'ProcessRunner',
989
+ () => `_runPipelineNonStreaming | cmds=${commands.length}`
990
+ );
991
+
992
+ const currentOutput = '';
993
+ let currentInput = getStdinString(this.options);
994
+ const pipelineDeps = { virtualCommands, globalShellSettings };
995
+
996
+ for (let i = 0; i < commands.length; i++) {
997
+ const command = commands[i];
998
+ const isLastCommand = i === commands.length - 1;
999
+ const isVirtual =
1000
+ isVirtualCommandsEnabled() && virtualCommands.has(command.cmd);
1001
+
1002
+ try {
1003
+ const handleResult = isVirtual
1004
+ ? await handleVirtualPipelineCommand(
1005
+ this,
1006
+ command,
1007
+ currentInput,
1008
+ isLastCommand,
1009
+ pipelineDeps
1010
+ )
1011
+ : await handleShellPipelineCommand(
1012
+ this,
1013
+ command,
1014
+ currentInput,
1015
+ isLastCommand,
1016
+ pipelineDeps
1017
+ );
1018
+
1019
+ if (handleResult.finalResult) {
1020
+ return handleResult.finalResult;
1021
+ }
1022
+ currentInput = handleResult.input;
1023
+ } catch (error) {
1024
+ return handlePipelineError(
1025
+ this,
1026
+ error,
1027
+ currentOutput,
1028
+ globalShellSettings
1029
+ );
1030
+ }
1031
+ }
1032
+ };
1033
+
1034
+ ProcessRunner.prototype._runPipeline = function (commands) {
1035
+ trace(
1036
+ 'ProcessRunner',
1037
+ () =>
1038
+ `_runPipeline ENTER | ${JSON.stringify(
1039
+ {
1040
+ commandsCount: commands.length,
1041
+ },
1042
+ null,
1043
+ 2
1044
+ )}`
1045
+ );
1046
+
1047
+ if (commands.length === 0) {
1048
+ trace(
1049
+ 'ProcessRunner',
1050
+ () =>
1051
+ `BRANCH: _runPipeline => NO_COMMANDS | ${JSON.stringify({}, null, 2)}`
1052
+ );
1053
+ return createResult({
1054
+ code: 1,
1055
+ stdout: '',
1056
+ stderr: 'No commands in pipeline',
1057
+ stdin: '',
1058
+ });
1059
+ }
1060
+
1061
+ if (isBun) {
1062
+ trace(
1063
+ 'ProcessRunner',
1064
+ () =>
1065
+ `BRANCH: _runPipeline => BUN_STREAMING | ${JSON.stringify({}, null, 2)}`
1066
+ );
1067
+ return this._runStreamingPipelineBun(commands);
1068
+ }
1069
+
1070
+ trace(
1071
+ 'ProcessRunner',
1072
+ () =>
1073
+ `BRANCH: _runPipeline => NODE_NON_STREAMING | ${JSON.stringify({}, null, 2)}`
1074
+ );
1075
+ return this._runPipelineNonStreaming(commands);
1076
+ };
1077
+
1078
+ ProcessRunner.prototype._runProgrammaticPipeline = async function (
1079
+ source,
1080
+ destination
1081
+ ) {
1082
+ trace(
1083
+ 'ProcessRunner',
1084
+ () => `_runProgrammaticPipeline ENTER | ${JSON.stringify({}, null, 2)}`
1085
+ );
1086
+
1087
+ try {
1088
+ trace('ProcessRunner', () => 'Executing source command');
1089
+ const sourceResult = await source;
1090
+
1091
+ if (sourceResult.code !== 0) {
1092
+ trace(
1093
+ 'ProcessRunner',
1094
+ () =>
1095
+ `BRANCH: _runProgrammaticPipeline => SOURCE_FAILED | ${JSON.stringify(
1096
+ {
1097
+ code: sourceResult.code,
1098
+ stderr: sourceResult.stderr,
1099
+ },
1100
+ null,
1101
+ 2
1102
+ )}`
1103
+ );
1104
+ return sourceResult;
1105
+ }
1106
+
1107
+ const ProcessRunnerRef = this.constructor;
1108
+ const destWithStdin = new ProcessRunnerRef(destination.spec, {
1109
+ ...destination.options,
1110
+ stdin: sourceResult.stdout,
1111
+ });
1112
+
1113
+ const destResult = await destWithStdin;
1114
+
1115
+ trace(
1116
+ 'ProcessRunner',
1117
+ () =>
1118
+ `destResult debug | ${JSON.stringify(
1119
+ {
1120
+ code: destResult.code,
1121
+ codeType: typeof destResult.code,
1122
+ hasCode: 'code' in destResult,
1123
+ keys: Object.keys(destResult),
1124
+ resultType: typeof destResult,
1125
+ fullResult: JSON.stringify(destResult, null, 2).slice(0, 200),
1126
+ },
1127
+ null,
1128
+ 2
1129
+ )}`
1130
+ );
1131
+
1132
+ return createResult({
1133
+ code: destResult.code,
1134
+ stdout: destResult.stdout,
1135
+ stderr: sourceResult.stderr + destResult.stderr,
1136
+ stdin: sourceResult.stdin,
1137
+ });
1138
+ } catch (error) {
1139
+ const result = createResult({
1140
+ code: error.code ?? 1,
1141
+ stdout: '',
1142
+ stderr: error.message || 'Pipeline execution failed',
1143
+ stdin:
1144
+ this.options.stdin && typeof this.options.stdin === 'string'
1145
+ ? this.options.stdin
1146
+ : this.options.stdin && Buffer.isBuffer(this.options.stdin)
1147
+ ? this.options.stdin.toString('utf8')
1148
+ : '',
1149
+ });
1150
+
1151
+ const buf = Buffer.from(result.stderr);
1152
+ if (this.options.mirror) {
1153
+ safeWrite(process.stderr, buf);
1154
+ }
1155
+ this._emitProcessedData('stderr', buf);
1156
+
1157
+ this.finish(result);
1158
+
1159
+ return result;
1160
+ }
1161
+ };
1162
+ }