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,1497 @@
1
+ // ProcessRunner execution methods - start, sync, async, and related methods
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, asBuffer } from './$.stream-utils.mjs';
8
+ import { pumpReadable } from './$.quote.mjs';
9
+ import { createResult } from './$.result.mjs';
10
+ import { parseShellCommand, needsRealShell } from './shell-parser.mjs';
11
+
12
+ const isBun = typeof globalThis.Bun !== 'undefined';
13
+
14
+ /**
15
+ * Check for shell operators in command
16
+ * @param {string} command - Command to check
17
+ * @returns {boolean}
18
+ */
19
+ function hasShellOperators(command) {
20
+ return (
21
+ command.includes('&&') ||
22
+ command.includes('||') ||
23
+ command.includes('(') ||
24
+ command.includes(';') ||
25
+ (command.includes('cd ') && command.includes('&&'))
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Check if command is a streaming pattern
31
+ * @param {string} command - Command to check
32
+ * @returns {boolean}
33
+ */
34
+ function isStreamingPattern(command) {
35
+ return (
36
+ command.includes('sleep') &&
37
+ command.includes(';') &&
38
+ (command.includes('echo') || command.includes('printf'))
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Determine if shell operators should be used
44
+ * @param {object} runner - ProcessRunner instance
45
+ * @param {string} command - Command to check
46
+ * @returns {boolean}
47
+ */
48
+ function shouldUseShellOperators(runner, command) {
49
+ const hasOps = hasShellOperators(command);
50
+ const isStreaming = isStreamingPattern(command);
51
+ return (
52
+ runner.options.shellOperators &&
53
+ hasOps &&
54
+ !isStreaming &&
55
+ !runner._isStreaming
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Check if stdin is interactive
61
+ * @param {string} stdin - Stdin option
62
+ * @param {object} options - Runner options
63
+ * @returns {boolean}
64
+ */
65
+ function isInteractiveMode(stdin, options) {
66
+ return (
67
+ stdin === 'inherit' &&
68
+ process.stdin.isTTY === true &&
69
+ process.stdout.isTTY === true &&
70
+ process.stderr.isTTY === true &&
71
+ options.interactive === true
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Spawn process using Bun
77
+ * @param {Array} argv - Command arguments
78
+ * @param {object} config - Spawn configuration
79
+ * @returns {object} Child process
80
+ */
81
+ function spawnWithBun(argv, config) {
82
+ const { cwd, env, isInteractive } = config;
83
+
84
+ trace(
85
+ 'ProcessRunner',
86
+ () =>
87
+ `spawnBun: Creating process | ${JSON.stringify({
88
+ command: argv[0],
89
+ args: argv.slice(1),
90
+ isInteractive,
91
+ cwd,
92
+ platform: process.platform,
93
+ })}`
94
+ );
95
+
96
+ if (isInteractive) {
97
+ trace(
98
+ 'ProcessRunner',
99
+ () => `spawnBun: Using interactive mode with inherited stdio`
100
+ );
101
+ return Bun.spawn(argv, {
102
+ cwd,
103
+ env,
104
+ stdin: 'inherit',
105
+ stdout: 'inherit',
106
+ stderr: 'inherit',
107
+ });
108
+ }
109
+
110
+ trace(
111
+ 'ProcessRunner',
112
+ () =>
113
+ `spawnBun: Using non-interactive mode with pipes and detached=${process.platform !== 'win32'}`
114
+ );
115
+
116
+ return Bun.spawn(argv, {
117
+ cwd,
118
+ env,
119
+ stdin: 'pipe',
120
+ stdout: 'pipe',
121
+ stderr: 'pipe',
122
+ detached: process.platform !== 'win32',
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Spawn process using Node
128
+ * @param {Array} argv - Command arguments
129
+ * @param {object} config - Spawn configuration
130
+ * @returns {object} Child process
131
+ */
132
+ function spawnWithNode(argv, config) {
133
+ const { cwd, env, isInteractive } = config;
134
+
135
+ trace(
136
+ 'ProcessRunner',
137
+ () =>
138
+ `spawnNode: Creating process | ${JSON.stringify({
139
+ command: argv[0],
140
+ args: argv.slice(1),
141
+ isInteractive,
142
+ cwd,
143
+ platform: process.platform,
144
+ })}`
145
+ );
146
+
147
+ if (isInteractive) {
148
+ return cp.spawn(argv[0], argv.slice(1), {
149
+ cwd,
150
+ env,
151
+ stdio: 'inherit',
152
+ });
153
+ }
154
+
155
+ const child = cp.spawn(argv[0], argv.slice(1), {
156
+ cwd,
157
+ env,
158
+ stdio: ['pipe', 'pipe', 'pipe'],
159
+ detached: process.platform !== 'win32',
160
+ });
161
+
162
+ trace(
163
+ 'ProcessRunner',
164
+ () =>
165
+ `spawnNode: Process created | ${JSON.stringify({
166
+ pid: child.pid,
167
+ killed: child.killed,
168
+ hasStdout: !!child.stdout,
169
+ hasStderr: !!child.stderr,
170
+ hasStdin: !!child.stdin,
171
+ })}`
172
+ );
173
+
174
+ return child;
175
+ }
176
+
177
+ /**
178
+ * Spawn child process with appropriate runtime
179
+ * @param {Array} argv - Command arguments
180
+ * @param {object} config - Spawn configuration
181
+ * @returns {object} Child process
182
+ */
183
+ function spawnChild(argv, config) {
184
+ const { stdin } = config;
185
+ const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
186
+ const preferNodeForInput = isBun && needsExplicitPipe;
187
+
188
+ trace(
189
+ 'ProcessRunner',
190
+ () =>
191
+ `About to spawn process | ${JSON.stringify({
192
+ needsExplicitPipe,
193
+ preferNodeForInput,
194
+ runtime: isBun ? 'Bun' : 'Node',
195
+ command: argv[0],
196
+ args: argv.slice(1),
197
+ })}`
198
+ );
199
+
200
+ if (preferNodeForInput) {
201
+ return spawnWithNode(argv, config);
202
+ }
203
+ return isBun ? spawnWithBun(argv, config) : spawnWithNode(argv, config);
204
+ }
205
+
206
+ /**
207
+ * Setup child process event listeners
208
+ * @param {object} runner - ProcessRunner instance
209
+ */
210
+ function setupChildEventListeners(runner) {
211
+ if (!runner.child || typeof runner.child.on !== 'function') {
212
+ return;
213
+ }
214
+
215
+ runner.child.on('spawn', () => {
216
+ trace(
217
+ 'ProcessRunner',
218
+ () =>
219
+ `Child process spawned successfully | ${JSON.stringify({
220
+ pid: runner.child.pid,
221
+ command: runner.spec?.command?.slice(0, 50),
222
+ })}`
223
+ );
224
+ });
225
+
226
+ runner.child.on('error', (error) => {
227
+ trace(
228
+ 'ProcessRunner',
229
+ () =>
230
+ `Child process error event | ${JSON.stringify({
231
+ pid: runner.child?.pid,
232
+ error: error.message,
233
+ code: error.code,
234
+ errno: error.errno,
235
+ syscall: error.syscall,
236
+ command: runner.spec?.command?.slice(0, 50),
237
+ })}`
238
+ );
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Create stdout pump
244
+ * @param {object} runner - ProcessRunner instance
245
+ * @param {number} childPid - Child process PID
246
+ * @returns {Promise}
247
+ */
248
+ function createStdoutPump(runner, childPid) {
249
+ if (!runner.child.stdout) {
250
+ return Promise.resolve();
251
+ }
252
+
253
+ return pumpReadable(runner.child.stdout, (buf) => {
254
+ trace(
255
+ 'ProcessRunner',
256
+ () =>
257
+ `stdout data received | ${JSON.stringify({
258
+ pid: childPid,
259
+ bufferLength: buf.length,
260
+ capture: runner.options.capture,
261
+ mirror: runner.options.mirror,
262
+ preview: buf.toString().slice(0, 100),
263
+ })}`
264
+ );
265
+
266
+ if (runner.options.capture) {
267
+ runner.outChunks.push(buf);
268
+ }
269
+ if (runner.options.mirror) {
270
+ safeWrite(process.stdout, buf);
271
+ }
272
+
273
+ runner._emitProcessedData('stdout', buf);
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Create stderr pump
279
+ * @param {object} runner - ProcessRunner instance
280
+ * @param {number} childPid - Child process PID
281
+ * @returns {Promise}
282
+ */
283
+ function createStderrPump(runner, childPid) {
284
+ if (!runner.child.stderr) {
285
+ return Promise.resolve();
286
+ }
287
+
288
+ return pumpReadable(runner.child.stderr, (buf) => {
289
+ trace(
290
+ 'ProcessRunner',
291
+ () =>
292
+ `stderr data received | ${JSON.stringify({
293
+ pid: childPid,
294
+ bufferLength: buf.length,
295
+ capture: runner.options.capture,
296
+ mirror: runner.options.mirror,
297
+ preview: buf.toString().slice(0, 100),
298
+ })}`
299
+ );
300
+
301
+ if (runner.options.capture) {
302
+ runner.errChunks.push(buf);
303
+ }
304
+ if (runner.options.mirror) {
305
+ safeWrite(process.stderr, buf);
306
+ }
307
+
308
+ runner._emitProcessedData('stderr', buf);
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Handle stdin for inherit mode
314
+ * @param {object} runner - ProcessRunner instance
315
+ * @param {boolean} isInteractive - Is interactive mode
316
+ * @returns {Promise}
317
+ */
318
+ function handleInheritStdin(runner, isInteractive) {
319
+ if (isInteractive) {
320
+ trace(
321
+ 'ProcessRunner',
322
+ () => `stdin: Using inherit mode for interactive command`
323
+ );
324
+ return Promise.resolve();
325
+ }
326
+
327
+ const isPipedIn = process.stdin && process.stdin.isTTY === false;
328
+ trace(
329
+ 'ProcessRunner',
330
+ () =>
331
+ `stdin: Non-interactive inherit mode | ${JSON.stringify({
332
+ isPipedIn,
333
+ stdinTTY: process.stdin.isTTY,
334
+ })}`
335
+ );
336
+
337
+ if (isPipedIn) {
338
+ trace('ProcessRunner', () => `stdin: Pumping piped input to child process`);
339
+ return runner._pumpStdinTo(
340
+ runner.child,
341
+ runner.options.capture ? runner.inChunks : null
342
+ );
343
+ }
344
+
345
+ trace(
346
+ 'ProcessRunner',
347
+ () => `stdin: Forwarding TTY stdin for non-interactive command`
348
+ );
349
+ return runner._forwardTTYStdin();
350
+ }
351
+
352
+ /**
353
+ * Handle stdin based on configuration
354
+ * @param {object} runner - ProcessRunner instance
355
+ * @param {string|Buffer} stdin - Stdin configuration
356
+ * @param {boolean} isInteractive - Is interactive mode
357
+ * @returns {Promise}
358
+ */
359
+ function handleStdin(runner, stdin, isInteractive) {
360
+ trace(
361
+ 'ProcessRunner',
362
+ () =>
363
+ `Setting up stdin handling | ${JSON.stringify({
364
+ stdinType: typeof stdin,
365
+ stdin:
366
+ stdin === 'inherit'
367
+ ? 'inherit'
368
+ : stdin === 'ignore'
369
+ ? 'ignore'
370
+ : typeof stdin === 'string'
371
+ ? `string(${stdin.length})`
372
+ : 'other',
373
+ isInteractive,
374
+ hasChildStdin: !!runner.child?.stdin,
375
+ processTTY: process.stdin.isTTY,
376
+ })}`
377
+ );
378
+
379
+ if (stdin === 'inherit') {
380
+ return handleInheritStdin(runner, isInteractive);
381
+ }
382
+
383
+ if (stdin === 'ignore') {
384
+ trace('ProcessRunner', () => `stdin: Ignoring and closing stdin`);
385
+ if (runner.child.stdin && typeof runner.child.stdin.end === 'function') {
386
+ runner.child.stdin.end();
387
+ }
388
+ return Promise.resolve();
389
+ }
390
+
391
+ if (stdin === 'pipe') {
392
+ trace(
393
+ 'ProcessRunner',
394
+ () => `stdin: Using pipe mode - leaving stdin open for manual control`
395
+ );
396
+ return Promise.resolve();
397
+ }
398
+
399
+ if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
400
+ const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
401
+ trace(
402
+ 'ProcessRunner',
403
+ () =>
404
+ `stdin: Writing buffer to child | ${JSON.stringify({
405
+ bufferLength: buf.length,
406
+ willCapture: runner.options.capture && !!runner.inChunks,
407
+ })}`
408
+ );
409
+ if (runner.options.capture && runner.inChunks) {
410
+ runner.inChunks.push(Buffer.from(buf));
411
+ }
412
+ return runner._writeToStdin(buf);
413
+ }
414
+
415
+ return Promise.resolve();
416
+ }
417
+
418
+ /**
419
+ * Create promise for child exit
420
+ * @param {object} child - Child process
421
+ * @returns {Promise}
422
+ */
423
+ function createExitPromise(child) {
424
+ if (isBun) {
425
+ return child.exited;
426
+ }
427
+
428
+ return new Promise((resolve) => {
429
+ trace(
430
+ 'ProcessRunner',
431
+ () => `Setting up child process event listeners for PID ${child.pid}`
432
+ );
433
+
434
+ child.on('close', (code, signal) => {
435
+ trace(
436
+ 'ProcessRunner',
437
+ () =>
438
+ `Child process close event | ${JSON.stringify({
439
+ pid: child.pid,
440
+ code,
441
+ signal,
442
+ killed: child.killed,
443
+ exitCode: child.exitCode,
444
+ signalCode: child.signalCode,
445
+ })}`
446
+ );
447
+ resolve(code);
448
+ });
449
+
450
+ child.on('exit', (code, signal) => {
451
+ trace(
452
+ 'ProcessRunner',
453
+ () =>
454
+ `Child process exit event | ${JSON.stringify({
455
+ pid: child.pid,
456
+ code,
457
+ signal,
458
+ killed: child.killed,
459
+ exitCode: child.exitCode,
460
+ signalCode: child.signalCode,
461
+ })}`
462
+ );
463
+ });
464
+ });
465
+ }
466
+
467
+ /**
468
+ * Determine final exit code
469
+ * @param {number|null|undefined} code - Raw exit code
470
+ * @param {boolean} cancelled - Was process cancelled
471
+ * @returns {number}
472
+ */
473
+ function determineFinalExitCode(code, cancelled) {
474
+ trace(
475
+ 'ProcessRunner',
476
+ () =>
477
+ `Raw exit code from child | ${JSON.stringify({
478
+ code,
479
+ codeType: typeof code,
480
+ cancelled,
481
+ isBun,
482
+ })}`
483
+ );
484
+
485
+ if (code !== undefined && code !== null) {
486
+ return code;
487
+ }
488
+
489
+ if (cancelled) {
490
+ trace(
491
+ 'ProcessRunner',
492
+ () => `Process was killed, using SIGTERM exit code 143`
493
+ );
494
+ return 143;
495
+ }
496
+
497
+ trace('ProcessRunner', () => `Process exited without code, defaulting to 0`);
498
+ return 0;
499
+ }
500
+
501
+ /**
502
+ * Build result data from runner state
503
+ * @param {object} runner - ProcessRunner instance
504
+ * @param {number} exitCode - Exit code
505
+ * @returns {object}
506
+ */
507
+ function buildResultData(runner, exitCode) {
508
+ return {
509
+ code: exitCode,
510
+ stdout: runner.options.capture
511
+ ? runner.outChunks && runner.outChunks.length > 0
512
+ ? Buffer.concat(runner.outChunks).toString('utf8')
513
+ : ''
514
+ : undefined,
515
+ stderr: runner.options.capture
516
+ ? runner.errChunks && runner.errChunks.length > 0
517
+ ? Buffer.concat(runner.errChunks).toString('utf8')
518
+ : ''
519
+ : undefined,
520
+ stdin:
521
+ runner.options.capture && runner.inChunks
522
+ ? Buffer.concat(runner.inChunks).toString('utf8')
523
+ : undefined,
524
+ child: runner.child,
525
+ };
526
+ }
527
+
528
+ /**
529
+ * Throw errexit error if needed
530
+ * @param {object} runner - ProcessRunner instance
531
+ * @param {object} globalShellSettings - Shell settings
532
+ */
533
+ function throwErrexitIfNeeded(runner, globalShellSettings) {
534
+ if (!globalShellSettings.errexit || runner.result.code === 0) {
535
+ return;
536
+ }
537
+
538
+ trace('ProcessRunner', () => `Errexit mode: throwing error`);
539
+
540
+ const error = new Error(
541
+ `Command failed with exit code ${runner.result.code}`
542
+ );
543
+ error.code = runner.result.code;
544
+ error.stdout = runner.result.stdout;
545
+ error.stderr = runner.result.stderr;
546
+ error.result = runner.result;
547
+
548
+ throw error;
549
+ }
550
+
551
+ /**
552
+ * Get stdin input for sync spawn
553
+ * @param {string|Buffer} stdin - Stdin option
554
+ * @returns {Buffer|undefined}
555
+ */
556
+ function getSyncStdinInput(stdin) {
557
+ if (typeof stdin === 'string') {
558
+ return Buffer.from(stdin);
559
+ }
560
+ if (Buffer.isBuffer(stdin)) {
561
+ return stdin;
562
+ }
563
+ return undefined;
564
+ }
565
+
566
+ /**
567
+ * Get stdin string for result
568
+ * @param {string|Buffer} stdin - Stdin option
569
+ * @returns {string}
570
+ */
571
+ function getStdinString(stdin) {
572
+ if (typeof stdin === 'string') {
573
+ return stdin;
574
+ }
575
+ if (Buffer.isBuffer(stdin)) {
576
+ return stdin.toString('utf8');
577
+ }
578
+ return '';
579
+ }
580
+
581
+ /**
582
+ * Execute sync process using Bun
583
+ * @param {Array} argv - Command arguments
584
+ * @param {object} options - Spawn options
585
+ * @returns {object} Result object
586
+ */
587
+ function executeSyncBun(argv, options) {
588
+ const { cwd, env, stdin } = options;
589
+ const proc = Bun.spawnSync(argv, {
590
+ cwd,
591
+ env,
592
+ stdin: getSyncStdinInput(stdin),
593
+ stdout: 'pipe',
594
+ stderr: 'pipe',
595
+ });
596
+
597
+ const result = createResult({
598
+ code: proc.exitCode || 0,
599
+ stdout: proc.stdout?.toString('utf8') || '',
600
+ stderr: proc.stderr?.toString('utf8') || '',
601
+ stdin: getStdinString(stdin),
602
+ });
603
+ result.child = proc;
604
+ return result;
605
+ }
606
+
607
+ /**
608
+ * Execute sync process using Node
609
+ * @param {Array} argv - Command arguments
610
+ * @param {object} options - Spawn options
611
+ * @returns {object} Result object
612
+ */
613
+ function executeSyncNode(argv, options) {
614
+ const { cwd, env, stdin } = options;
615
+ const proc = cp.spawnSync(argv[0], argv.slice(1), {
616
+ cwd,
617
+ env,
618
+ input: getSyncStdinInput(stdin),
619
+ encoding: 'utf8',
620
+ stdio: ['pipe', 'pipe', 'pipe'],
621
+ });
622
+
623
+ const result = createResult({
624
+ code: proc.status || 0,
625
+ stdout: proc.stdout || '',
626
+ stderr: proc.stderr || '',
627
+ stdin: getStdinString(stdin),
628
+ });
629
+ result.child = proc;
630
+ return result;
631
+ }
632
+
633
+ /**
634
+ * Execute sync process with appropriate runtime
635
+ * @param {Array} argv - Command arguments
636
+ * @param {object} options - Spawn options
637
+ * @returns {object} Result object
638
+ */
639
+ function executeSyncProcess(argv, options) {
640
+ return isBun ? executeSyncBun(argv, options) : executeSyncNode(argv, options);
641
+ }
642
+
643
+ /**
644
+ * Handle sync result processing
645
+ * @param {object} runner - ProcessRunner instance
646
+ * @param {object} result - Result object
647
+ * @param {object} globalShellSettings - Shell settings
648
+ * @returns {object} Result
649
+ */
650
+ function processSyncResult(runner, result, globalShellSettings) {
651
+ if (runner.options.mirror) {
652
+ if (result.stdout) {
653
+ safeWrite(process.stdout, result.stdout);
654
+ }
655
+ if (result.stderr) {
656
+ safeWrite(process.stderr, result.stderr);
657
+ }
658
+ }
659
+
660
+ runner.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
661
+ runner.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
662
+
663
+ if (result.stdout) {
664
+ runner._emitProcessedData('stdout', Buffer.from(result.stdout));
665
+ }
666
+ if (result.stderr) {
667
+ runner._emitProcessedData('stderr', Buffer.from(result.stderr));
668
+ }
669
+
670
+ runner.finish(result);
671
+
672
+ if (globalShellSettings.errexit && result.code !== 0) {
673
+ const error = new Error(`Command failed with exit code ${result.code}`);
674
+ error.code = result.code;
675
+ error.stdout = result.stdout;
676
+ error.stderr = result.stderr;
677
+ error.result = result;
678
+ throw error;
679
+ }
680
+
681
+ return result;
682
+ }
683
+
684
+ /**
685
+ * Setup external abort signal listener
686
+ * @param {object} runner - ProcessRunner instance
687
+ */
688
+ function setupExternalAbortSignal(runner) {
689
+ const signal = runner.options.signal;
690
+ if (!signal || typeof signal.addEventListener !== 'function') {
691
+ return;
692
+ }
693
+
694
+ trace(
695
+ 'ProcessRunner',
696
+ () =>
697
+ `Setting up external abort signal listener | ${JSON.stringify({
698
+ hasSignal: !!signal,
699
+ signalAborted: signal.aborted,
700
+ hasInternalController: !!runner._abortController,
701
+ internalAborted: runner._abortController?.signal.aborted,
702
+ })}`
703
+ );
704
+
705
+ signal.addEventListener('abort', () => {
706
+ trace(
707
+ 'ProcessRunner',
708
+ () =>
709
+ `External abort signal triggered | ${JSON.stringify({
710
+ externalSignalAborted: signal.aborted,
711
+ hasInternalController: !!runner._abortController,
712
+ internalAborted: runner._abortController?.signal.aborted,
713
+ command: runner.spec?.command?.slice(0, 50),
714
+ })}`
715
+ );
716
+
717
+ runner.kill('SIGTERM');
718
+ trace(
719
+ 'ProcessRunner',
720
+ () => 'Process kill initiated due to external abort signal'
721
+ );
722
+
723
+ if (runner._abortController && !runner._abortController.signal.aborted) {
724
+ trace(
725
+ 'ProcessRunner',
726
+ () => 'Aborting internal controller due to external signal'
727
+ );
728
+ runner._abortController.abort();
729
+ }
730
+ });
731
+
732
+ if (signal.aborted) {
733
+ trace(
734
+ 'ProcessRunner',
735
+ () =>
736
+ `External signal already aborted, killing process and aborting internal controller`
737
+ );
738
+ runner.kill('SIGTERM');
739
+ if (runner._abortController && !runner._abortController.signal.aborted) {
740
+ runner._abortController.abort();
741
+ }
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Reinitialize capture chunks when capture option changes
747
+ * @param {object} runner - ProcessRunner instance
748
+ */
749
+ function reinitCaptureChunks(runner) {
750
+ trace(
751
+ 'ProcessRunner',
752
+ () =>
753
+ `BRANCH: capture => REINIT_CHUNKS | ${JSON.stringify({
754
+ capture: runner.options.capture,
755
+ })}`
756
+ );
757
+
758
+ runner.outChunks = runner.options.capture ? [] : null;
759
+ runner.errChunks = runner.options.capture ? [] : null;
760
+ runner.inChunks =
761
+ runner.options.capture && runner.options.stdin === 'inherit'
762
+ ? []
763
+ : runner.options.capture &&
764
+ (typeof runner.options.stdin === 'string' ||
765
+ Buffer.isBuffer(runner.options.stdin))
766
+ ? [Buffer.from(runner.options.stdin)]
767
+ : [];
768
+ }
769
+
770
+ /**
771
+ * Try running command via enhanced shell parser
772
+ * @param {object} runner - ProcessRunner instance
773
+ * @param {string} command - Command to parse
774
+ * @returns {Promise<object>|null} Result if handled, null if not
775
+ */
776
+ async function tryEnhancedShellParser(runner, command) {
777
+ const enhancedParsed = parseShellCommand(command);
778
+ if (!enhancedParsed || enhancedParsed.type === 'simple') {
779
+ return null;
780
+ }
781
+
782
+ trace(
783
+ 'ProcessRunner',
784
+ () =>
785
+ `Using enhanced parser for shell operators | ${JSON.stringify({
786
+ type: enhancedParsed.type,
787
+ command: command.slice(0, 50),
788
+ })}`
789
+ );
790
+
791
+ if (enhancedParsed.type === 'sequence') {
792
+ return await runner._runSequence(enhancedParsed);
793
+ }
794
+ if (enhancedParsed.type === 'subshell') {
795
+ return await runner._runSubshell(enhancedParsed);
796
+ }
797
+ if (enhancedParsed.type === 'pipeline') {
798
+ return await runner._runPipeline(enhancedParsed.commands);
799
+ }
800
+
801
+ return null;
802
+ }
803
+
804
+ /**
805
+ * Try running command as virtual command
806
+ * @param {object} runner - ProcessRunner instance
807
+ * @param {object} parsed - Parsed command
808
+ * @param {object} deps - Dependencies
809
+ * @returns {Promise<object>|null} Result if handled, null if not
810
+ */
811
+ async function tryVirtualCommand(runner, parsed, deps) {
812
+ const { virtualCommands, isVirtualCommandsEnabled } = deps;
813
+
814
+ if (
815
+ parsed.type !== 'simple' ||
816
+ !isVirtualCommandsEnabled() ||
817
+ !virtualCommands.has(parsed.cmd) ||
818
+ runner.options._bypassVirtual
819
+ ) {
820
+ return null;
821
+ }
822
+
823
+ const hasCustomStdin =
824
+ runner.options.stdin &&
825
+ runner.options.stdin !== 'inherit' &&
826
+ runner.options.stdin !== 'ignore';
827
+
828
+ const commandsThatNeedRealStdin = ['sleep', 'cat'];
829
+ const shouldBypassVirtual =
830
+ hasCustomStdin && commandsThatNeedRealStdin.includes(parsed.cmd);
831
+
832
+ if (shouldBypassVirtual) {
833
+ trace(
834
+ 'ProcessRunner',
835
+ () =>
836
+ `Bypassing built-in virtual command due to custom stdin | ${JSON.stringify(
837
+ {
838
+ cmd: parsed.cmd,
839
+ stdin: typeof runner.options.stdin,
840
+ }
841
+ )}`
842
+ );
843
+ return null;
844
+ }
845
+
846
+ trace(
847
+ 'ProcessRunner',
848
+ () =>
849
+ `BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
850
+ isVirtual: true,
851
+ args: parsed.args,
852
+ })}`
853
+ );
854
+
855
+ return await runner._runVirtual(parsed.cmd, parsed.args, runner.spec.command);
856
+ }
857
+
858
+ /**
859
+ * Log xtrace/verbose if enabled
860
+ * @param {object} globalShellSettings - Shell settings
861
+ * @param {string} command - Command or argv
862
+ */
863
+ function logShellTrace(globalShellSettings, command) {
864
+ if (globalShellSettings.xtrace) {
865
+ console.log(`+ ${command}`);
866
+ }
867
+ if (globalShellSettings.verbose) {
868
+ console.log(command);
869
+ }
870
+ }
871
+
872
+ /**
873
+ * Handle shell mode execution
874
+ * @param {object} runner - ProcessRunner instance
875
+ * @param {object} deps - Dependencies
876
+ * @returns {Promise<object>|null} Result if handled by special cases
877
+ */
878
+ async function handleShellMode(runner, deps) {
879
+ const { virtualCommands, isVirtualCommandsEnabled } = deps;
880
+ const command = runner.spec.command;
881
+
882
+ trace(
883
+ 'ProcessRunner',
884
+ () => `BRANCH: spec.mode => shell | ${JSON.stringify({})}`
885
+ );
886
+
887
+ const useShellOps = shouldUseShellOperators(runner, command);
888
+
889
+ trace(
890
+ 'ProcessRunner',
891
+ () =>
892
+ `Shell operator detection | ${JSON.stringify({
893
+ hasShellOperators: hasShellOperators(command),
894
+ shellOperatorsEnabled: runner.options.shellOperators,
895
+ isStreamingPattern: isStreamingPattern(command),
896
+ isStreaming: runner._isStreaming,
897
+ shouldUseShellOperators: useShellOps,
898
+ command: command.slice(0, 100),
899
+ })}`
900
+ );
901
+
902
+ if (
903
+ !runner.options._bypassVirtual &&
904
+ useShellOps &&
905
+ !needsRealShell(command)
906
+ ) {
907
+ const result = await tryEnhancedShellParser(runner, command);
908
+ if (result) {
909
+ return result;
910
+ }
911
+ }
912
+
913
+ const parsed = runner._parseCommand(command);
914
+ trace(
915
+ 'ProcessRunner',
916
+ () =>
917
+ `Parsed command | ${JSON.stringify({
918
+ type: parsed?.type,
919
+ cmd: parsed?.cmd,
920
+ argsCount: parsed?.args?.length,
921
+ })}`
922
+ );
923
+
924
+ if (parsed) {
925
+ if (parsed.type === 'pipeline') {
926
+ trace(
927
+ 'ProcessRunner',
928
+ () =>
929
+ `BRANCH: parsed.type => pipeline | ${JSON.stringify({
930
+ commandCount: parsed.commands?.length,
931
+ })}`
932
+ );
933
+ return await runner._runPipeline(parsed.commands);
934
+ }
935
+
936
+ const virtualResult = await tryVirtualCommand(runner, parsed, {
937
+ virtualCommands,
938
+ isVirtualCommandsEnabled,
939
+ });
940
+ if (virtualResult) {
941
+ return virtualResult;
942
+ }
943
+ }
944
+
945
+ return null;
946
+ }
947
+
948
+ /**
949
+ * Execute child process and collect results
950
+ * @param {object} runner - ProcessRunner instance
951
+ * @param {Array} argv - Command arguments
952
+ * @param {object} config - Spawn configuration
953
+ * @returns {Promise<object>} Result
954
+ */
955
+ async function executeChildProcess(runner, argv, config) {
956
+ const { stdin, isInteractive } = config;
957
+
958
+ runner.child = spawnChild(argv, config);
959
+
960
+ if (runner.child) {
961
+ trace(
962
+ 'ProcessRunner',
963
+ () =>
964
+ `Child process created | ${JSON.stringify({
965
+ pid: runner.child.pid,
966
+ detached: runner.child.options?.detached,
967
+ killed: runner.child.killed,
968
+ hasStdout: !!runner.child.stdout,
969
+ hasStderr: !!runner.child.stderr,
970
+ hasStdin: !!runner.child.stdin,
971
+ platform: process.platform,
972
+ command: runner.spec?.command?.slice(0, 100),
973
+ })}`
974
+ );
975
+ setupChildEventListeners(runner);
976
+ }
977
+
978
+ const childPid = runner.child?.pid;
979
+ const outPump = createStdoutPump(runner, childPid);
980
+ const errPump = createStderrPump(runner, childPid);
981
+ const stdinPumpPromise = handleStdin(runner, stdin, isInteractive);
982
+ const exited = createExitPromise(runner.child);
983
+
984
+ const code = await exited;
985
+ await Promise.all([outPump, errPump, stdinPumpPromise]);
986
+
987
+ const finalExitCode = determineFinalExitCode(code, runner._cancelled);
988
+ const resultData = buildResultData(runner, finalExitCode);
989
+
990
+ trace(
991
+ 'ProcessRunner',
992
+ () =>
993
+ `Process completed | ${JSON.stringify({
994
+ command: runner.command,
995
+ finalExitCode,
996
+ captured: runner.options.capture,
997
+ hasStdout: !!resultData.stdout,
998
+ hasStderr: !!resultData.stderr,
999
+ stdoutLength: resultData.stdout?.length || 0,
1000
+ stderrLength: resultData.stderr?.length || 0,
1001
+ stdoutPreview: resultData.stdout?.slice(0, 100),
1002
+ stderrPreview: resultData.stderr?.slice(0, 100),
1003
+ childPid: runner.child?.pid,
1004
+ cancelled: runner._cancelled,
1005
+ cancellationSignal: runner._cancellationSignal,
1006
+ platform: process.platform,
1007
+ runtime: isBun ? 'Bun' : 'Node.js',
1008
+ })}`
1009
+ );
1010
+
1011
+ return {
1012
+ ...resultData,
1013
+ text() {
1014
+ return Promise.resolve(resultData.stdout || '');
1015
+ },
1016
+ };
1017
+ }
1018
+
1019
+ /**
1020
+ * Attach execution methods to ProcessRunner prototype
1021
+ * @param {Function} ProcessRunner - The ProcessRunner class
1022
+ * @param {Object} deps - Dependencies (virtualCommands, globalShellSettings, isVirtualCommandsEnabled)
1023
+ */
1024
+ export function attachExecutionMethods(ProcessRunner, deps) {
1025
+ const { globalShellSettings } = deps;
1026
+
1027
+ // Unified start method
1028
+ ProcessRunner.prototype.start = function (options = {}) {
1029
+ const mode = options.mode || 'async';
1030
+
1031
+ trace(
1032
+ 'ProcessRunner',
1033
+ () =>
1034
+ `start ENTER | ${JSON.stringify({
1035
+ mode,
1036
+ options,
1037
+ started: this.started,
1038
+ hasPromise: !!this.promise,
1039
+ hasChild: !!this.child,
1040
+ command: this.spec?.command?.slice(0, 50),
1041
+ })}`
1042
+ );
1043
+
1044
+ if (Object.keys(options).length > 0 && !this.started) {
1045
+ trace(
1046
+ 'ProcessRunner',
1047
+ () =>
1048
+ `BRANCH: options => MERGE | ${JSON.stringify({
1049
+ oldOptions: this.options,
1050
+ newOptions: options,
1051
+ })}`
1052
+ );
1053
+
1054
+ this.options = { ...this.options, ...options };
1055
+ setupExternalAbortSignal(this);
1056
+
1057
+ if ('capture' in options) {
1058
+ reinitCaptureChunks(this);
1059
+ }
1060
+
1061
+ trace(
1062
+ 'ProcessRunner',
1063
+ () =>
1064
+ `OPTIONS_MERGED | ${JSON.stringify({ finalOptions: this.options })}`
1065
+ );
1066
+ }
1067
+
1068
+ if (mode === 'sync') {
1069
+ trace('ProcessRunner', () => `BRANCH: mode => sync`);
1070
+ return this._startSync();
1071
+ }
1072
+
1073
+ trace('ProcessRunner', () => `BRANCH: mode => async`);
1074
+ return this._startAsync();
1075
+ };
1076
+
1077
+ ProcessRunner.prototype.sync = function () {
1078
+ return this.start({ mode: 'sync' });
1079
+ };
1080
+
1081
+ ProcessRunner.prototype.async = function () {
1082
+ return this.start({ mode: 'async' });
1083
+ };
1084
+
1085
+ ProcessRunner.prototype.run = function (options = {}) {
1086
+ trace(
1087
+ 'ProcessRunner',
1088
+ () => `run ENTER | ${JSON.stringify({ options }, null, 2)}`
1089
+ );
1090
+ return this.start(options);
1091
+ };
1092
+
1093
+ ProcessRunner.prototype._startAsync = function () {
1094
+ if (this.started) {
1095
+ return this.promise;
1096
+ }
1097
+ if (this.promise) {
1098
+ return this.promise;
1099
+ }
1100
+
1101
+ this.promise = this._doStartAsync();
1102
+ return this.promise;
1103
+ };
1104
+
1105
+ ProcessRunner.prototype._doStartAsync = async function () {
1106
+ trace(
1107
+ 'ProcessRunner',
1108
+ () =>
1109
+ `_doStartAsync ENTER | ${JSON.stringify({
1110
+ mode: this.spec.mode,
1111
+ command: this.spec.command?.slice(0, 100),
1112
+ })}`
1113
+ );
1114
+
1115
+ this.started = true;
1116
+ this._mode = 'async';
1117
+
1118
+ try {
1119
+ const { cwd, env, stdin } = this.options;
1120
+
1121
+ // Handle pipeline mode
1122
+ if (this.spec.mode === 'pipeline') {
1123
+ trace(
1124
+ 'ProcessRunner',
1125
+ () =>
1126
+ `BRANCH: spec.mode => pipeline | ${JSON.stringify({
1127
+ hasSource: !!this.spec.source,
1128
+ hasDestination: !!this.spec.destination,
1129
+ })}`
1130
+ );
1131
+ return await this._runProgrammaticPipeline(
1132
+ this.spec.source,
1133
+ this.spec.destination
1134
+ );
1135
+ }
1136
+
1137
+ // Handle shell mode special cases
1138
+ if (this.spec.mode === 'shell') {
1139
+ const shellResult = await handleShellMode(this, deps);
1140
+ if (shellResult) {
1141
+ return shellResult;
1142
+ }
1143
+ }
1144
+
1145
+ // Build command arguments
1146
+ const shell = findAvailableShell();
1147
+ const argv =
1148
+ this.spec.mode === 'shell'
1149
+ ? [shell.cmd, ...shell.args, this.spec.command]
1150
+ : [this.spec.file, ...this.spec.args];
1151
+
1152
+ trace(
1153
+ 'ProcessRunner',
1154
+ () =>
1155
+ `Constructed argv | ${JSON.stringify({
1156
+ mode: this.spec.mode,
1157
+ argv,
1158
+ originalCommand: this.spec.command,
1159
+ })}`
1160
+ );
1161
+
1162
+ // Log command if tracing enabled
1163
+ const traceCmd =
1164
+ this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
1165
+ logShellTrace(globalShellSettings, traceCmd);
1166
+
1167
+ // Detect interactive mode
1168
+ const isInteractive = isInteractiveMode(stdin, this.options);
1169
+
1170
+ trace(
1171
+ 'ProcessRunner',
1172
+ () =>
1173
+ `Interactive command detection | ${JSON.stringify({
1174
+ isInteractive,
1175
+ stdinInherit: stdin === 'inherit',
1176
+ stdinTTY: process.stdin.isTTY,
1177
+ stdoutTTY: process.stdout.isTTY,
1178
+ stderrTTY: process.stderr.isTTY,
1179
+ interactiveOption: this.options.interactive,
1180
+ })}`
1181
+ );
1182
+
1183
+ // Execute child process
1184
+ const result = await executeChildProcess(this, argv, {
1185
+ cwd,
1186
+ env,
1187
+ stdin,
1188
+ isInteractive,
1189
+ });
1190
+
1191
+ this.finish(result);
1192
+
1193
+ trace(
1194
+ 'ProcessRunner',
1195
+ () =>
1196
+ `Process finished, result set | ${JSON.stringify({
1197
+ finished: this.finished,
1198
+ resultCode: this.result?.code,
1199
+ })}`
1200
+ );
1201
+
1202
+ throwErrexitIfNeeded(this, globalShellSettings);
1203
+
1204
+ return this.result;
1205
+ } catch (error) {
1206
+ trace(
1207
+ 'ProcessRunner',
1208
+ () =>
1209
+ `Caught error in _doStartAsync | ${JSON.stringify({
1210
+ errorMessage: error.message,
1211
+ errorCode: error.code,
1212
+ isCommandError: error.isCommandError,
1213
+ hasResult: !!error.result,
1214
+ command: this.spec?.command?.slice(0, 100),
1215
+ })}`
1216
+ );
1217
+
1218
+ if (!this.finished) {
1219
+ const errorResult = createResult({
1220
+ code: error.code ?? 1,
1221
+ stdout: error.stdout ?? '',
1222
+ stderr: error.stderr ?? error.message ?? '',
1223
+ stdin: '',
1224
+ });
1225
+ this.finish(errorResult);
1226
+ }
1227
+
1228
+ throw error;
1229
+ }
1230
+ };
1231
+
1232
+ ProcessRunner.prototype._pumpStdinTo = async function (child, captureChunks) {
1233
+ trace('ProcessRunner', () => `_pumpStdinTo ENTER`);
1234
+ if (!child.stdin) {
1235
+ return;
1236
+ }
1237
+
1238
+ const bunWriter =
1239
+ isBun && child.stdin && typeof child.stdin.getWriter === 'function'
1240
+ ? child.stdin.getWriter()
1241
+ : null;
1242
+
1243
+ for await (const chunk of process.stdin) {
1244
+ const buf = asBuffer(chunk);
1245
+ captureChunks && captureChunks.push(buf);
1246
+ if (bunWriter) {
1247
+ await bunWriter.write(buf);
1248
+ } else if (typeof child.stdin.write === 'function') {
1249
+ StreamUtils.addStdinErrorHandler(child.stdin, 'child stdin buffer');
1250
+ StreamUtils.safeStreamWrite(child.stdin, buf, 'child stdin buffer');
1251
+ } else if (isBun && typeof Bun.write === 'function') {
1252
+ await Bun.write(child.stdin, buf);
1253
+ }
1254
+ }
1255
+
1256
+ if (bunWriter) {
1257
+ await bunWriter.close();
1258
+ } else if (typeof child.stdin.end === 'function') {
1259
+ child.stdin.end();
1260
+ }
1261
+ };
1262
+
1263
+ ProcessRunner.prototype._writeToStdin = async function (buf) {
1264
+ trace('ProcessRunner', () => `_writeToStdin | len=${buf?.length || 0}`);
1265
+ const bytes =
1266
+ buf instanceof Uint8Array
1267
+ ? buf
1268
+ : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
1269
+
1270
+ if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
1271
+ if (StreamUtils.isBunStream(this.child.stdin)) {
1272
+ // Stream was already closed by writeToStream utility - no action needed
1273
+ } else if (StreamUtils.isNodeStream(this.child.stdin)) {
1274
+ try {
1275
+ this.child.stdin.end();
1276
+ } catch (_endError) {
1277
+ /* Expected when stream is already closed */
1278
+ }
1279
+ }
1280
+ } else if (isBun && typeof Bun.write === 'function') {
1281
+ await Bun.write(this.child.stdin, buf);
1282
+ }
1283
+ };
1284
+
1285
+ ProcessRunner.prototype._forwardTTYStdin = function () {
1286
+ trace('ProcessRunner', () => `_forwardTTYStdin ENTER`);
1287
+ if (!process.stdin.isTTY || !this.child.stdin) {
1288
+ return;
1289
+ }
1290
+
1291
+ try {
1292
+ if (process.stdin.setRawMode) {
1293
+ process.stdin.setRawMode(true);
1294
+ }
1295
+ process.stdin.resume();
1296
+
1297
+ const onData = (chunk) => {
1298
+ if (chunk[0] === 3) {
1299
+ this._sendSigintToChild();
1300
+ return;
1301
+ }
1302
+ if (this.child.stdin?.write) {
1303
+ this.child.stdin.write(chunk);
1304
+ }
1305
+ };
1306
+
1307
+ const cleanup = () => {
1308
+ process.stdin.removeListener('data', onData);
1309
+ if (process.stdin.setRawMode) {
1310
+ process.stdin.setRawMode(false);
1311
+ }
1312
+ process.stdin.pause();
1313
+ };
1314
+
1315
+ process.stdin.on('data', onData);
1316
+
1317
+ const childExit = isBun
1318
+ ? this.child.exited
1319
+ : new Promise((resolve) => {
1320
+ this.child.once('close', resolve);
1321
+ this.child.once('exit', resolve);
1322
+ });
1323
+
1324
+ childExit.then(cleanup).catch(cleanup);
1325
+
1326
+ return childExit;
1327
+ } catch (_error) {
1328
+ // TTY forwarding error - ignore
1329
+ }
1330
+ };
1331
+
1332
+ ProcessRunner.prototype._sendSigintToChild = function () {
1333
+ if (!this.child?.pid) {
1334
+ return;
1335
+ }
1336
+ try {
1337
+ if (isBun) {
1338
+ this.child.kill('SIGINT');
1339
+ } else {
1340
+ try {
1341
+ process.kill(-this.child.pid, 'SIGINT');
1342
+ } catch (_e) {
1343
+ process.kill(this.child.pid, 'SIGINT');
1344
+ }
1345
+ }
1346
+ } catch (_err) {
1347
+ // Error sending SIGINT - ignore
1348
+ }
1349
+ };
1350
+
1351
+ ProcessRunner.prototype._parseCommand = function (command) {
1352
+ const trimmed = command.trim();
1353
+ if (!trimmed) {
1354
+ return null;
1355
+ }
1356
+
1357
+ if (trimmed.includes('|')) {
1358
+ return this._parsePipeline(trimmed);
1359
+ }
1360
+
1361
+ const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
1362
+ if (parts.length === 0) {
1363
+ return null;
1364
+ }
1365
+
1366
+ const cmd = parts[0];
1367
+ const args = parts.slice(1).map((arg) => {
1368
+ if (
1369
+ (arg.startsWith('"') && arg.endsWith('"')) ||
1370
+ (arg.startsWith("'") && arg.endsWith("'"))
1371
+ ) {
1372
+ return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
1373
+ }
1374
+ return { value: arg, quoted: false };
1375
+ });
1376
+
1377
+ return { cmd, args, type: 'simple' };
1378
+ };
1379
+
1380
+ ProcessRunner.prototype._parsePipeline = function (command) {
1381
+ const segments = [];
1382
+ let current = '';
1383
+ let inQuotes = false;
1384
+ let quoteChar = '';
1385
+
1386
+ for (let i = 0; i < command.length; i++) {
1387
+ const char = command[i];
1388
+
1389
+ if (!inQuotes && (char === '"' || char === "'")) {
1390
+ inQuotes = true;
1391
+ quoteChar = char;
1392
+ current += char;
1393
+ } else if (inQuotes && char === quoteChar) {
1394
+ inQuotes = false;
1395
+ quoteChar = '';
1396
+ current += char;
1397
+ } else if (!inQuotes && char === '|') {
1398
+ segments.push(current.trim());
1399
+ current = '';
1400
+ } else {
1401
+ current += char;
1402
+ }
1403
+ }
1404
+
1405
+ if (current.trim()) {
1406
+ segments.push(current.trim());
1407
+ }
1408
+
1409
+ const commands = segments
1410
+ .map((segment) => {
1411
+ const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
1412
+ if (parts.length === 0) {
1413
+ return null;
1414
+ }
1415
+
1416
+ const cmd = parts[0];
1417
+ const args = parts.slice(1).map((arg) => {
1418
+ if (
1419
+ (arg.startsWith('"') && arg.endsWith('"')) ||
1420
+ (arg.startsWith("'") && arg.endsWith("'"))
1421
+ ) {
1422
+ return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
1423
+ }
1424
+ return { value: arg, quoted: false };
1425
+ });
1426
+
1427
+ return { cmd, args };
1428
+ })
1429
+ .filter(Boolean);
1430
+
1431
+ return { type: 'pipeline', commands };
1432
+ };
1433
+
1434
+ // Sync execution
1435
+ ProcessRunner.prototype._startSync = function () {
1436
+ trace('ProcessRunner', () => `_startSync ENTER`);
1437
+
1438
+ if (this.started) {
1439
+ throw new Error(
1440
+ 'Command already started - cannot run sync after async start'
1441
+ );
1442
+ }
1443
+
1444
+ this.started = true;
1445
+ this._mode = 'sync';
1446
+
1447
+ const { cwd, env, stdin } = this.options;
1448
+ const shell = findAvailableShell();
1449
+ const argv =
1450
+ this.spec.mode === 'shell'
1451
+ ? [shell.cmd, ...shell.args, this.spec.command]
1452
+ : [this.spec.file, ...this.spec.args];
1453
+
1454
+ const traceCmd =
1455
+ this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
1456
+ logShellTrace(globalShellSettings, traceCmd);
1457
+
1458
+ const result = executeSyncProcess(argv, { cwd, env, stdin });
1459
+ return processSyncResult(this, result, globalShellSettings);
1460
+ };
1461
+
1462
+ // Promise interface
1463
+ ProcessRunner.prototype.then = function (onFulfilled, onRejected) {
1464
+ if (!this.promise) {
1465
+ this.promise = this._startAsync();
1466
+ }
1467
+ return this.promise.then(onFulfilled, onRejected);
1468
+ };
1469
+
1470
+ ProcessRunner.prototype.catch = function (onRejected) {
1471
+ if (!this.promise) {
1472
+ this.promise = this._startAsync();
1473
+ }
1474
+ return this.promise.catch(onRejected);
1475
+ };
1476
+
1477
+ ProcessRunner.prototype.finally = function (onFinally) {
1478
+ if (!this.promise) {
1479
+ this.promise = this._startAsync();
1480
+ }
1481
+ return this.promise.finally(() => {
1482
+ if (!this.finished) {
1483
+ this.finish(
1484
+ createResult({
1485
+ code: 1,
1486
+ stdout: '',
1487
+ stderr: 'Process terminated unexpectedly',
1488
+ stdin: '',
1489
+ })
1490
+ );
1491
+ }
1492
+ if (onFinally) {
1493
+ onFinally();
1494
+ }
1495
+ });
1496
+ };
1497
+ }