command-stream 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/$.mjs +1315 -912
  2. package/package.json +1 -1
package/$.mjs CHANGED
@@ -6,37 +6,351 @@
6
6
  // 4. Stream access: $`command`.stdout, $`command`.stderr
7
7
 
8
8
  import { createRequire } from 'module';
9
- import { fileURLToPath } from 'url';
9
+ import cp from 'child_process';
10
+ import fs from 'fs';
11
+ import path from 'path';
10
12
 
11
13
  const isBun = typeof globalThis.Bun !== 'undefined';
12
14
 
13
- // Verbose tracing for debugging (enabled in CI or when COMMAND_STREAM_VERBOSE is set)
14
15
  const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';
15
16
 
17
+ // Interactive commands that need TTY forwarding by default
18
+ const INTERACTIVE_COMMANDS = new Set([
19
+ 'top', 'htop', 'btop', 'less', 'more', 'vi', 'vim', 'nano', 'emacs',
20
+ 'man', 'pager', 'watch', 'tmux', 'screen', 'ssh', 'ftp', 'sftp',
21
+ 'mysql', 'psql', 'redis-cli', 'mongo', 'sqlite3', 'irb', 'python',
22
+ 'node', 'repl', 'gdb', 'lldb', 'bc', 'dc', 'ed'
23
+ ]);
24
+
16
25
  // Trace function for verbose logging
17
- function trace(category, message, data = {}) {
26
+ function trace(category, messageOrFunc) {
18
27
  if (!VERBOSE) return;
19
-
28
+ const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
20
29
  const timestamp = new Date().toISOString();
21
- const dataStr = Object.keys(data).length > 0 ? ' | ' + JSON.stringify(data) : '';
22
- console.error(`[TRACE ${timestamp}] [${category}] ${message}${dataStr}`);
30
+ console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
23
31
  }
24
32
 
25
- // Trace decision branches
26
- function traceBranch(category, condition, branch, data = {}) {
27
- if (!VERBOSE) return;
33
+ // Check if a command is interactive and needs TTY forwarding
34
+ function isInteractiveCommand(command) {
35
+ if (!command || typeof command !== 'string') return false;
36
+
37
+ // Extract command and arguments from shell command string
38
+ const parts = command.trim().split(/\s+/);
39
+ const commandName = parts[0];
40
+ const baseName = path.basename(commandName);
41
+
42
+ // Special handling for commands that are only interactive when run without arguments/scripts
43
+ if (baseName === 'node' || baseName === 'python' || baseName === 'python3') {
44
+ // These are only interactive when run without a script file
45
+ // If there are additional arguments (like a script file), they're not interactive
46
+ return parts.length === 1;
47
+ }
28
48
 
29
- trace(category, `BRANCH: ${condition} => ${branch}`, data);
49
+ return INTERACTIVE_COMMANDS.has(baseName);
30
50
  }
31
51
 
32
- // Trace function entry/exit
33
- function traceFunc(category, funcName, phase, data = {}) {
34
- if (!VERBOSE) return;
35
-
36
- trace(category, `${funcName} ${phase}`, data);
52
+
53
+ // Track parent stream state for graceful shutdown
54
+ let parentStreamsMonitored = false;
55
+ const activeProcessRunners = new Set();
56
+
57
+ function monitorParentStreams() {
58
+ if (parentStreamsMonitored) return;
59
+ parentStreamsMonitored = true;
60
+
61
+ const checkParentStream = (stream, name) => {
62
+ if (stream && typeof stream.on === 'function') {
63
+ stream.on('close', () => {
64
+ trace('ProcessRunner', () => `Parent ${name} closed - triggering graceful shutdown | ${JSON.stringify({ activeProcesses: activeProcessRunners.size }, null, 2)}`);
65
+ for (const runner of activeProcessRunners) {
66
+ runner._handleParentStreamClosure();
67
+ }
68
+ });
69
+ }
70
+ };
71
+
72
+ checkParentStream(process.stdout, 'stdout');
73
+ checkParentStream(process.stderr, 'stderr');
37
74
  }
38
75
 
39
- // Global shell settings (like bash set -e / set +e)
76
+ function safeWrite(stream, data, processRunner = null) {
77
+ monitorParentStreams();
78
+
79
+ if (!StreamUtils.isStreamWritable(stream)) {
80
+ trace('ProcessRunner', () => `safeWrite skipped - stream not writable | ${JSON.stringify({
81
+ hasStream: !!stream,
82
+ writable: stream?.writable,
83
+ destroyed: stream?.destroyed,
84
+ closed: stream?.closed
85
+ }, null, 2)}`);
86
+
87
+ if (processRunner && (stream === process.stdout || stream === process.stderr)) {
88
+ processRunner._handleParentStreamClosure();
89
+ }
90
+
91
+ return false;
92
+ }
93
+
94
+ try {
95
+ return stream.write(data);
96
+ } catch (error) {
97
+ trace('ProcessRunner', () => `safeWrite error | ${JSON.stringify({
98
+ error: error.message,
99
+ code: error.code,
100
+ writable: stream.writable,
101
+ destroyed: stream.destroyed
102
+ }, null, 2)}`);
103
+
104
+ if (error.code === 'EPIPE' && processRunner &&
105
+ (stream === process.stdout || stream === process.stderr)) {
106
+ processRunner._handleParentStreamClosure();
107
+ }
108
+
109
+ return false;
110
+ }
111
+ }
112
+
113
+ // Stream utility functions for safe operations and error handling
114
+ const StreamUtils = {
115
+ /**
116
+ * Check if a stream is safe to write to
117
+ */
118
+ isStreamWritable(stream) {
119
+ return stream && stream.writable && !stream.destroyed && !stream.closed;
120
+ },
121
+
122
+ /**
123
+ * Add standardized error handler to stdin streams
124
+ */
125
+ addStdinErrorHandler(stream, contextName = 'stdin', onNonEpipeError = null) {
126
+ if (stream && typeof stream.on === 'function') {
127
+ stream.on('error', (error) => {
128
+ const handled = this.handleStreamError(error, `${contextName} error event`, false);
129
+ if (!handled && onNonEpipeError) {
130
+ onNonEpipeError(error);
131
+ }
132
+ });
133
+ }
134
+ },
135
+
136
+ /**
137
+ * Safely write to a stream with comprehensive error handling
138
+ */
139
+ safeStreamWrite(stream, data, contextName = 'stream') {
140
+ if (!this.isStreamWritable(stream)) {
141
+ trace('ProcessRunner', () => `${contextName} write skipped - not writable | ${JSON.stringify({
142
+ hasStream: !!stream,
143
+ writable: stream?.writable,
144
+ destroyed: stream?.destroyed,
145
+ closed: stream?.closed
146
+ }, null, 2)}`);
147
+ return false;
148
+ }
149
+
150
+ try {
151
+ const result = stream.write(data);
152
+ trace('ProcessRunner', () => `${contextName} write successful | ${JSON.stringify({
153
+ dataLength: data?.length || 0
154
+ }, null, 2)}`);
155
+ return result;
156
+ } catch (error) {
157
+ if (error.code !== 'EPIPE') {
158
+ trace('ProcessRunner', () => `${contextName} write error | ${JSON.stringify({
159
+ error: error.message,
160
+ code: error.code,
161
+ isEPIPE: false
162
+ }, null, 2)}`);
163
+ throw error; // Re-throw non-EPIPE errors
164
+ } else {
165
+ trace('ProcessRunner', () => `${contextName} EPIPE error (ignored) | ${JSON.stringify({
166
+ error: error.message,
167
+ code: error.code,
168
+ isEPIPE: true
169
+ }, null, 2)}`);
170
+ }
171
+ return false;
172
+ }
173
+ },
174
+
175
+ /**
176
+ * Safely end a stream with error handling
177
+ */
178
+ safeStreamEnd(stream, contextName = 'stream') {
179
+ if (!this.isStreamWritable(stream) || typeof stream.end !== 'function') {
180
+ trace('ProcessRunner', () => `${contextName} end skipped - not available | ${JSON.stringify({
181
+ hasStream: !!stream,
182
+ hasEnd: stream && typeof stream.end === 'function',
183
+ writable: stream?.writable
184
+ }, null, 2)}`);
185
+ return false;
186
+ }
187
+
188
+ try {
189
+ stream.end();
190
+ trace('ProcessRunner', () => `${contextName} ended successfully`);
191
+ return true;
192
+ } catch (error) {
193
+ if (error.code !== 'EPIPE') {
194
+ trace('ProcessRunner', () => `${contextName} end error | ${JSON.stringify({
195
+ error: error.message,
196
+ code: error.code
197
+ }, null, 2)}`);
198
+ } else {
199
+ trace('ProcessRunner', () => `${contextName} EPIPE on end (ignored) | ${JSON.stringify({
200
+ error: error.message,
201
+ code: error.code
202
+ }, null, 2)}`);
203
+ }
204
+ return false;
205
+ }
206
+ },
207
+
208
+ /**
209
+ * Setup comprehensive stdin handling (error handler + safe operations)
210
+ */
211
+ setupStdinHandling(stream, contextName = 'stdin') {
212
+ this.addStdinErrorHandler(stream, contextName);
213
+
214
+ return {
215
+ write: (data) => this.safeStreamWrite(stream, data, contextName),
216
+ end: () => this.safeStreamEnd(stream, contextName),
217
+ isWritable: () => this.isStreamWritable(stream)
218
+ };
219
+ },
220
+
221
+ /**
222
+ * Handle stream errors with consistent EPIPE behavior
223
+ */
224
+ handleStreamError(error, contextName, shouldThrow = true) {
225
+ if (error.code !== 'EPIPE') {
226
+ trace('ProcessRunner', () => `${contextName} error | ${JSON.stringify({
227
+ error: error.message,
228
+ code: error.code,
229
+ isEPIPE: false
230
+ }, null, 2)}`);
231
+ if (shouldThrow) throw error;
232
+ return false;
233
+ } else {
234
+ trace('ProcessRunner', () => `${contextName} EPIPE error (ignored) | ${JSON.stringify({
235
+ error: error.message,
236
+ code: error.code,
237
+ isEPIPE: true
238
+ }, null, 2)}`);
239
+ return true; // EPIPE handled gracefully
240
+ }
241
+ },
242
+
243
+ /**
244
+ * Detect if stream supports Bun-style writing
245
+ */
246
+ isBunStream(stream) {
247
+ return isBun && stream && typeof stream.getWriter === 'function';
248
+ },
249
+
250
+ /**
251
+ * Detect if stream supports Node.js-style writing
252
+ */
253
+ isNodeStream(stream) {
254
+ return stream && typeof stream.write === 'function';
255
+ },
256
+
257
+ /**
258
+ * Write to either Bun or Node.js style stream
259
+ */
260
+ async writeToStream(stream, data, contextName = 'stream') {
261
+ if (this.isBunStream(stream)) {
262
+ try {
263
+ const writer = stream.getWriter();
264
+ await writer.write(data);
265
+ writer.releaseLock();
266
+ return true;
267
+ } catch (error) {
268
+ return this.handleStreamError(error, `${contextName} Bun writer`, false);
269
+ }
270
+ } else if (this.isNodeStream(stream)) {
271
+ try {
272
+ stream.write(data);
273
+ return true;
274
+ } catch (error) {
275
+ return this.handleStreamError(error, `${contextName} Node writer`, false);
276
+ }
277
+ }
278
+ return false;
279
+ }
280
+ };
281
+
282
+ // Virtual command utility functions for consistent behavior and error handling
283
+ const VirtualUtils = {
284
+ /**
285
+ * Create standardized error response for missing operands
286
+ */
287
+ missingOperandError(commandName, customMessage = null) {
288
+ const message = customMessage || `${commandName}: missing operand`;
289
+ return { stderr: message, code: 1 };
290
+ },
291
+
292
+ /**
293
+ * Create standardized error response for invalid arguments
294
+ */
295
+ invalidArgumentError(commandName, message) {
296
+ return { stderr: `${commandName}: ${message}`, code: 1 };
297
+ },
298
+
299
+ /**
300
+ * Create standardized success response
301
+ */
302
+ success(stdout = '', code = 0) {
303
+ return { stdout, stderr: '', code };
304
+ },
305
+
306
+ /**
307
+ * Create standardized error response
308
+ */
309
+ error(stderr = '', code = 1) {
310
+ return { stdout: '', stderr, code };
311
+ },
312
+
313
+ /**
314
+ * Validate that command has required number of arguments
315
+ */
316
+ validateArgs(args, minCount, commandName) {
317
+ if (args.length < minCount) {
318
+ if (minCount === 1) {
319
+ return this.missingOperandError(commandName);
320
+ } else {
321
+ return this.invalidArgumentError(commandName, `requires at least ${minCount} arguments`);
322
+ }
323
+ }
324
+ return null; // No error
325
+ },
326
+
327
+ /**
328
+ * Resolve file path with optional cwd parameter
329
+ */
330
+ resolvePath(filePath, cwd = null) {
331
+ const basePath = cwd || process.cwd();
332
+ return path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
333
+ },
334
+
335
+ /**
336
+ * Safe file system operation wrapper
337
+ */
338
+ async safeFsOperation(operation, errorPrefix) {
339
+ try {
340
+ return await operation();
341
+ } catch (error) {
342
+ return this.error(`${errorPrefix}: ${error.message}`);
343
+ }
344
+ },
345
+
346
+ /**
347
+ * Create async wrapper for Promise-based operations
348
+ */
349
+ createAsyncWrapper(promiseFactory) {
350
+ return new Promise(promiseFactory);
351
+ }
352
+ };
353
+
40
354
  let globalShellSettings = {
41
355
  errexit: false, // set -e equivalent: exit on error
42
356
  verbose: false, // set -v equivalent: print commands
@@ -45,24 +359,20 @@ let globalShellSettings = {
45
359
  nounset: false // set -u equivalent: error on undefined variables
46
360
  };
47
361
 
48
- // Helper function to create result objects with Bun.$ compatibility
49
362
  function createResult({ code, stdout = '', stderr = '', stdin = '' }) {
50
363
  return {
51
364
  code,
52
365
  stdout,
53
366
  stderr,
54
367
  stdin,
55
- // Bun.$ compatibility method
56
368
  async text() {
57
369
  return stdout;
58
370
  }
59
371
  };
60
372
  }
61
373
 
62
- // Virtual command registry - unified system for all commands
63
374
  const virtualCommands = new Map();
64
375
 
65
- // Global flag to enable/disable virtual commands (for backward compatibility)
66
376
  let virtualCommandsEnabled = true;
67
377
 
68
378
  // EventEmitter-like implementation
@@ -76,9 +386,9 @@ class StreamEmitter {
76
386
  this.listeners.set(event, []);
77
387
  }
78
388
  this.listeners.get(event).push(listener);
79
-
389
+
80
390
  // No auto-start - explicit start() or await will start the process
81
-
391
+
82
392
  return this;
83
393
  }
84
394
 
@@ -113,28 +423,28 @@ function quote(value) {
113
423
  }
114
424
 
115
425
  function buildShellCommand(strings, values) {
116
- traceFunc('Utils', 'buildShellCommand', 'ENTER', {
426
+ trace('Utils', () => `buildShellCommand ENTER | ${JSON.stringify({
117
427
  stringsLength: strings.length,
118
428
  valuesLength: values.length
119
- });
120
-
429
+ }, null, 2)}`);
430
+
121
431
  let out = '';
122
432
  for (let i = 0; i < strings.length; i++) {
123
433
  out += strings[i];
124
434
  if (i < values.length) {
125
435
  const v = values[i];
126
436
  if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'raw')) {
127
- traceBranch('Utils', 'buildShellCommand', 'RAW_VALUE', { value: String(v.raw) });
437
+ trace('Utils', () => `BRANCH: buildShellCommand => RAW_VALUE | ${JSON.stringify({ value: String(v.raw) }, null, 2)}`);
128
438
  out += String(v.raw);
129
439
  } else {
130
440
  const quoted = quote(v);
131
- traceBranch('Utils', 'buildShellCommand', 'QUOTED_VALUE', { original: v, quoted });
441
+ trace('Utils', () => `BRANCH: buildShellCommand => QUOTED_VALUE | ${JSON.stringify({ original: v, quoted }, null, 2)}`);
132
442
  out += quoted;
133
443
  }
134
444
  }
135
445
  }
136
-
137
- traceFunc('Utils', 'buildShellCommand', 'EXIT', { command: out });
446
+
447
+ trace('Utils', () => `buildShellCommand EXIT | ${JSON.stringify({ command: out }, null, 2)}`);
138
448
  return out;
139
449
  }
140
450
 
@@ -155,12 +465,12 @@ async function pumpReadable(readable, onChunk) {
155
465
  class ProcessRunner extends StreamEmitter {
156
466
  constructor(spec, options = {}) {
157
467
  super();
158
-
159
- traceFunc('ProcessRunner', 'constructor', 'ENTER', {
468
+
469
+ trace('ProcessRunner', () => `constructor ENTER | ${JSON.stringify({
160
470
  spec: typeof spec === 'object' ? { ...spec, command: spec.command?.slice(0, 100) } : spec,
161
- options
162
- });
163
-
471
+ options
472
+ }, null, 2)}`);
473
+
164
474
  this.spec = spec;
165
475
  this.options = {
166
476
  mirror: true,
@@ -170,168 +480,293 @@ class ProcessRunner extends StreamEmitter {
170
480
  env: undefined,
171
481
  ...options
172
482
  };
173
-
483
+
174
484
  this.outChunks = this.options.capture ? [] : null;
175
485
  this.errChunks = this.options.capture ? [] : null;
176
- this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
177
- this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
178
- [Buffer.from(this.options.stdin)] : [];
179
-
486
+ this.inChunks = this.options.capture && this.options.stdin === 'inherit' ? [] :
487
+ this.options.capture && (typeof this.options.stdin === 'string' || Buffer.isBuffer(this.options.stdin)) ?
488
+ [Buffer.from(this.options.stdin)] : [];
489
+
180
490
  this.result = null;
181
491
  this.child = null;
182
492
  this.started = false;
183
493
  this.finished = false;
184
-
494
+
185
495
  // Promise for awaiting final result
186
496
  this.promise = null;
187
-
188
- // Track the execution mode
497
+
189
498
  this._mode = null; // 'async' or 'sync'
190
-
191
- // Cancellation support for virtual commands
499
+
192
500
  this._cancelled = false;
193
501
  this._virtualGenerator = null;
194
502
  this._abortController = new AbortController();
503
+
504
+ activeProcessRunners.add(this);
505
+
506
+ // Track finished state changes to trigger cleanup
507
+ this._finished = false;
508
+ }
509
+
510
+ get finished() {
511
+ return this._finished;
512
+ }
513
+
514
+ _emitProcessedData(type, buf) {
515
+ const processedBuf = processOutput(buf, this.options.ansi);
516
+ this.emit(type, processedBuf);
517
+ this.emit('data', { type, data: processedBuf });
518
+ }
519
+
520
+ async _forwardTTYStdin() {
521
+ if (!process.stdin.isTTY || !this.child.stdin) {
522
+ return;
523
+ }
524
+
525
+ try {
526
+ // Set raw mode to forward keystrokes immediately
527
+ if (process.stdin.setRawMode) {
528
+ process.stdin.setRawMode(true);
529
+ }
530
+ process.stdin.resume();
531
+
532
+ // Forward stdin data to child process
533
+ const onData = (chunk) => {
534
+ if (this.child.stdin) {
535
+ if (isBun && this.child.stdin.write) {
536
+ this.child.stdin.write(chunk);
537
+ } else if (this.child.stdin.write) {
538
+ this.child.stdin.write(chunk);
539
+ }
540
+ }
541
+ };
542
+
543
+ const cleanup = () => {
544
+ process.stdin.removeListener('data', onData);
545
+ if (process.stdin.setRawMode) {
546
+ process.stdin.setRawMode(false);
547
+ }
548
+ process.stdin.pause();
549
+ };
550
+
551
+ process.stdin.on('data', onData);
552
+
553
+ // Clean up when child process exits
554
+ const childExit = isBun ? this.child.exited : new Promise((resolve) => {
555
+ this.child.once('close', resolve);
556
+ this.child.once('exit', resolve);
557
+ });
558
+
559
+ childExit.then(cleanup).catch(cleanup);
560
+
561
+ return childExit;
562
+ } catch (error) {
563
+ trace('ProcessRunner', () => `TTY stdin forwarding error | ${JSON.stringify({ error: error.message }, null, 2)}`);
564
+ }
565
+ }
566
+
567
+ set finished(value) {
568
+ if (value === true && this._finished === false) {
569
+ this._finished = true;
570
+ this._cleanup(); // Trigger cleanup when process finishes
571
+ } else {
572
+ this._finished = value;
573
+ }
574
+ }
575
+
576
+ _handleParentStreamClosure() {
577
+ if (this.finished || this._cancelled) return;
578
+
579
+ trace('ProcessRunner', () => `Handling parent stream closure | ${JSON.stringify({
580
+ started: this.started,
581
+ hasChild: !!this.child,
582
+ command: this.spec.command?.slice(0, 50) || this.spec.file
583
+ }, null, 2)}`);
584
+
585
+ this._cancelled = true;
586
+
587
+ // Cancel abort controller for virtual commands
588
+ if (this._abortController) {
589
+ this._abortController.abort();
590
+ }
591
+
592
+ // Gracefully close child process if it exists
593
+ if (this.child) {
594
+ try {
595
+ // Close stdin first to signal completion
596
+ if (this.child.stdin && typeof this.child.stdin.end === 'function') {
597
+ this.child.stdin.end();
598
+ } else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
599
+ const writer = this.child.stdin.getWriter();
600
+ writer.close().catch(() => { }); // Ignore close errors
601
+ }
602
+
603
+ setTimeout(() => {
604
+ if (this.child && !this.finished) {
605
+ trace('ProcessRunner', () => 'Terminating child process after parent stream closure');
606
+ if (typeof this.child.kill === 'function') {
607
+ this.child.kill('SIGTERM');
608
+ }
609
+ }
610
+ }, 100);
611
+
612
+ } catch (error) {
613
+ trace('ProcessRunner', () => `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`);
614
+ }
615
+ }
616
+
617
+ activeProcessRunners.delete(this);
618
+ }
619
+
620
+ _cleanup() {
621
+ activeProcessRunners.delete(this);
195
622
  }
196
623
 
197
624
  // Unified start method that can work in both async and sync modes
198
625
  start(options = {}) {
199
626
  const mode = options.mode || 'async';
200
-
201
- traceFunc('ProcessRunner', 'start', 'ENTER', { mode, options });
202
-
627
+
628
+ trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({ mode, options }, null, 2)}`);
629
+
203
630
  if (mode === 'sync') {
204
- traceBranch('ProcessRunner', 'mode', 'sync', {});
631
+ trace('ProcessRunner', () => `BRANCH: mode => sync | ${JSON.stringify({}, null, 2)}`);
205
632
  return this._startSync();
206
633
  } else {
207
- traceBranch('ProcessRunner', 'mode', 'async', {});
634
+ trace('ProcessRunner', () => `BRANCH: mode => async | ${JSON.stringify({}, null, 2)}`);
208
635
  return this._startAsync();
209
636
  }
210
637
  }
211
-
638
+
212
639
  // Shortcut for sync mode
213
640
  sync() {
214
641
  return this.start({ mode: 'sync' });
215
642
  }
216
-
643
+
217
644
  // Shortcut for async mode
218
645
  async() {
219
646
  return this.start({ mode: 'async' });
220
647
  }
221
-
648
+
222
649
  async _startAsync() {
223
650
  if (this.started) return this.promise;
224
651
  if (this.promise) return this.promise;
225
-
652
+
226
653
  this.promise = this._doStartAsync();
227
654
  return this.promise;
228
655
  }
229
-
656
+
230
657
  async _doStartAsync() {
231
- traceFunc('ProcessRunner', '_doStartAsync', 'ENTER', {
658
+ trace('ProcessRunner', () => `_doStartAsync ENTER | ${JSON.stringify({
232
659
  mode: this.spec.mode,
233
660
  command: this.spec.command?.slice(0, 100)
234
- });
235
-
661
+ }, null, 2)}`);
662
+
236
663
  this.started = true;
237
664
  this._mode = 'async';
238
665
 
239
666
  const { cwd, env, stdin } = this.options;
240
-
241
- // Handle programmatic pipeline mode
667
+
242
668
  if (this.spec.mode === 'pipeline') {
243
- traceBranch('ProcessRunner', 'spec.mode', 'pipeline', {
669
+ trace('ProcessRunner', () => `BRANCH: spec.mode => pipeline | ${JSON.stringify({
244
670
  hasSource: !!this.spec.source,
245
671
  hasDestination: !!this.spec.destination
246
- });
672
+ }, null, 2)}`);
247
673
  return await this._runProgrammaticPipeline(this.spec.source, this.spec.destination);
248
674
  }
249
-
250
- // Check if this is a virtual command first
675
+
251
676
  if (this.spec.mode === 'shell') {
252
- traceBranch('ProcessRunner', 'spec.mode', 'shell', {});
253
-
254
- // Parse the command to check for virtual commands or pipelines
677
+ trace('ProcessRunner', () => `BRANCH: spec.mode => shell | ${JSON.stringify({}, null, 2)}`);
678
+
255
679
  const parsed = this._parseCommand(this.spec.command);
256
- trace('ProcessRunner', 'Parsed command', {
680
+ trace('ProcessRunner', () => `Parsed command | ${JSON.stringify({
257
681
  type: parsed?.type,
258
682
  cmd: parsed?.cmd,
259
683
  argsCount: parsed?.args?.length
260
- });
261
-
684
+ }, null, 2)}`);
685
+
262
686
  if (parsed) {
263
687
  if (parsed.type === 'pipeline') {
264
- traceBranch('ProcessRunner', 'parsed.type', 'pipeline', {
265
- commandCount: parsed.commands?.length
266
- });
688
+ trace('ProcessRunner', () => `BRANCH: parsed.type => pipeline | ${JSON.stringify({
689
+ commandCount: parsed.commands?.length
690
+ }, null, 2)}`);
267
691
  return await this._runPipeline(parsed.commands);
268
692
  } else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
269
- traceBranch('ProcessRunner', 'virtualCommand', parsed.cmd, {
693
+ trace('ProcessRunner', () => `BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
270
694
  isVirtual: true,
271
- args: parsed.args
272
- });
695
+ args: parsed.args
696
+ }, null, 2)}`);
273
697
  return await this._runVirtual(parsed.cmd, parsed.args);
274
698
  }
275
699
  }
276
700
  }
277
-
278
- const spawnBun = (argv) => {
279
- return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
280
- };
281
- const spawnNode = async (argv) => {
282
- const cp = await import('child_process');
283
- return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
284
- };
285
701
 
286
702
  const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
287
-
288
- // Shell tracing (set -x equivalent)
703
+
289
704
  if (globalShellSettings.xtrace) {
290
705
  const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
291
706
  console.log(`+ ${traceCmd}`);
292
707
  }
293
-
294
- // Verbose mode (set -v equivalent)
708
+
295
709
  if (globalShellSettings.verbose) {
296
710
  const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
297
711
  console.log(verboseCmd);
298
712
  }
299
-
713
+
714
+ // Detect if this is an interactive command that needs direct TTY access
715
+ // Only activate for interactive commands when we have a real TTY and the command is likely to need it
716
+ const isInteractive = stdin === 'inherit' &&
717
+ process.stdin.isTTY === true &&
718
+ process.stdout.isTTY === true &&
719
+ process.stderr.isTTY === true &&
720
+ (this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file));
721
+
722
+ const spawnBun = (argv) => {
723
+ if (isInteractive) {
724
+ // For interactive commands, use inherit to provide direct TTY access
725
+ return Bun.spawn(argv, { cwd, env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' });
726
+ }
727
+ return Bun.spawn(argv, { cwd, env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
728
+ };
729
+ const spawnNode = async (argv) => {
730
+ if (isInteractive) {
731
+ // For interactive commands, use inherit to provide direct TTY access
732
+ return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: 'inherit' });
733
+ }
734
+ return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
735
+ };
736
+
300
737
  const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
301
738
  const preferNodeForInput = isBun && needsExplicitPipe;
302
739
  this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
303
740
 
304
- // Setup stdout streaming
305
- const outPump = pumpReadable(this.child.stdout, async (buf) => {
741
+ // For interactive commands with stdio: 'inherit', stdout/stderr will be null
742
+ const outPump = this.child.stdout ? pumpReadable(this.child.stdout, async (buf) => {
306
743
  if (this.options.capture) this.outChunks.push(buf);
307
- if (this.options.mirror) process.stdout.write(buf);
308
-
744
+ if (this.options.mirror) safeWrite(process.stdout, buf);
745
+
309
746
  // Emit chunk events
310
- this.emit('stdout', buf);
311
- this.emit('data', { type: 'stdout', data: buf });
312
- });
747
+ this._emitProcessedData('stdout', buf);
748
+ }) : Promise.resolve();
313
749
 
314
- // Setup stderr streaming
315
- const errPump = pumpReadable(this.child.stderr, async (buf) => {
750
+ const errPump = this.child.stderr ? pumpReadable(this.child.stderr, async (buf) => {
316
751
  if (this.options.capture) this.errChunks.push(buf);
317
- if (this.options.mirror) process.stderr.write(buf);
318
-
752
+ if (this.options.mirror) safeWrite(process.stderr, buf);
753
+
319
754
  // Emit chunk events
320
- this.emit('stderr', buf);
321
- this.emit('data', { type: 'stderr', data: buf });
322
- });
755
+ this._emitProcessedData('stderr', buf);
756
+ }) : Promise.resolve();
323
757
 
324
- // Handle stdin
325
758
  let stdinPumpPromise = Promise.resolve();
326
759
  if (stdin === 'inherit') {
327
- const isPipedIn = process.stdin && process.stdin.isTTY === false;
328
- if (isPipedIn) {
329
- stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
760
+ if (isInteractive) {
761
+ // For interactive commands with stdio: 'inherit', stdin is handled automatically
762
+ stdinPumpPromise = Promise.resolve();
330
763
  } else {
331
- if (this.child.stdin && typeof this.child.stdin.end === 'function') {
332
- try { this.child.stdin.end(); } catch {}
333
- } else if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
334
- try { const w = this.child.stdin.getWriter(); await w.close(); } catch {}
764
+ const isPipedIn = process.stdin && process.stdin.isTTY === false;
765
+ if (isPipedIn) {
766
+ stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
767
+ } else {
768
+ // For TTY (interactive terminal), forward stdin directly for non-interactive commands
769
+ stdinPumpPromise = this._forwardTTYStdin();
335
770
  }
336
771
  }
337
772
  } else if (stdin === 'ignore') {
@@ -347,12 +782,12 @@ class ProcessRunner extends StreamEmitter {
347
782
  await Promise.all([outPump, errPump, stdinPumpPromise]);
348
783
 
349
784
  // Debug: Check the raw exit code
350
- trace('ProcessRunner', 'Raw exit code from child', {
785
+ trace('ProcessRunner', () => `Raw exit code from child | ${JSON.stringify({
351
786
  code,
352
787
  codeType: typeof code,
353
788
  childExitCode: this.child?.exitCode,
354
789
  isBun
355
- });
790
+ }, null, 2)}`);
356
791
 
357
792
  const resultData = {
358
793
  code: code ?? 0, // Default to 0 if exit code is null/undefined
@@ -361,10 +796,9 @@ class ProcessRunner extends StreamEmitter {
361
796
  stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
362
797
  child: this.child
363
798
  };
364
-
799
+
365
800
  this.result = {
366
801
  ...resultData,
367
- // Bun.$ compatibility method
368
802
  async text() {
369
803
  return resultData.stdout || '';
370
804
  }
@@ -373,8 +807,7 @@ class ProcessRunner extends StreamEmitter {
373
807
  this.finished = true;
374
808
  this.emit('end', this.result);
375
809
  this.emit('exit', this.result.code);
376
-
377
- // Handle shell settings (set -e equivalent)
810
+
378
811
  if (globalShellSettings.errexit && this.result.code !== 0) {
379
812
  const error = new Error(`Command failed with exit code ${this.result.code}`);
380
813
  error.code = this.result.code;
@@ -383,7 +816,7 @@ class ProcessRunner extends StreamEmitter {
383
816
  error.result = this.result;
384
817
  throw error;
385
818
  }
386
-
819
+
387
820
  return this.result;
388
821
  }
389
822
 
@@ -394,7 +827,11 @@ class ProcessRunner extends StreamEmitter {
394
827
  const buf = asBuffer(chunk);
395
828
  captureChunks && captureChunks.push(buf);
396
829
  if (bunWriter) await bunWriter.write(buf);
397
- else if (typeof child.stdin.write === 'function') child.stdin.write(buf);
830
+ else if (typeof child.stdin.write === 'function') {
831
+ // Use StreamUtils for consistent stdin handling
832
+ StreamUtils.addStdinErrorHandler(child.stdin, 'child stdin buffer');
833
+ StreamUtils.safeStreamWrite(child.stdin, buf, 'child stdin buffer');
834
+ }
398
835
  else if (isBun && typeof Bun.write === 'function') await Bun.write(child.stdin, buf);
399
836
  }
400
837
  if (bunWriter) await bunWriter.close();
@@ -402,13 +839,14 @@ class ProcessRunner extends StreamEmitter {
402
839
  }
403
840
 
404
841
  async _writeToStdin(buf) {
405
- if (isBun && this.child.stdin && typeof this.child.stdin.getWriter === 'function') {
406
- const w = this.child.stdin.getWriter();
407
- const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
408
- await w.write(bytes);
409
- await w.close();
410
- } else if (this.child.stdin && typeof this.child.stdin.write === 'function') {
411
- this.child.stdin.end(buf);
842
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
843
+ if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
844
+ // Successfully wrote to stream
845
+ if (StreamUtils.isBunStream(this.child.stdin)) {
846
+ // Stream was already closed by writeToStream utility
847
+ } else if (StreamUtils.isNodeStream(this.child.stdin)) {
848
+ try { this.child.stdin.end(); } catch { }
849
+ }
412
850
  } else if (isBun && typeof Bun.write === 'function') {
413
851
  await Bun.write(this.child.stdin, buf);
414
852
  }
@@ -417,26 +855,25 @@ class ProcessRunner extends StreamEmitter {
417
855
  _parseCommand(command) {
418
856
  const trimmed = command.trim();
419
857
  if (!trimmed) return null;
420
-
421
- // Check for pipes
858
+
422
859
  if (trimmed.includes('|')) {
423
860
  return this._parsePipeline(trimmed);
424
861
  }
425
-
862
+
426
863
  // Simple command parsing
427
864
  const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
428
865
  if (parts.length === 0) return null;
429
-
866
+
430
867
  const cmd = parts[0];
431
868
  const args = parts.slice(1).map(arg => {
432
869
  // Keep track of whether the arg was quoted
433
- if ((arg.startsWith('"') && arg.endsWith('"')) ||
434
- (arg.startsWith("'") && arg.endsWith("'"))) {
870
+ if ((arg.startsWith('"') && arg.endsWith('"')) ||
871
+ (arg.startsWith("'") && arg.endsWith("'"))) {
435
872
  return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
436
873
  }
437
874
  return { value: arg, quoted: false };
438
875
  });
439
-
876
+
440
877
  return { cmd, args, type: 'simple' };
441
878
  }
442
879
 
@@ -446,10 +883,10 @@ class ProcessRunner extends StreamEmitter {
446
883
  let current = '';
447
884
  let inQuotes = false;
448
885
  let quoteChar = '';
449
-
886
+
450
887
  for (let i = 0; i < command.length; i++) {
451
888
  const char = command[i];
452
-
889
+
453
890
  if (!inQuotes && (char === '"' || char === "'")) {
454
891
  inQuotes = true;
455
892
  quoteChar = char;
@@ -465,46 +902,44 @@ class ProcessRunner extends StreamEmitter {
465
902
  current += char;
466
903
  }
467
904
  }
468
-
905
+
469
906
  if (current.trim()) {
470
907
  segments.push(current.trim());
471
908
  }
472
-
473
- // Parse each segment as a simple command
909
+
474
910
  const commands = segments.map(segment => {
475
911
  const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
476
912
  if (parts.length === 0) return null;
477
-
913
+
478
914
  const cmd = parts[0];
479
915
  const args = parts.slice(1).map(arg => {
480
916
  // Keep track of whether the arg was quoted
481
- if ((arg.startsWith('"') && arg.endsWith('"')) ||
482
- (arg.startsWith("'") && arg.endsWith("'"))) {
483
- // Store the original with quotes for system commands
917
+ if ((arg.startsWith('"') && arg.endsWith('"')) ||
918
+ (arg.startsWith("'") && arg.endsWith("'"))) {
484
919
  return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
485
920
  }
486
921
  return { value: arg, quoted: false };
487
922
  });
488
-
923
+
489
924
  return { cmd, args };
490
925
  }).filter(Boolean);
491
-
926
+
492
927
  return { type: 'pipeline', commands };
493
928
  }
494
929
 
495
930
  async _runVirtual(cmd, args) {
496
- traceFunc('ProcessRunner', '_runVirtual', 'ENTER', { cmd, args });
497
-
931
+ trace('ProcessRunner', () => `_runVirtual ENTER | ${JSON.stringify({ cmd, args }, null, 2)}`);
932
+
498
933
  const handler = virtualCommands.get(cmd);
499
934
  if (!handler) {
500
- trace('ProcessRunner', 'Virtual command not found', { cmd });
935
+ trace('ProcessRunner', () => `Virtual command not found | ${JSON.stringify({ cmd }, null, 2)}`);
501
936
  throw new Error(`Virtual command not found: ${cmd}`);
502
937
  }
503
938
 
504
- trace('ProcessRunner', 'Found virtual command handler', {
939
+ trace('ProcessRunner', () => `Found virtual command handler | ${JSON.stringify({
505
940
  cmd,
506
941
  isGenerator: handler.constructor.name === 'AsyncGeneratorFunction'
507
- });
942
+ }, null, 2)}`);
508
943
 
509
944
  try {
510
945
  // Prepare stdin
@@ -526,40 +961,34 @@ class ProcessRunner extends StreamEmitter {
526
961
  console.log(`${cmd} ${argValues.join(' ')}`);
527
962
  }
528
963
 
529
- // Execute the virtual command
530
964
  let result;
531
-
532
- // Check if handler is async generator (streaming)
965
+
533
966
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
534
- // Handle streaming virtual command with cancellation support
535
967
  const chunks = [];
536
-
537
- // Create options with cancellation check and abort signal
968
+
538
969
  const commandOptions = {
539
970
  ...this.options,
540
971
  isCancelled: () => this._cancelled,
541
972
  signal: this._abortController.signal
542
973
  };
543
-
544
- const generator = handler(argValues, stdinData, commandOptions);
974
+
975
+ const generator = handler({ args: argValues, stdin: stdinData, ...commandOptions });
545
976
  this._virtualGenerator = generator;
546
-
547
- // Create a promise that resolves when cancelled
977
+
548
978
  const cancelPromise = new Promise(resolve => {
549
979
  this._cancelResolve = resolve;
550
980
  });
551
-
981
+
552
982
  try {
553
983
  const iterator = generator[Symbol.asyncIterator]();
554
984
  let done = false;
555
-
985
+
556
986
  while (!done && !this._cancelled) {
557
- // Race between getting next value and cancellation
558
987
  const result = await Promise.race([
559
988
  iterator.next(),
560
989
  cancelPromise.then(() => ({ done: true, cancelled: true }))
561
990
  ]);
562
-
991
+
563
992
  if (result.cancelled || this._cancelled) {
564
993
  // Cancelled - close the generator
565
994
  if (iterator.return) {
@@ -567,22 +996,21 @@ class ProcessRunner extends StreamEmitter {
567
996
  }
568
997
  break;
569
998
  }
570
-
999
+
571
1000
  done = result.done;
572
-
1001
+
573
1002
  if (!done) {
574
1003
  const chunk = result.value;
575
1004
  const buf = Buffer.from(chunk);
576
1005
  chunks.push(buf);
577
-
1006
+
578
1007
  // Only output if not cancelled
579
1008
  if (!this._cancelled) {
580
1009
  if (this.options.mirror) {
581
- process.stdout.write(buf);
1010
+ safeWrite(process.stdout, buf);
582
1011
  }
583
-
584
- this.emit('stdout', buf);
585
- this.emit('data', { type: 'stdout', data: buf });
1012
+
1013
+ this._emitProcessedData('stdout', buf);
586
1014
  }
587
1015
  }
588
1016
  }
@@ -591,7 +1019,7 @@ class ProcessRunner extends StreamEmitter {
591
1019
  this._virtualGenerator = null;
592
1020
  this._cancelResolve = null;
593
1021
  }
594
-
1022
+
595
1023
  result = {
596
1024
  code: 0,
597
1025
  stdout: this.options.capture ? Buffer.concat(chunks).toString('utf8') : undefined,
@@ -600,9 +1028,8 @@ class ProcessRunner extends StreamEmitter {
600
1028
  };
601
1029
  } else {
602
1030
  // Regular async function
603
- result = await handler(argValues, stdinData, this.options);
604
-
605
- // Ensure result has required fields, respecting capture option
1031
+ result = await handler({ args: argValues, stdin: stdinData, ...this.options });
1032
+
606
1033
  result = {
607
1034
  code: result.code ?? 0,
608
1035
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
@@ -610,36 +1037,33 @@ class ProcessRunner extends StreamEmitter {
610
1037
  stdin: this.options.capture ? stdinData : undefined,
611
1038
  ...result
612
1039
  };
613
-
1040
+
614
1041
  // Mirror and emit output
615
1042
  if (result.stdout) {
616
1043
  const buf = Buffer.from(result.stdout);
617
1044
  if (this.options.mirror) {
618
- process.stdout.write(buf);
1045
+ safeWrite(process.stdout, buf);
619
1046
  }
620
- this.emit('stdout', buf);
621
- this.emit('data', { type: 'stdout', data: buf });
1047
+ this._emitProcessedData('stdout', buf);
622
1048
  }
623
-
1049
+
624
1050
  if (result.stderr) {
625
1051
  const buf = Buffer.from(result.stderr);
626
1052
  if (this.options.mirror) {
627
- process.stderr.write(buf);
1053
+ safeWrite(process.stderr, buf);
628
1054
  }
629
- this.emit('stderr', buf);
630
- this.emit('data', { type: 'stderr', data: buf });
1055
+ this._emitProcessedData('stderr', buf);
631
1056
  }
632
1057
  }
633
1058
 
634
1059
  // Store result
635
1060
  this.result = result;
636
1061
  this.finished = true;
637
-
1062
+
638
1063
  // Emit completion events
639
1064
  this.emit('end', result);
640
1065
  this.emit('exit', result.code);
641
-
642
- // Handle shell settings
1066
+
643
1067
  if (globalShellSettings.errexit && result.code !== 0) {
644
1068
  const error = new Error(`Command failed with exit code ${result.code}`);
645
1069
  error.code = result.code;
@@ -648,87 +1072,84 @@ class ProcessRunner extends StreamEmitter {
648
1072
  error.result = result;
649
1073
  throw error;
650
1074
  }
651
-
1075
+
652
1076
  return result;
653
1077
  } catch (error) {
654
- // Handle errors from virtual commands
655
1078
  const result = {
656
1079
  code: error.code ?? 1,
657
1080
  stdout: error.stdout ?? '',
658
1081
  stderr: error.stderr ?? error.message,
659
1082
  stdin: ''
660
1083
  };
661
-
1084
+
662
1085
  this.result = result;
663
1086
  this.finished = true;
664
-
1087
+
665
1088
  if (result.stderr) {
666
1089
  const buf = Buffer.from(result.stderr);
667
1090
  if (this.options.mirror) {
668
- process.stderr.write(buf);
1091
+ safeWrite(process.stderr, buf);
669
1092
  }
670
- this.emit('stderr', buf);
671
- this.emit('data', { type: 'stderr', data: buf });
1093
+ this._emitProcessedData('stderr', buf);
672
1094
  }
673
-
1095
+
674
1096
  this.emit('end', result);
675
1097
  this.emit('exit', result.code);
676
-
1098
+
677
1099
  if (globalShellSettings.errexit) {
678
1100
  throw error;
679
1101
  }
680
-
1102
+
681
1103
  return result;
682
1104
  }
683
1105
  }
684
1106
 
685
1107
  async _runStreamingPipelineBun(commands) {
686
- traceFunc('ProcessRunner', '_runStreamingPipelineBun', 'ENTER', {
687
- commandsCount: commands.length
688
- });
689
-
1108
+ trace('ProcessRunner', () => `_runStreamingPipelineBun ENTER | ${JSON.stringify({
1109
+ commandsCount: commands.length
1110
+ }, null, 2)}`);
1111
+
690
1112
  // For true streaming, we need to handle virtual and real commands differently
691
- // but make them work together seamlessly
692
-
1113
+
693
1114
  // First, analyze the pipeline to identify virtual vs real commands
694
1115
  const pipelineInfo = commands.map(command => {
695
1116
  const { cmd, args } = command;
696
1117
  const isVirtual = virtualCommandsEnabled && virtualCommands.has(cmd);
697
1118
  return { ...command, isVirtual };
698
1119
  });
699
-
700
- trace('ProcessRunner', 'Pipeline analysis', {
1120
+
1121
+ trace('ProcessRunner', () => `Pipeline analysis | ${JSON.stringify({
701
1122
  virtualCount: pipelineInfo.filter(p => p.isVirtual).length,
702
- realCount: pipelineInfo.filter(p => !p.isVirtual).length
703
- });
704
-
1123
+ realCount: pipelineInfo.filter(p => !p.isVirtual).length
1124
+ }, null, 2)}`);
1125
+
705
1126
  // If pipeline contains virtual commands, use advanced streaming
706
1127
  if (pipelineInfo.some(info => info.isVirtual)) {
707
- traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'MIXED_PIPELINE', {});
1128
+ trace('ProcessRunner', () => `BRANCH: _runStreamingPipelineBun => MIXED_PIPELINE | ${JSON.stringify({}, null, 2)}`);
708
1129
  return this._runMixedStreamingPipeline(commands);
709
1130
  }
710
-
1131
+
711
1132
  // For pipelines with commands that buffer (like jq), use tee streaming
712
- const needsStreamingWorkaround = commands.some(c =>
1133
+ const needsStreamingWorkaround = commands.some(c =>
713
1134
  c.cmd === 'jq' || c.cmd === 'grep' || c.cmd === 'sed' || c.cmd === 'cat' || c.cmd === 'awk'
714
1135
  );
715
1136
  if (needsStreamingWorkaround) {
716
- traceBranch('ProcessRunner', '_runStreamingPipelineBun', 'TEE_STREAMING', {
717
- bufferedCommands: commands.filter(c =>
1137
+ trace('ProcessRunner', () => `BRANCH: _runStreamingPipelineBun => TEE_STREAMING | ${JSON.stringify({
1138
+ bufferedCommands: commands.filter(c =>
718
1139
  ['jq', 'grep', 'sed', 'cat', 'awk'].includes(c.cmd)
719
1140
  ).map(c => c.cmd)
720
- });
1141
+ }, null, 2)}`);
721
1142
  return this._runTeeStreamingPipeline(commands);
722
1143
  }
723
-
1144
+
724
1145
  // All real commands - use native pipe connections
725
1146
  const processes = [];
726
1147
  let allStderr = '';
727
-
1148
+
728
1149
  for (let i = 0; i < commands.length; i++) {
729
1150
  const command = commands[i];
730
1151
  const { cmd, args } = command;
731
-
1152
+
732
1153
  // Build command string
733
1154
  const commandParts = [cmd];
734
1155
  for (const arg of args) {
@@ -749,12 +1170,12 @@ class ProcessRunner extends StreamEmitter {
749
1170
  }
750
1171
  }
751
1172
  const commandStr = commandParts.join(' ');
752
-
1173
+
753
1174
  // Determine stdin for this process
754
1175
  let stdin;
755
1176
  let needsManualStdin = false;
756
1177
  let stdinData;
757
-
1178
+
758
1179
  if (i === 0) {
759
1180
  // First command - use provided stdin or pipe
760
1181
  if (this.options.stdin && typeof this.options.stdin === 'string') {
@@ -772,18 +1193,17 @@ class ProcessRunner extends StreamEmitter {
772
1193
  // Connect to previous process stdout
773
1194
  stdin = processes[i - 1].stdout;
774
1195
  }
775
-
776
- // Spawn the process directly (not through sh) for better streaming
1196
+
777
1197
  // 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
1198
+ const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
1199
+ commandStr.includes('>') || commandStr.includes('<') ||
1200
+ commandStr.includes('&&') || commandStr.includes('||') ||
1201
+ commandStr.includes(';') || commandStr.includes('`');
1202
+
1203
+ const spawnArgs = needsShell
784
1204
  ? ['sh', '-c', commandStr]
785
1205
  : [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
786
-
1206
+
787
1207
  const proc = Bun.spawn(spawnArgs, {
788
1208
  cwd: this.options.cwd,
789
1209
  env: this.options.env,
@@ -791,22 +1211,28 @@ class ProcessRunner extends StreamEmitter {
791
1211
  stdout: 'pipe',
792
1212
  stderr: 'pipe'
793
1213
  });
794
-
1214
+
795
1215
  // Write stdin data if needed for first process
796
1216
  if (needsManualStdin && stdinData && proc.stdin) {
1217
+ // Use StreamUtils for consistent stdin handling
1218
+ const stdinHandler = StreamUtils.setupStdinHandling(proc.stdin, 'Bun process stdin');
1219
+
797
1220
  (async () => {
798
1221
  try {
799
- // Bun's FileSink has write and end methods
800
- await proc.stdin.write(stdinData);
801
- await proc.stdin.end();
1222
+ if (stdinHandler.isWritable()) {
1223
+ await proc.stdin.write(stdinData); // Bun's FileSink async write
1224
+ await proc.stdin.end();
1225
+ }
802
1226
  } catch (e) {
803
- console.error('Error writing stdin:', e);
1227
+ if (e.code !== 'EPIPE') {
1228
+ trace('ProcessRunner', () => `Error with Bun stdin async operations | ${JSON.stringify({ error: e.message, code: e.code }, null, 2)}`);
1229
+ }
804
1230
  }
805
1231
  })();
806
1232
  }
807
-
1233
+
808
1234
  processes.push(proc);
809
-
1235
+
810
1236
  // Collect stderr from all processes
811
1237
  (async () => {
812
1238
  for await (const chunk of proc.stderr) {
@@ -815,35 +1241,32 @@ class ProcessRunner extends StreamEmitter {
815
1241
  // Only emit stderr for the last command
816
1242
  if (i === commands.length - 1) {
817
1243
  if (this.options.mirror) {
818
- process.stderr.write(buf);
1244
+ safeWrite(process.stderr, buf);
819
1245
  }
820
- this.emit('stderr', buf);
821
- this.emit('data', { type: 'stderr', data: buf });
1246
+ this._emitProcessedData('stderr', buf);
822
1247
  }
823
1248
  }
824
1249
  })();
825
1250
  }
826
-
1251
+
827
1252
  // Stream output from the last process
828
1253
  const lastProc = processes[processes.length - 1];
829
1254
  let finalOutput = '';
830
-
1255
+
831
1256
  // Stream stdout from last process
832
1257
  for await (const chunk of lastProc.stdout) {
833
1258
  const buf = Buffer.from(chunk);
834
1259
  finalOutput += buf.toString();
835
1260
  if (this.options.mirror) {
836
- process.stdout.write(buf);
1261
+ safeWrite(process.stdout, buf);
837
1262
  }
838
- this.emit('stdout', buf);
839
- this.emit('data', { type: 'stdout', data: buf });
1263
+ this._emitProcessedData('stdout', buf);
840
1264
  }
841
-
1265
+
842
1266
  // Wait for all processes to complete
843
1267
  const exitCodes = await Promise.all(processes.map(p => p.exited));
844
1268
  const lastExitCode = exitCodes[exitCodes.length - 1];
845
-
846
- // Check for pipeline failures if pipefail is set
1269
+
847
1270
  if (globalShellSettings.pipefail) {
848
1271
  const failedIndex = exitCodes.findIndex(code => code !== 0);
849
1272
  if (failedIndex !== -1) {
@@ -852,21 +1275,21 @@ class ProcessRunner extends StreamEmitter {
852
1275
  throw error;
853
1276
  }
854
1277
  }
855
-
1278
+
856
1279
  const result = createResult({
857
1280
  code: lastExitCode || 0,
858
1281
  stdout: finalOutput,
859
1282
  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') : ''
1283
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1284
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
862
1285
  });
863
-
1286
+
864
1287
  this.result = result;
865
1288
  this.finished = true;
866
-
1289
+
867
1290
  this.emit('end', result);
868
1291
  this.emit('exit', result.code);
869
-
1292
+
870
1293
  if (globalShellSettings.errexit && result.code !== 0) {
871
1294
  const error = new Error(`Pipeline failed with exit code ${result.code}`);
872
1295
  error.code = result.code;
@@ -875,26 +1298,26 @@ class ProcessRunner extends StreamEmitter {
875
1298
  error.result = result;
876
1299
  throw error;
877
1300
  }
878
-
1301
+
879
1302
  return result;
880
1303
  }
881
1304
 
882
1305
  async _runTeeStreamingPipeline(commands) {
883
- traceFunc('ProcessRunner', '_runTeeStreamingPipeline', 'ENTER', {
884
- commandsCount: commands.length
885
- });
886
-
1306
+ trace('ProcessRunner', () => `_runTeeStreamingPipeline ENTER | ${JSON.stringify({
1307
+ commandsCount: commands.length
1308
+ }, null, 2)}`);
1309
+
887
1310
  // Use tee() to split streams for real-time reading
888
1311
  // This works around jq and similar commands that buffer when piped
889
-
1312
+
890
1313
  const processes = [];
891
1314
  let allStderr = '';
892
1315
  let currentStream = null;
893
-
1316
+
894
1317
  for (let i = 0; i < commands.length; i++) {
895
1318
  const command = commands[i];
896
1319
  const { cmd, args } = command;
897
-
1320
+
898
1321
  // Build command string
899
1322
  const commandParts = [cmd];
900
1323
  for (const arg of args) {
@@ -915,12 +1338,12 @@ class ProcessRunner extends StreamEmitter {
915
1338
  }
916
1339
  }
917
1340
  const commandStr = commandParts.join(' ');
918
-
1341
+
919
1342
  // Determine stdin for this process
920
1343
  let stdin;
921
1344
  let needsManualStdin = false;
922
1345
  let stdinData;
923
-
1346
+
924
1347
  if (i === 0) {
925
1348
  // First command - use provided stdin or ignore
926
1349
  if (this.options.stdin && typeof this.options.stdin === 'string') {
@@ -935,20 +1358,18 @@ class ProcessRunner extends StreamEmitter {
935
1358
  stdin = 'ignore';
936
1359
  }
937
1360
  } else {
938
- // Use the stream from previous process
939
1361
  stdin = currentStream;
940
1362
  }
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
1363
+
1364
+ const needsShell = commandStr.includes('*') || commandStr.includes('$') ||
1365
+ commandStr.includes('>') || commandStr.includes('<') ||
1366
+ commandStr.includes('&&') || commandStr.includes('||') ||
1367
+ commandStr.includes(';') || commandStr.includes('`');
1368
+
1369
+ const spawnArgs = needsShell
949
1370
  ? ['sh', '-c', commandStr]
950
1371
  : [cmd, ...args.map(a => a.value !== undefined ? a.value : a)];
951
-
1372
+
952
1373
  const proc = Bun.spawn(spawnArgs, {
953
1374
  cwd: this.options.cwd,
954
1375
  env: this.options.env,
@@ -956,50 +1377,41 @@ class ProcessRunner extends StreamEmitter {
956
1377
  stdout: 'pipe',
957
1378
  stderr: 'pipe'
958
1379
  });
959
-
1380
+
960
1381
  // Write stdin data if needed for first process
961
1382
  if (needsManualStdin && stdinData && proc.stdin) {
1383
+ // Use StreamUtils for consistent stdin handling
1384
+ const stdinHandler = StreamUtils.setupStdinHandling(proc.stdin, 'Node process stdin');
1385
+
962
1386
  try {
963
- await proc.stdin.write(stdinData);
964
- await proc.stdin.end();
1387
+ if (stdinHandler.isWritable()) {
1388
+ await proc.stdin.write(stdinData); // Node async write
1389
+ await proc.stdin.end();
1390
+ }
965
1391
  } catch (e) {
966
- // Ignore stdin errors
1392
+ if (e.code !== 'EPIPE') {
1393
+ trace('ProcessRunner', () => `Error with Node stdin async operations | ${JSON.stringify({ error: e.message, code: e.code }, null, 2)}`);
1394
+ }
967
1395
  }
968
1396
  }
969
-
1397
+
970
1398
  processes.push(proc);
971
-
1399
+
972
1400
  // For non-last processes, tee the output so we can both pipe and read
973
1401
  if (i < commands.length - 1) {
974
1402
  const [readStream, pipeStream] = proc.stdout.tee();
975
1403
  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
- }
1404
+
1405
+ // Read from the tee'd stream to keep it flowing
1406
+ (async () => {
1407
+ for await (const chunk of readStream) {
1408
+ // Just consume to keep flowing - don't emit intermediate output
1409
+ }
1410
+ })();
999
1411
  } else {
1000
1412
  currentStream = proc.stdout;
1001
1413
  }
1002
-
1414
+
1003
1415
  // Collect stderr from all processes
1004
1416
  (async () => {
1005
1417
  for await (const chunk of proc.stderr) {
@@ -1007,39 +1419,32 @@ class ProcessRunner extends StreamEmitter {
1007
1419
  allStderr += buf.toString();
1008
1420
  if (i === commands.length - 1) {
1009
1421
  if (this.options.mirror) {
1010
- process.stderr.write(buf);
1422
+ safeWrite(process.stderr, buf);
1011
1423
  }
1012
- this.emit('stderr', buf);
1013
- this.emit('data', { type: 'stderr', data: buf });
1424
+ this._emitProcessedData('stderr', buf);
1014
1425
  }
1015
1426
  }
1016
1427
  })();
1017
1428
  }
1018
-
1429
+
1019
1430
  // Read final output from the last process
1020
1431
  const lastProc = processes[processes.length - 1];
1021
1432
  let finalOutput = '';
1022
-
1023
- // If we haven't emitted stdout yet (no tee), emit from last process
1024
- const shouldEmitFromLast = commands.length === 1;
1025
-
1433
+
1434
+ // Always emit from the last process for proper pipeline output
1026
1435
  for await (const chunk of lastProc.stdout) {
1027
1436
  const buf = Buffer.from(chunk);
1028
1437
  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 });
1438
+ if (this.options.mirror) {
1439
+ safeWrite(process.stdout, buf);
1035
1440
  }
1441
+ this._emitProcessedData('stdout', buf);
1036
1442
  }
1037
-
1443
+
1038
1444
  // Wait for all processes to complete
1039
1445
  const exitCodes = await Promise.all(processes.map(p => p.exited));
1040
1446
  const lastExitCode = exitCodes[exitCodes.length - 1];
1041
-
1042
- // Check for pipeline failures if pipefail is set
1447
+
1043
1448
  if (globalShellSettings.pipefail) {
1044
1449
  const failedIndex = exitCodes.findIndex(code => code !== 0);
1045
1450
  if (failedIndex !== -1) {
@@ -1048,21 +1453,21 @@ class ProcessRunner extends StreamEmitter {
1048
1453
  throw error;
1049
1454
  }
1050
1455
  }
1051
-
1456
+
1052
1457
  const result = createResult({
1053
1458
  code: lastExitCode || 0,
1054
1459
  stdout: finalOutput,
1055
1460
  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') : ''
1461
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1462
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1058
1463
  });
1059
-
1464
+
1060
1465
  this.result = result;
1061
1466
  this.finished = true;
1062
-
1467
+
1063
1468
  this.emit('end', result);
1064
1469
  this.emit('exit', result.code);
1065
-
1470
+
1066
1471
  if (globalShellSettings.errexit && result.code !== 0) {
1067
1472
  const error = new Error(`Pipeline failed with exit code ${result.code}`);
1068
1473
  error.code = result.code;
@@ -1071,30 +1476,27 @@ class ProcessRunner extends StreamEmitter {
1071
1476
  error.result = result;
1072
1477
  throw error;
1073
1478
  }
1074
-
1479
+
1075
1480
  return result;
1076
1481
  }
1077
1482
 
1078
1483
 
1079
1484
  async _runMixedStreamingPipeline(commands) {
1080
- traceFunc('ProcessRunner', '_runMixedStreamingPipeline', 'ENTER', {
1081
- commandsCount: commands.length
1082
- });
1083
-
1084
- // Handle pipelines with both virtual and real commands
1485
+ trace('ProcessRunner', () => `_runMixedStreamingPipeline ENTER | ${JSON.stringify({
1486
+ commandsCount: commands.length
1487
+ }, null, 2)}`);
1488
+
1085
1489
  // Each stage reads from previous stage's output stream
1086
-
1490
+
1087
1491
  let currentInputStream = null;
1088
1492
  let finalOutput = '';
1089
1493
  let allStderr = '';
1090
-
1091
- // Set up initial input stream if provided
1494
+
1092
1495
  if (this.options.stdin) {
1093
- const inputData = typeof this.options.stdin === 'string'
1094
- ? this.options.stdin
1496
+ const inputData = typeof this.options.stdin === 'string'
1497
+ ? this.options.stdin
1095
1498
  : this.options.stdin.toString('utf8');
1096
-
1097
- // Create a readable stream from the input
1499
+
1098
1500
  currentInputStream = new ReadableStream({
1099
1501
  start(controller) {
1100
1502
  controller.enqueue(new TextEncoder().encode(inputData));
@@ -1102,21 +1504,20 @@ class ProcessRunner extends StreamEmitter {
1102
1504
  }
1103
1505
  });
1104
1506
  }
1105
-
1507
+
1106
1508
  for (let i = 0; i < commands.length; i++) {
1107
1509
  const command = commands[i];
1108
1510
  const { cmd, args } = command;
1109
1511
  const isLastCommand = i === commands.length - 1;
1110
-
1512
+
1111
1513
  if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
1112
- // Handle virtual command with streaming
1113
- traceBranch('ProcessRunner', '_runMixedStreamingPipeline', 'VIRTUAL_COMMAND', {
1514
+ trace('ProcessRunner', () => `BRANCH: _runMixedStreamingPipeline => VIRTUAL_COMMAND | ${JSON.stringify({
1114
1515
  cmd,
1115
- commandIndex: i
1116
- });
1516
+ commandIndex: i
1517
+ }, null, 2)}`);
1117
1518
  const handler = virtualCommands.get(cmd);
1118
1519
  const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
1119
-
1520
+
1120
1521
  // Read input from stream if available
1121
1522
  let inputData = '';
1122
1523
  if (currentInputStream) {
@@ -1131,30 +1532,29 @@ class ProcessRunner extends StreamEmitter {
1131
1532
  reader.releaseLock();
1132
1533
  }
1133
1534
  }
1134
-
1135
- // Check if handler is async generator (streaming)
1535
+
1136
1536
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
1137
- // Create output stream from generator
1138
1537
  const chunks = [];
1139
1538
  const self = this; // Capture this context
1140
1539
  currentInputStream = new ReadableStream({
1141
1540
  async start(controller) {
1142
- for await (const chunk of handler(argValues, inputData, {})) {
1541
+ const { stdin: _, ...optionsWithoutStdin } = self.options;
1542
+ for await (const chunk of handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin })) {
1143
1543
  const data = Buffer.from(chunk);
1144
1544
  controller.enqueue(data);
1145
-
1545
+
1146
1546
  // Emit for last command
1147
1547
  if (isLastCommand) {
1148
1548
  chunks.push(data);
1149
1549
  if (self.options.mirror) {
1150
- process.stdout.write(data);
1550
+ safeWrite(process.stdout, data);
1151
1551
  }
1152
1552
  self.emit('stdout', data);
1153
1553
  self.emit('data', { type: 'stdout', data });
1154
1554
  }
1155
1555
  }
1156
1556
  controller.close();
1157
-
1557
+
1158
1558
  if (isLastCommand) {
1159
1559
  finalOutput = Buffer.concat(chunks).toString('utf8');
1160
1560
  }
@@ -1162,33 +1562,31 @@ class ProcessRunner extends StreamEmitter {
1162
1562
  });
1163
1563
  } else {
1164
1564
  // Regular async function
1165
- const result = await handler(argValues, inputData, {});
1565
+ const { stdin: _, ...optionsWithoutStdin } = this.options;
1566
+ const result = await handler({ args: argValues, stdin: inputData, ...optionsWithoutStdin });
1166
1567
  const outputData = result.stdout || '';
1167
-
1568
+
1168
1569
  if (isLastCommand) {
1169
1570
  finalOutput = outputData;
1170
1571
  const buf = Buffer.from(outputData);
1171
1572
  if (this.options.mirror) {
1172
- process.stdout.write(buf);
1573
+ safeWrite(process.stdout, buf);
1173
1574
  }
1174
- this.emit('stdout', buf);
1175
- this.emit('data', { type: 'stdout', data: buf });
1575
+ this._emitProcessedData('stdout', buf);
1176
1576
  }
1177
-
1178
- // Create stream from output
1577
+
1179
1578
  currentInputStream = new ReadableStream({
1180
1579
  start(controller) {
1181
1580
  controller.enqueue(new TextEncoder().encode(outputData));
1182
1581
  controller.close();
1183
1582
  }
1184
1583
  });
1185
-
1584
+
1186
1585
  if (result.stderr) {
1187
1586
  allStderr += result.stderr;
1188
1587
  }
1189
1588
  }
1190
1589
  } else {
1191
- // Handle real command - spawn with streaming
1192
1590
  const commandParts = [cmd];
1193
1591
  for (const arg of args) {
1194
1592
  if (arg.value !== undefined) {
@@ -1208,8 +1606,7 @@ class ProcessRunner extends StreamEmitter {
1208
1606
  }
1209
1607
  }
1210
1608
  const commandStr = commandParts.join(' ');
1211
-
1212
- // Spawn the process
1609
+
1213
1610
  const proc = Bun.spawn(['sh', '-c', commandStr], {
1214
1611
  cwd: this.options.cwd,
1215
1612
  env: this.options.env,
@@ -1217,23 +1614,33 @@ class ProcessRunner extends StreamEmitter {
1217
1614
  stdout: 'pipe',
1218
1615
  stderr: 'pipe'
1219
1616
  });
1220
-
1617
+
1221
1618
  // Write input stream to process stdin if needed
1222
1619
  if (currentInputStream && proc.stdin) {
1223
1620
  const reader = currentInputStream.getReader();
1224
1621
  const writer = proc.stdin.getWriter ? proc.stdin.getWriter() : proc.stdin;
1225
-
1622
+
1226
1623
  (async () => {
1227
1624
  try {
1228
1625
  while (true) {
1229
1626
  const { done, value } = await reader.read();
1230
1627
  if (done) break;
1231
1628
  if (writer.write) {
1232
- await writer.write(value);
1629
+ try {
1630
+ await writer.write(value);
1631
+ } catch (error) {
1632
+ StreamUtils.handleStreamError(error, 'stream writer', false);
1633
+ break; // Stop streaming if write fails
1634
+ }
1233
1635
  } else if (writer.getWriter) {
1234
- const w = writer.getWriter();
1235
- await w.write(value);
1236
- w.releaseLock();
1636
+ try {
1637
+ const w = writer.getWriter();
1638
+ await w.write(value);
1639
+ w.releaseLock();
1640
+ } catch (error) {
1641
+ StreamUtils.handleStreamError(error, 'stream writer (getWriter)', false);
1642
+ break; // Stop streaming if write fails
1643
+ }
1237
1644
  }
1238
1645
  }
1239
1646
  } finally {
@@ -1243,25 +1650,22 @@ class ProcessRunner extends StreamEmitter {
1243
1650
  }
1244
1651
  })();
1245
1652
  }
1246
-
1247
- // Set up output stream
1653
+
1248
1654
  currentInputStream = proc.stdout;
1249
-
1250
- // Handle stderr
1655
+
1251
1656
  (async () => {
1252
1657
  for await (const chunk of proc.stderr) {
1253
1658
  const buf = Buffer.from(chunk);
1254
1659
  allStderr += buf.toString();
1255
1660
  if (isLastCommand) {
1256
1661
  if (this.options.mirror) {
1257
- process.stderr.write(buf);
1662
+ safeWrite(process.stderr, buf);
1258
1663
  }
1259
- this.emit('stderr', buf);
1260
- this.emit('data', { type: 'stderr', data: buf });
1664
+ this._emitProcessedData('stderr', buf);
1261
1665
  }
1262
1666
  }
1263
1667
  })();
1264
-
1668
+
1265
1669
  // For last command, stream output
1266
1670
  if (isLastCommand) {
1267
1671
  const chunks = [];
@@ -1269,44 +1673,41 @@ class ProcessRunner extends StreamEmitter {
1269
1673
  const buf = Buffer.from(chunk);
1270
1674
  chunks.push(buf);
1271
1675
  if (this.options.mirror) {
1272
- process.stdout.write(buf);
1676
+ safeWrite(process.stdout, buf);
1273
1677
  }
1274
- this.emit('stdout', buf);
1275
- this.emit('data', { type: 'stdout', data: buf });
1678
+ this._emitProcessedData('stdout', buf);
1276
1679
  }
1277
1680
  finalOutput = Buffer.concat(chunks).toString('utf8');
1278
1681
  await proc.exited;
1279
1682
  }
1280
1683
  }
1281
1684
  }
1282
-
1685
+
1283
1686
  const result = createResult({
1284
1687
  code: 0, // TODO: Track exit codes properly
1285
1688
  stdout: finalOutput,
1286
1689
  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') : ''
1690
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1691
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1289
1692
  });
1290
-
1693
+
1291
1694
  this.result = result;
1292
1695
  this.finished = true;
1293
-
1696
+
1294
1697
  this.emit('end', result);
1295
1698
  this.emit('exit', result.code);
1296
-
1699
+
1297
1700
  return result;
1298
1701
  }
1299
1702
 
1300
1703
  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)
1704
+ trace('ProcessRunner', () => `_runPipelineNonStreaming ENTER | ${JSON.stringify({
1705
+ commandsCount: commands.length
1706
+ }, null, 2)}`);
1707
+
1306
1708
  let currentOutput = '';
1307
1709
  let currentInput = '';
1308
-
1309
- // Get initial stdin from options
1710
+
1310
1711
  if (this.options.stdin && typeof this.options.stdin === 'string') {
1311
1712
  currentInput = this.options.stdin;
1312
1713
  } else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
@@ -1317,21 +1718,20 @@ class ProcessRunner extends StreamEmitter {
1317
1718
  for (let i = 0; i < commands.length; i++) {
1318
1719
  const command = commands[i];
1319
1720
  const { cmd, args } = command;
1320
-
1321
- // Check if this is a virtual command (only if virtual commands are enabled)
1721
+
1322
1722
  if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
1323
- traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'VIRTUAL_COMMAND', {
1723
+ trace('ProcessRunner', () => `BRANCH: _runPipelineNonStreaming => VIRTUAL_COMMAND | ${JSON.stringify({
1324
1724
  cmd,
1325
- argsCount: args.length
1326
- });
1327
-
1725
+ argsCount: args.length
1726
+ }, null, 2)}`);
1727
+
1328
1728
  // Run virtual command with current input
1329
1729
  const handler = virtualCommands.get(cmd);
1330
-
1730
+
1331
1731
  try {
1332
1732
  // Extract actual values for virtual command
1333
1733
  const argValues = args.map(arg => arg.value !== undefined ? arg.value : arg);
1334
-
1734
+
1335
1735
  // Shell tracing for virtual commands
1336
1736
  if (globalShellSettings.xtrace) {
1337
1737
  console.log(`+ ${cmd} ${argValues.join(' ')}`);
@@ -1341,12 +1741,11 @@ class ProcessRunner extends StreamEmitter {
1341
1741
  }
1342
1742
 
1343
1743
  let result;
1344
-
1345
- // Check if handler is async generator (streaming)
1744
+
1346
1745
  if (handler.constructor.name === 'AsyncGeneratorFunction') {
1347
- traceBranch('ProcessRunner', '_runPipelineNonStreaming', 'ASYNC_GENERATOR', { cmd });
1746
+ trace('ProcessRunner', () => `BRANCH: _runPipelineNonStreaming => ASYNC_GENERATOR | ${JSON.stringify({ cmd }, null, 2)}`);
1348
1747
  const chunks = [];
1349
- for await (const chunk of handler(argValues, currentInput, this.options)) {
1748
+ for await (const chunk of handler({ args: argValues, stdin: currentInput, ...this.options })) {
1350
1749
  chunks.push(Buffer.from(chunk));
1351
1750
  }
1352
1751
  result = {
@@ -1357,7 +1756,7 @@ class ProcessRunner extends StreamEmitter {
1357
1756
  };
1358
1757
  } else {
1359
1758
  // Regular async function
1360
- result = await handler(argValues, currentInput, this.options);
1759
+ result = await handler({ args: argValues, stdin: currentInput, ...this.options });
1361
1760
  result = {
1362
1761
  code: result.code ?? 0,
1363
1762
  stdout: this.options.capture ? (result.stdout ?? '') : undefined,
@@ -1366,50 +1765,46 @@ class ProcessRunner extends StreamEmitter {
1366
1765
  ...result
1367
1766
  };
1368
1767
  }
1369
-
1768
+
1370
1769
  // If this isn't the last command, pass stdout as stdin to next command
1371
1770
  if (i < commands.length - 1) {
1372
1771
  currentInput = result.stdout;
1373
1772
  } else {
1374
1773
  // This is the last command - emit output and store final result
1375
1774
  currentOutput = result.stdout;
1376
-
1775
+
1377
1776
  // Mirror and emit output for final command
1378
1777
  if (result.stdout) {
1379
1778
  const buf = Buffer.from(result.stdout);
1380
1779
  if (this.options.mirror) {
1381
- process.stdout.write(buf);
1780
+ safeWrite(process.stdout, buf);
1382
1781
  }
1383
- this.emit('stdout', buf);
1384
- this.emit('data', { type: 'stdout', data: buf });
1782
+ this._emitProcessedData('stdout', buf);
1385
1783
  }
1386
-
1784
+
1387
1785
  if (result.stderr) {
1388
1786
  const buf = Buffer.from(result.stderr);
1389
1787
  if (this.options.mirror) {
1390
- process.stderr.write(buf);
1788
+ safeWrite(process.stderr, buf);
1391
1789
  }
1392
- this.emit('stderr', buf);
1393
- this.emit('data', { type: 'stderr', data: buf });
1790
+ this._emitProcessedData('stderr', buf);
1394
1791
  }
1395
-
1396
- // Store final result using createResult helper for .text() method compatibility
1792
+
1397
1793
  const finalResult = createResult({
1398
1794
  code: result.code,
1399
1795
  stdout: currentOutput,
1400
1796
  stderr: result.stderr,
1401
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1402
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1797
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1798
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1403
1799
  });
1404
-
1800
+
1405
1801
  this.result = finalResult;
1406
1802
  this.finished = true;
1407
-
1803
+
1408
1804
  // Emit completion events
1409
1805
  this.emit('end', finalResult);
1410
1806
  this.emit('exit', finalResult.code);
1411
-
1412
- // Handle shell settings
1807
+
1413
1808
  if (globalShellSettings.errexit && finalResult.code !== 0) {
1414
1809
  const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
1415
1810
  error.code = finalResult.code;
@@ -1418,11 +1813,10 @@ class ProcessRunner extends StreamEmitter {
1418
1813
  error.result = finalResult;
1419
1814
  throw error;
1420
1815
  }
1421
-
1816
+
1422
1817
  return finalResult;
1423
1818
  }
1424
-
1425
- // Handle errors from intermediate commands
1819
+
1426
1820
  if (globalShellSettings.errexit && result.code !== 0) {
1427
1821
  const error = new Error(`Pipeline command failed with exit code ${result.code}`);
1428
1822
  error.code = result.code;
@@ -1432,34 +1826,32 @@ class ProcessRunner extends StreamEmitter {
1432
1826
  throw error;
1433
1827
  }
1434
1828
  } catch (error) {
1435
- // Handle errors from virtual commands in pipeline
1436
1829
  const result = createResult({
1437
1830
  code: error.code ?? 1,
1438
1831
  stdout: currentOutput,
1439
1832
  stderr: error.stderr ?? error.message,
1440
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1441
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1833
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1834
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1442
1835
  });
1443
-
1836
+
1444
1837
  this.result = result;
1445
1838
  this.finished = true;
1446
-
1839
+
1447
1840
  if (result.stderr) {
1448
1841
  const buf = Buffer.from(result.stderr);
1449
1842
  if (this.options.mirror) {
1450
- process.stderr.write(buf);
1843
+ safeWrite(process.stderr, buf);
1451
1844
  }
1452
- this.emit('stderr', buf);
1453
- this.emit('data', { type: 'stderr', data: buf });
1845
+ this._emitProcessedData('stderr', buf);
1454
1846
  }
1455
-
1847
+
1456
1848
  this.emit('end', result);
1457
1849
  this.emit('exit', result.code);
1458
-
1850
+
1459
1851
  if (globalShellSettings.errexit) {
1460
1852
  throw error;
1461
1853
  }
1462
-
1854
+
1463
1855
  return result;
1464
1856
  }
1465
1857
  } else {
@@ -1469,7 +1861,6 @@ class ProcessRunner extends StreamEmitter {
1469
1861
  const commandParts = [cmd];
1470
1862
  for (const arg of args) {
1471
1863
  if (arg.value !== undefined) {
1472
- // Handle our parsed arg structure
1473
1864
  if (arg.quoted) {
1474
1865
  // Preserve original quotes
1475
1866
  commandParts.push(`${arg.quoteChar}${arg.value}${arg.quoteChar}`);
@@ -1480,7 +1871,6 @@ class ProcessRunner extends StreamEmitter {
1480
1871
  commandParts.push(arg.value);
1481
1872
  }
1482
1873
  } else {
1483
- // Handle plain string args (backward compatibility)
1484
1874
  if (typeof arg === 'string' && arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
1485
1875
  commandParts.push(`"${arg}"`);
1486
1876
  } else {
@@ -1489,7 +1879,7 @@ class ProcessRunner extends StreamEmitter {
1489
1879
  }
1490
1880
  }
1491
1881
  const commandStr = commandParts.join(' ');
1492
-
1882
+
1493
1883
  // Shell tracing for system commands
1494
1884
  if (globalShellSettings.xtrace) {
1495
1885
  console.log(`+ ${commandStr}`);
@@ -1497,46 +1887,41 @@ class ProcessRunner extends StreamEmitter {
1497
1887
  if (globalShellSettings.verbose) {
1498
1888
  console.log(commandStr);
1499
1889
  }
1500
-
1501
- // Execute the system command with current input as stdin (ASYNC VERSION)
1890
+
1502
1891
  const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
1503
- const require = createRequire(import.meta.url);
1504
- const cp = require('child_process');
1505
-
1892
+
1506
1893
  return new Promise((resolve, reject) => {
1507
1894
  const proc = cp.spawn(argv[0], argv.slice(1), {
1508
1895
  cwd: this.options.cwd,
1509
1896
  env: this.options.env,
1510
1897
  stdio: ['pipe', 'pipe', 'pipe']
1511
1898
  });
1512
-
1899
+
1513
1900
  let stdout = '';
1514
1901
  let stderr = '';
1515
-
1902
+
1516
1903
  proc.stdout.on('data', (chunk) => {
1517
1904
  stdout += chunk.toString();
1518
1905
  // If this is the last command, emit streaming data
1519
1906
  if (isLastCommand) {
1520
1907
  if (this.options.mirror) {
1521
- process.stdout.write(chunk);
1908
+ safeWrite(process.stdout, chunk);
1522
1909
  }
1523
- this.emit('stdout', chunk);
1524
- this.emit('data', { type: 'stdout', data: chunk });
1910
+ this._emitProcessedData('stdout', chunk);
1525
1911
  }
1526
1912
  });
1527
-
1913
+
1528
1914
  proc.stderr.on('data', (chunk) => {
1529
1915
  stderr += chunk.toString();
1530
1916
  // If this is the last command, emit streaming data
1531
1917
  if (isLastCommand) {
1532
1918
  if (this.options.mirror) {
1533
- process.stderr.write(chunk);
1919
+ safeWrite(process.stderr, chunk);
1534
1920
  }
1535
- this.emit('stderr', chunk);
1536
- this.emit('data', { type: 'stderr', data: chunk });
1921
+ this._emitProcessedData('stderr', chunk);
1537
1922
  }
1538
1923
  });
1539
-
1924
+
1540
1925
  proc.on('close', (code) => {
1541
1926
  resolve({
1542
1927
  status: code,
@@ -1544,29 +1929,43 @@ class ProcessRunner extends StreamEmitter {
1544
1929
  stderr
1545
1930
  });
1546
1931
  });
1547
-
1932
+
1548
1933
  proc.on('error', reject);
1549
-
1934
+
1935
+ // Use StreamUtils for comprehensive stdin handling
1936
+ if (proc.stdin) {
1937
+ StreamUtils.addStdinErrorHandler(proc.stdin, 'spawnNodeAsync stdin', reject);
1938
+ }
1939
+
1550
1940
  if (stdin) {
1551
- proc.stdin.write(stdin);
1941
+ trace('ProcessRunner', () => `Attempting to write stdin to spawnNodeAsync | ${JSON.stringify({
1942
+ hasStdin: !!proc.stdin,
1943
+ writable: proc.stdin?.writable,
1944
+ destroyed: proc.stdin?.destroyed,
1945
+ closed: proc.stdin?.closed,
1946
+ stdinLength: stdin.length
1947
+ }, null, 2)}`);
1948
+
1949
+ StreamUtils.safeStreamWrite(proc.stdin, stdin, 'spawnNodeAsync stdin');
1552
1950
  }
1553
- proc.stdin.end();
1951
+
1952
+ // Safely end the stdin stream
1953
+ StreamUtils.safeStreamEnd(proc.stdin, 'spawnNodeAsync stdin');
1554
1954
  });
1555
1955
  };
1556
-
1956
+
1557
1957
  // Execute using shell to handle complex commands
1558
1958
  const argv = ['sh', '-c', commandStr];
1559
1959
  const isLastCommand = (i === commands.length - 1);
1560
1960
  const proc = await spawnNodeAsync(argv, currentInput, isLastCommand);
1561
-
1961
+
1562
1962
  let result = {
1563
1963
  code: proc.status || 0,
1564
1964
  stdout: proc.stdout || '',
1565
1965
  stderr: proc.stderr || '',
1566
1966
  stdin: currentInput
1567
1967
  };
1568
-
1569
- // If command failed and pipefail is set, fail the entire pipeline
1968
+
1570
1969
  if (globalShellSettings.pipefail && result.code !== 0) {
1571
1970
  const error = new Error(`Pipeline command '${commandStr}' failed with exit code ${result.code}`);
1572
1971
  error.code = result.code;
@@ -1574,7 +1973,7 @@ class ProcessRunner extends StreamEmitter {
1574
1973
  error.stderr = result.stderr;
1575
1974
  throw error;
1576
1975
  }
1577
-
1976
+
1578
1977
  // If this isn't the last command, pass stdout as stdin to next command
1579
1978
  if (i < commands.length - 1) {
1580
1979
  currentInput = result.stdout;
@@ -1586,7 +1985,7 @@ class ProcessRunner extends StreamEmitter {
1586
1985
  } else {
1587
1986
  // This is the last command - store final result (streaming already handled during execution)
1588
1987
  currentOutput = result.stdout;
1589
-
1988
+
1590
1989
  // Collect all accumulated stderr
1591
1990
  let allStderr = '';
1592
1991
  if (this.errChunks && this.errChunks.length > 0) {
@@ -1595,24 +1994,22 @@ class ProcessRunner extends StreamEmitter {
1595
1994
  if (result.stderr) {
1596
1995
  allStderr += result.stderr;
1597
1996
  }
1598
-
1599
- // Store final result using createResult helper for .text() method compatibility
1997
+
1600
1998
  const finalResult = createResult({
1601
1999
  code: result.code,
1602
2000
  stdout: currentOutput,
1603
2001
  stderr: allStderr,
1604
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1605
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
2002
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
2003
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1606
2004
  });
1607
-
2005
+
1608
2006
  this.result = finalResult;
1609
2007
  this.finished = true;
1610
-
2008
+
1611
2009
  // Emit completion events
1612
2010
  this.emit('end', finalResult);
1613
2011
  this.emit('exit', finalResult.code);
1614
-
1615
- // Handle shell settings
2012
+
1616
2013
  if (globalShellSettings.errexit && finalResult.code !== 0) {
1617
2014
  const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
1618
2015
  error.code = finalResult.code;
@@ -1621,39 +2018,37 @@ class ProcessRunner extends StreamEmitter {
1621
2018
  error.result = finalResult;
1622
2019
  throw error;
1623
2020
  }
1624
-
2021
+
1625
2022
  return finalResult;
1626
2023
  }
1627
-
2024
+
1628
2025
  } catch (error) {
1629
- // Handle errors from system commands in pipeline
1630
2026
  const result = createResult({
1631
2027
  code: error.code ?? 1,
1632
2028
  stdout: currentOutput,
1633
2029
  stderr: error.stderr ?? error.message,
1634
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1635
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
2030
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
2031
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1636
2032
  });
1637
-
2033
+
1638
2034
  this.result = result;
1639
2035
  this.finished = true;
1640
-
2036
+
1641
2037
  if (result.stderr) {
1642
2038
  const buf = Buffer.from(result.stderr);
1643
2039
  if (this.options.mirror) {
1644
- process.stderr.write(buf);
2040
+ safeWrite(process.stderr, buf);
1645
2041
  }
1646
- this.emit('stderr', buf);
1647
- this.emit('data', { type: 'stderr', data: buf });
2042
+ this._emitProcessedData('stderr', buf);
1648
2043
  }
1649
-
2044
+
1650
2045
  this.emit('end', result);
1651
2046
  this.emit('exit', result.code);
1652
-
2047
+
1653
2048
  if (globalShellSettings.errexit) {
1654
2049
  throw error;
1655
2050
  }
1656
-
2051
+
1657
2052
  return result;
1658
2053
  }
1659
2054
  }
@@ -1661,110 +2056,103 @@ class ProcessRunner extends StreamEmitter {
1661
2056
  }
1662
2057
 
1663
2058
  async _runPipeline(commands) {
1664
- traceFunc('ProcessRunner', '_runPipeline', 'ENTER', {
1665
- commandsCount: commands.length
1666
- });
1667
-
2059
+ trace('ProcessRunner', () => `_runPipeline ENTER | ${JSON.stringify({
2060
+ commandsCount: commands.length
2061
+ }, null, 2)}`);
2062
+
1668
2063
  if (commands.length === 0) {
1669
- traceBranch('ProcessRunner', '_runPipeline', 'NO_COMMANDS', {});
2064
+ trace('ProcessRunner', () => `BRANCH: _runPipeline => NO_COMMANDS | ${JSON.stringify({}, null, 2)}`);
1670
2065
  return createResult({ code: 1, stdout: '', stderr: 'No commands in pipeline', stdin: '' });
1671
2066
  }
1672
2067
 
1673
2068
 
1674
2069
  // For true streaming, we need to connect processes via pipes
1675
2070
  if (isBun) {
1676
- traceBranch('ProcessRunner', '_runPipeline', 'BUN_STREAMING', {});
2071
+ trace('ProcessRunner', () => `BRANCH: _runPipeline => BUN_STREAMING | ${JSON.stringify({}, null, 2)}`);
1677
2072
  return this._runStreamingPipelineBun(commands);
1678
2073
  }
1679
-
2074
+
1680
2075
  // For Node.js, fall back to non-streaming implementation for now
1681
- traceBranch('ProcessRunner', '_runPipeline', 'NODE_NON_STREAMING', {});
2076
+ trace('ProcessRunner', () => `BRANCH: _runPipeline => NODE_NON_STREAMING | ${JSON.stringify({}, null, 2)}`);
1682
2077
  return this._runPipelineNonStreaming(commands);
1683
2078
  }
1684
2079
 
1685
2080
  // Run programmatic pipeline (.pipe() method)
1686
2081
  async _runProgrammaticPipeline(source, destination) {
1687
- traceFunc('ProcessRunner', '_runProgrammaticPipeline', 'ENTER', {});
1688
-
2082
+ trace('ProcessRunner', () => `_runProgrammaticPipeline ENTER | ${JSON.stringify({}, null, 2)}`);
2083
+
1689
2084
  try {
1690
- // Execute the source command first
1691
- trace('ProcessRunner', 'Executing source command', {});
2085
+ trace('ProcessRunner', () => 'Executing source command');
1692
2086
  const sourceResult = await source;
1693
-
2087
+
1694
2088
  if (sourceResult.code !== 0) {
1695
- // If source failed, return its result
1696
- traceBranch('ProcessRunner', '_runProgrammaticPipeline', 'SOURCE_FAILED', {
2089
+ trace('ProcessRunner', () => `BRANCH: _runProgrammaticPipeline => SOURCE_FAILED | ${JSON.stringify({
1697
2090
  code: sourceResult.code,
1698
- stderr: sourceResult.stderr
1699
- });
2091
+ stderr: sourceResult.stderr
2092
+ }, null, 2)}`);
1700
2093
  return sourceResult;
1701
2094
  }
1702
-
1703
- // Create a new ProcessRunner with the correct stdin for the destination
2095
+
1704
2096
  const destWithStdin = new ProcessRunner(destination.spec, {
1705
2097
  ...destination.options,
1706
2098
  stdin: sourceResult.stdout
1707
2099
  });
1708
-
1709
- // Execute the destination command
2100
+
1710
2101
  const destResult = await destWithStdin;
1711
-
2102
+
1712
2103
  // Debug: Log what destResult looks like
1713
- trace('ProcessRunner', 'destResult debug', {
1714
- code: destResult.code,
2104
+ trace('ProcessRunner', () => `destResult debug | ${JSON.stringify({
2105
+ code: destResult.code,
1715
2106
  codeType: typeof destResult.code,
1716
2107
  hasCode: 'code' in destResult,
1717
2108
  keys: Object.keys(destResult),
1718
2109
  resultType: typeof destResult,
1719
2110
  fullResult: JSON.stringify(destResult, null, 2).slice(0, 200)
1720
- });
2111
+ }, null, 2)}`);
1721
2112
 
1722
- // Return the final result with combined information
1723
2113
  return createResult({
1724
2114
  code: destResult.code,
1725
2115
  stdout: destResult.stdout,
1726
2116
  stderr: sourceResult.stderr + destResult.stderr,
1727
2117
  stdin: sourceResult.stdin
1728
2118
  });
1729
-
2119
+
1730
2120
  } catch (error) {
1731
2121
  const result = createResult({
1732
2122
  code: error.code ?? 1,
1733
2123
  stdout: '',
1734
2124
  stderr: error.message || 'Pipeline execution failed',
1735
- stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
1736
- this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
2125
+ stdin: this.options.stdin && typeof this.options.stdin === 'string' ? this.options.stdin :
2126
+ this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
1737
2127
  });
1738
-
2128
+
1739
2129
  this.result = result;
1740
2130
  this.finished = true;
1741
-
2131
+
1742
2132
  const buf = Buffer.from(result.stderr);
1743
2133
  if (this.options.mirror) {
1744
- process.stderr.write(buf);
2134
+ safeWrite(process.stderr, buf);
1745
2135
  }
1746
- this.emit('stderr', buf);
1747
- this.emit('data', { type: 'stderr', data: buf });
1748
-
2136
+ this._emitProcessedData('stderr', buf);
2137
+
1749
2138
  this.emit('end', result);
1750
2139
  this.emit('exit', result.code);
1751
-
2140
+
1752
2141
  return result;
1753
2142
  }
1754
2143
  }
1755
2144
 
1756
- // Async iteration support
1757
2145
  async* stream() {
1758
- traceFunc('ProcessRunner', 'stream', 'ENTER', {
2146
+ trace('ProcessRunner', () => `stream ENTER | ${JSON.stringify({
1759
2147
  started: this.started,
1760
- finished: this.finished
1761
- });
1762
-
2148
+ finished: this.finished
2149
+ }, null, 2)}`);
2150
+
1763
2151
  if (!this.started) {
1764
- trace('ProcessRunner', 'Auto-starting async process from stream()', {});
2152
+ trace('ProcessRunner', () => 'Auto-starting async process from stream()');
1765
2153
  this._startAsync(); // Start but don't await
1766
2154
  }
1767
-
2155
+
1768
2156
  let buffer = [];
1769
2157
  let resolve, reject;
1770
2158
  let ended = false;
@@ -1804,115 +2192,108 @@ class ProcessRunner extends StreamEmitter {
1804
2192
  cleanedUp = true;
1805
2193
  this.off('data', onData);
1806
2194
  this.off('end', onEnd);
1807
-
1808
- // Kill the process if it's still running when iteration is stopped
2195
+
1809
2196
  // This happens when breaking from a for-await loop
1810
2197
  if (!this.finished) {
1811
2198
  this.kill();
1812
2199
  }
1813
2200
  }
1814
2201
  }
1815
-
1816
- // Kill the running process or cancel virtual command
2202
+
1817
2203
  kill() {
1818
- traceFunc('ProcessRunner', 'kill', 'ENTER', {
2204
+ trace('ProcessRunner', () => `kill ENTER | ${JSON.stringify({
1819
2205
  cancelled: this._cancelled,
1820
2206
  finished: this.finished,
1821
2207
  hasChild: !!this.child,
1822
2208
  hasVirtualGenerator: !!this._virtualGenerator
1823
- });
1824
-
2209
+ }, null, 2)}`);
2210
+
1825
2211
  // Mark as cancelled for virtual commands
1826
2212
  this._cancelled = true;
1827
-
1828
- // Resolve the cancel promise to break the race in virtual command execution
2213
+
1829
2214
  if (this._cancelResolve) {
1830
- trace('ProcessRunner', 'Resolving cancel promise', {});
2215
+ trace('ProcessRunner', () => 'Resolving cancel promise');
1831
2216
  this._cancelResolve();
1832
2217
  }
1833
-
2218
+
1834
2219
  // Abort any async operations
1835
2220
  if (this._abortController) {
1836
- trace('ProcessRunner', 'Aborting controller', {});
2221
+ trace('ProcessRunner', () => 'Aborting controller');
1837
2222
  this._abortController.abort();
1838
2223
  }
1839
-
2224
+
1840
2225
  // If it's a virtual generator, try to close it
1841
2226
  if (this._virtualGenerator && this._virtualGenerator.return) {
1842
- trace('ProcessRunner', 'Closing virtual generator', {});
2227
+ trace('ProcessRunner', () => 'Closing virtual generator');
1843
2228
  try {
1844
2229
  this._virtualGenerator.return();
1845
2230
  } catch (err) {
1846
- trace('ProcessRunner', 'Error closing generator', { error: err.message });
2231
+ trace('ProcessRunner', () => `Error closing generator | ${JSON.stringify({ error: err.message }, null, 2)}`);
1847
2232
  }
1848
2233
  }
1849
-
2234
+
1850
2235
  // Kill child process if it exists
1851
2236
  if (this.child && !this.finished) {
1852
- traceBranch('ProcessRunner', 'hasChild', 'killing', { pid: this.child.pid });
2237
+ trace('ProcessRunner', () => `BRANCH: hasChild => killing | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
1853
2238
  try {
1854
- // Kill the process group to ensure all child processes are terminated
1855
2239
  if (this.child.pid) {
1856
2240
  if (isBun) {
1857
- trace('ProcessRunner', 'Killing Bun process', { pid: this.child.pid });
2241
+ trace('ProcessRunner', () => `Killing Bun process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
1858
2242
  this.child.kill();
1859
2243
  } else {
1860
2244
  // In Node.js, kill the process group
1861
- trace('ProcessRunner', 'Killing Node process group', { pid: this.child.pid });
2245
+ trace('ProcessRunner', () => `Killing Node process group | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
1862
2246
  process.kill(-this.child.pid, 'SIGTERM');
1863
2247
  }
1864
2248
  }
1865
2249
  this.finished = true;
1866
2250
  } catch (err) {
1867
2251
  // Process might already be dead
1868
- trace('ProcessRunner', 'Error killing process', { error: err.message });
2252
+ trace('ProcessRunner', () => `Error killing process | ${JSON.stringify({ error: err.message }, null, 2)}`);
1869
2253
  console.error('Error killing process:', err.message);
1870
2254
  }
1871
2255
  }
1872
-
2256
+
1873
2257
  // Mark as finished
1874
2258
  this.finished = true;
1875
-
1876
- traceFunc('ProcessRunner', 'kill', 'EXIT', {
2259
+
2260
+ trace('ProcessRunner', () => `kill EXIT | ${JSON.stringify({
1877
2261
  cancelled: this._cancelled,
1878
- finished: this.finished
1879
- });
2262
+ finished: this.finished
2263
+ }, null, 2)}`);
1880
2264
  }
1881
2265
 
1882
- // Programmatic piping support
1883
2266
  pipe(destination) {
1884
- traceFunc('ProcessRunner', 'pipe', 'ENTER', {
2267
+ trace('ProcessRunner', () => `pipe ENTER | ${JSON.stringify({
1885
2268
  hasDestination: !!destination,
1886
- destinationType: destination?.constructor?.name
1887
- });
1888
-
1889
- // If destination is a ProcessRunner, create a pipeline
2269
+ destinationType: destination?.constructor?.name
2270
+ }, null, 2)}`);
2271
+
1890
2272
  if (destination instanceof ProcessRunner) {
1891
- traceBranch('ProcessRunner', 'pipe', 'PROCESS_RUNNER_DEST', {});
1892
- // Create a new ProcessRunner that represents the piped operation
2273
+ trace('ProcessRunner', () => `BRANCH: pipe => PROCESS_RUNNER_DEST | ${JSON.stringify({}, null, 2)}`);
1893
2274
  const pipeSpec = {
1894
2275
  mode: 'pipeline',
1895
2276
  source: this,
1896
2277
  destination: destination
1897
2278
  };
1898
-
2279
+
1899
2280
  const pipeRunner = new ProcessRunner(pipeSpec, {
1900
2281
  ...this.options,
1901
2282
  capture: destination.options.capture ?? true
1902
2283
  });
1903
-
1904
- traceFunc('ProcessRunner', 'pipe', 'EXIT', { mode: 'pipeline' });
2284
+
2285
+ trace('ProcessRunner', () => `pipe EXIT | ${JSON.stringify({ mode: 'pipeline' }, null, 2)}`);
1905
2286
  return pipeRunner;
1906
2287
  }
1907
-
2288
+
1908
2289
  // If destination is a template literal result (from $`command`), use its spec
1909
2290
  if (destination && destination.spec) {
1910
- traceBranch('ProcessRunner', 'pipe', 'TEMPLATE_LITERAL_DEST', {});
2291
+ trace('ProcessRunner', () => `BRANCH: pipe => TEMPLATE_LITERAL_DEST | ${JSON.stringify({}, null, 2)}`);
1911
2292
  const destRunner = new ProcessRunner(destination.spec, destination.options);
1912
2293
  return this.pipe(destRunner);
1913
2294
  }
1914
-
1915
- traceBranch('ProcessRunner', 'pipe', 'INVALID_DEST', {});
2295
+
2296
+ trace('ProcessRunner', () => `BRANCH: pipe => INVALID_DEST | ${JSON.stringify({}, null, 2)}`);
1916
2297
  throw new Error('pipe() destination must be a ProcessRunner or $`command` result');
1917
2298
  }
1918
2299
 
@@ -1940,110 +2321,103 @@ class ProcessRunner extends StreamEmitter {
1940
2321
 
1941
2322
  // Internal sync execution
1942
2323
  _startSync() {
1943
- traceFunc('ProcessRunner', '_startSync', 'ENTER', {
2324
+ trace('ProcessRunner', () => `_startSync ENTER | ${JSON.stringify({
1944
2325
  started: this.started,
1945
- spec: this.spec
1946
- });
1947
-
2326
+ spec: this.spec
2327
+ }, null, 2)}`);
2328
+
1948
2329
  if (this.started) {
1949
- traceBranch('ProcessRunner', '_startSync', 'ALREADY_STARTED', {});
2330
+ trace('ProcessRunner', () => `BRANCH: _startSync => ALREADY_STARTED | ${JSON.stringify({}, null, 2)}`);
1950
2331
  throw new Error('Command already started - cannot run sync after async start');
1951
2332
  }
1952
-
2333
+
1953
2334
  this.started = true;
1954
2335
  this._mode = 'sync';
1955
- trace('ProcessRunner', 'Starting sync execution', { mode: this._mode });
1956
-
2336
+ trace('ProcessRunner', () => `Starting sync execution | ${JSON.stringify({ mode: this._mode }, null, 2)}`);
2337
+
1957
2338
  const { cwd, env, stdin } = this.options;
1958
2339
  const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
1959
-
1960
- // Shell tracing (set -x equivalent)
2340
+
1961
2341
  if (globalShellSettings.xtrace) {
1962
2342
  const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
1963
2343
  console.log(`+ ${traceCmd}`);
1964
2344
  }
1965
-
1966
- // Verbose mode (set -v equivalent)
2345
+
1967
2346
  if (globalShellSettings.verbose) {
1968
2347
  const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
1969
2348
  console.log(verboseCmd);
1970
2349
  }
1971
-
2350
+
1972
2351
  let result;
1973
-
2352
+
1974
2353
  if (isBun) {
1975
2354
  // Use Bun's synchronous spawn
1976
2355
  const proc = Bun.spawnSync(argv, {
1977
2356
  cwd,
1978
2357
  env,
1979
- stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
1980
- Buffer.isBuffer(stdin) ? stdin :
1981
- stdin === 'ignore' ? undefined : undefined,
2358
+ stdin: typeof stdin === 'string' ? Buffer.from(stdin) :
2359
+ Buffer.isBuffer(stdin) ? stdin :
2360
+ stdin === 'ignore' ? undefined : undefined,
1982
2361
  stdout: 'pipe',
1983
2362
  stderr: 'pipe'
1984
2363
  });
1985
-
2364
+
1986
2365
  result = createResult({
1987
2366
  code: proc.exitCode || 0,
1988
2367
  stdout: proc.stdout?.toString('utf8') || '',
1989
2368
  stderr: proc.stderr?.toString('utf8') || '',
1990
- stdin: typeof stdin === 'string' ? stdin :
1991
- Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
2369
+ stdin: typeof stdin === 'string' ? stdin :
2370
+ Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
1992
2371
  });
1993
2372
  result.child = proc;
1994
2373
  } else {
1995
2374
  // Use Node's synchronous spawn
1996
- const require = createRequire(import.meta.url);
1997
- const cp = require('child_process');
1998
2375
  const proc = cp.spawnSync(argv[0], argv.slice(1), {
1999
2376
  cwd,
2000
2377
  env,
2001
- input: typeof stdin === 'string' ? stdin :
2002
- Buffer.isBuffer(stdin) ? stdin : undefined,
2378
+ input: typeof stdin === 'string' ? stdin :
2379
+ Buffer.isBuffer(stdin) ? stdin : undefined,
2003
2380
  encoding: 'utf8',
2004
2381
  stdio: ['pipe', 'pipe', 'pipe']
2005
2382
  });
2006
-
2383
+
2007
2384
  result = createResult({
2008
2385
  code: proc.status || 0,
2009
2386
  stdout: proc.stdout || '',
2010
2387
  stderr: proc.stderr || '',
2011
- stdin: typeof stdin === 'string' ? stdin :
2012
- Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
2388
+ stdin: typeof stdin === 'string' ? stdin :
2389
+ Buffer.isBuffer(stdin) ? stdin.toString('utf8') : ''
2013
2390
  });
2014
2391
  result.child = proc;
2015
2392
  }
2016
-
2393
+
2017
2394
  // Mirror output if requested (but always capture for result)
2018
2395
  if (this.options.mirror) {
2019
- if (result.stdout) process.stdout.write(result.stdout);
2020
- if (result.stderr) process.stderr.write(result.stderr);
2396
+ if (result.stdout) safeWrite(process.stdout, result.stdout);
2397
+ if (result.stderr) safeWrite(process.stderr, result.stderr);
2021
2398
  }
2022
-
2399
+
2023
2400
  // Store chunks for events (batched after completion)
2024
2401
  this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
2025
2402
  this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
2026
-
2403
+
2027
2404
  this.result = result;
2028
2405
  this.finished = true;
2029
-
2406
+
2030
2407
  // Emit batched events after completion
2031
2408
  if (result.stdout) {
2032
2409
  const stdoutBuf = Buffer.from(result.stdout);
2033
- this.emit('stdout', stdoutBuf);
2034
- this.emit('data', { type: 'stdout', data: stdoutBuf });
2410
+ this._emitProcessedData('stdout', stdoutBuf);
2035
2411
  }
2036
-
2412
+
2037
2413
  if (result.stderr) {
2038
2414
  const stderrBuf = Buffer.from(result.stderr);
2039
- this.emit('stderr', stderrBuf);
2040
- this.emit('data', { type: 'stderr', data: stderrBuf });
2415
+ this._emitProcessedData('stderr', stderrBuf);
2041
2416
  }
2042
-
2417
+
2043
2418
  this.emit('end', result);
2044
2419
  this.emit('exit', result.code);
2045
-
2046
- // Handle shell settings (set -e equivalent)
2420
+
2047
2421
  if (globalShellSettings.errexit && result.code !== 0) {
2048
2422
  const error = new Error(`Command failed with exit code ${result.code}`);
2049
2423
  error.code = result.code;
@@ -2052,7 +2426,7 @@ class ProcessRunner extends StreamEmitter {
2052
2426
  error.result = result;
2053
2427
  throw error;
2054
2428
  }
2055
-
2429
+
2056
2430
  return result;
2057
2431
  }
2058
2432
 
@@ -2072,93 +2446,91 @@ class ProcessRunner extends StreamEmitter {
2072
2446
 
2073
2447
  // Public APIs
2074
2448
  async function sh(commandString, options = {}) {
2075
- traceFunc('API', 'sh', 'ENTER', {
2449
+ trace('API', () => `sh ENTER | ${JSON.stringify({
2076
2450
  command: commandString,
2077
- options
2078
- });
2079
-
2451
+ options
2452
+ }, null, 2)}`);
2453
+
2080
2454
  const runner = new ProcessRunner({ mode: 'shell', command: commandString }, options);
2081
2455
  const result = await runner._startAsync();
2082
-
2083
- traceFunc('API', 'sh', 'EXIT', { code: result.code });
2456
+
2457
+ trace('API', () => `sh EXIT | ${JSON.stringify({ code: result.code }, null, 2)}`);
2084
2458
  return result;
2085
2459
  }
2086
2460
 
2087
2461
  async function exec(file, args = [], options = {}) {
2088
- traceFunc('API', 'exec', 'ENTER', {
2462
+ trace('API', () => `exec ENTER | ${JSON.stringify({
2089
2463
  file,
2090
2464
  argsCount: args.length,
2091
- options
2092
- });
2093
-
2465
+ options
2466
+ }, null, 2)}`);
2467
+
2094
2468
  const runner = new ProcessRunner({ mode: 'exec', file, args }, options);
2095
2469
  const result = await runner._startAsync();
2096
-
2097
- traceFunc('API', 'exec', 'EXIT', { code: result.code });
2470
+
2471
+ trace('API', () => `exec EXIT | ${JSON.stringify({ code: result.code }, null, 2)}`);
2098
2472
  return result;
2099
2473
  }
2100
2474
 
2101
2475
  async function run(commandOrTokens, options = {}) {
2102
- traceFunc('API', 'run', 'ENTER', {
2476
+ trace('API', () => `run ENTER | ${JSON.stringify({
2103
2477
  type: typeof commandOrTokens,
2104
- options
2105
- });
2106
-
2478
+ options
2479
+ }, null, 2)}`);
2480
+
2107
2481
  if (typeof commandOrTokens === 'string') {
2108
- traceBranch('API', 'run', 'STRING_COMMAND', { command: commandOrTokens });
2482
+ trace('API', () => `BRANCH: run => STRING_COMMAND | ${JSON.stringify({ command: commandOrTokens }, null, 2)}`);
2109
2483
  return sh(commandOrTokens, { ...options, mirror: false, capture: true });
2110
2484
  }
2111
-
2485
+
2112
2486
  const [file, ...args] = commandOrTokens;
2113
- traceBranch('API', 'run', 'TOKEN_ARRAY', { file, argsCount: args.length });
2487
+ trace('API', () => `BRANCH: run => TOKEN_ARRAY | ${JSON.stringify({ file, argsCount: args.length }, null, 2)}`);
2114
2488
  return exec(file, args, { ...options, mirror: false, capture: true });
2115
2489
  }
2116
2490
 
2117
- // Enhanced tagged template that returns ProcessRunner
2118
2491
  function $tagged(strings, ...values) {
2119
- traceFunc('API', '$tagged', 'ENTER', {
2492
+ trace('API', () => `$tagged ENTER | ${JSON.stringify({
2120
2493
  stringsLength: strings.length,
2121
- valuesLength: values.length
2122
- });
2123
-
2494
+ valuesLength: values.length
2495
+ }, null, 2)}`);
2496
+
2124
2497
  const cmd = buildShellCommand(strings, values);
2125
2498
  const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true });
2126
-
2127
- traceFunc('API', '$tagged', 'EXIT', { command: cmd });
2499
+
2500
+ trace('API', () => `$tagged EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
2128
2501
  return runner;
2129
2502
  }
2130
2503
 
2131
2504
  function create(defaultOptions = {}) {
2132
- traceFunc('API', 'create', 'ENTER', { defaultOptions });
2133
-
2505
+ trace('API', () => `create ENTER | ${JSON.stringify({ defaultOptions }, null, 2)}`);
2506
+
2134
2507
  const tagged = (strings, ...values) => {
2135
- traceFunc('API', 'create.tagged', 'ENTER', {
2508
+ trace('API', () => `create.tagged ENTER | ${JSON.stringify({
2136
2509
  stringsLength: strings.length,
2137
- valuesLength: values.length
2138
- });
2139
-
2510
+ valuesLength: values.length
2511
+ }, null, 2)}`);
2512
+
2140
2513
  const cmd = buildShellCommand(strings, values);
2141
2514
  const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...defaultOptions });
2142
-
2143
- traceFunc('API', 'create.tagged', 'EXIT', { command: cmd });
2515
+
2516
+ trace('API', () => `create.tagged EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
2144
2517
  return runner;
2145
2518
  };
2146
-
2147
- traceFunc('API', 'create', 'EXIT', {});
2519
+
2520
+ trace('API', () => `create EXIT | ${JSON.stringify({}, null, 2)}`);
2148
2521
  return tagged;
2149
2522
  }
2150
2523
 
2151
- function raw(value) {
2152
- return { raw: String(value) };
2524
+ function raw(value) {
2525
+ return { raw: String(value) };
2153
2526
  }
2154
2527
 
2155
- // Shell setting control functions (like bash set/unset)
2156
2528
  function set(option) {
2157
2529
  const mapping = {
2158
2530
  'e': 'errexit', // set -e: exit on error
2159
2531
  'errexit': 'errexit',
2160
2532
  'v': 'verbose', // set -v: verbose
2161
- 'verbose': 'verbose',
2533
+ 'verbose': 'verbose',
2162
2534
  'x': 'xtrace', // set -x: trace execution
2163
2535
  'xtrace': 'xtrace',
2164
2536
  'u': 'nounset', // set -u: error on unset vars
@@ -2166,7 +2538,7 @@ function set(option) {
2166
2538
  'o pipefail': 'pipefail', // set -o pipefail
2167
2539
  'pipefail': 'pipefail'
2168
2540
  };
2169
-
2541
+
2170
2542
  if (mapping[option]) {
2171
2543
  globalShellSettings[mapping[option]] = true;
2172
2544
  if (globalShellSettings.verbose) {
@@ -2180,7 +2552,7 @@ function unset(option) {
2180
2552
  const mapping = {
2181
2553
  'e': 'errexit',
2182
2554
  'errexit': 'errexit',
2183
- 'v': 'verbose',
2555
+ 'v': 'verbose',
2184
2556
  'verbose': 'verbose',
2185
2557
  'x': 'xtrace',
2186
2558
  'xtrace': 'xtrace',
@@ -2189,7 +2561,7 @@ function unset(option) {
2189
2561
  'o pipefail': 'pipefail',
2190
2562
  'pipefail': 'pipefail'
2191
2563
  };
2192
-
2564
+
2193
2565
  if (mapping[option]) {
2194
2566
  globalShellSettings[mapping[option]] = false;
2195
2567
  if (globalShellSettings.verbose) {
@@ -2204,10 +2576,10 @@ const shell = {
2204
2576
  set,
2205
2577
  unset,
2206
2578
  settings: () => ({ ...globalShellSettings }),
2207
-
2579
+
2208
2580
  // Bash-like shortcuts
2209
2581
  errexit: (enable = true) => enable ? set('e') : unset('e'),
2210
- verbose: (enable = true) => enable ? set('v') : unset('v'),
2582
+ verbose: (enable = true) => enable ? set('v') : unset('v'),
2211
2583
  xtrace: (enable = true) => enable ? set('x') : unset('x'),
2212
2584
  pipefail: (enable = true) => enable ? set('o pipefail') : unset('o pipefail'),
2213
2585
  nounset: (enable = true) => enable ? set('u') : unset('u'),
@@ -2215,20 +2587,16 @@ const shell = {
2215
2587
 
2216
2588
  // Virtual command registration API
2217
2589
  function register(name, handler) {
2218
- traceFunc('VirtualCommands', 'register', 'ENTER', { name });
2219
-
2590
+ trace('VirtualCommands', () => `register ENTER | ${JSON.stringify({ name }, null, 2)}`);
2220
2591
  virtualCommands.set(name, handler);
2221
-
2222
- traceFunc('VirtualCommands', 'register', 'EXIT', { registered: true });
2592
+ trace('VirtualCommands', () => `register EXIT | ${JSON.stringify({ registered: true }, null, 2)}`);
2223
2593
  return virtualCommands;
2224
2594
  }
2225
2595
 
2226
2596
  function unregister(name) {
2227
- traceFunc('VirtualCommands', 'unregister', 'ENTER', { name });
2228
-
2597
+ trace('VirtualCommands', () => `unregister ENTER | ${JSON.stringify({ name }, null, 2)}`);
2229
2598
  const deleted = virtualCommands.delete(name);
2230
-
2231
- traceFunc('VirtualCommands', 'unregister', 'EXIT', { deleted });
2599
+ trace('VirtualCommands', () => `unregister EXIT | ${JSON.stringify({ deleted }, null, 2)}`);
2232
2600
  return deleted;
2233
2601
  }
2234
2602
 
@@ -2249,62 +2617,62 @@ function disableVirtualCommands() {
2249
2617
  // Built-in commands that match Bun.$ functionality
2250
2618
  function registerBuiltins() {
2251
2619
  // cd - change directory
2252
- register('cd', async (args) => {
2620
+ register('cd', async ({ args }) => {
2253
2621
  const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
2254
- trace('VirtualCommand', 'cd: changing directory', { target });
2255
-
2622
+ trace('VirtualCommand', () => `cd: changing directory | ${JSON.stringify({ target }, null, 2)}`);
2623
+
2256
2624
  try {
2257
2625
  process.chdir(target);
2258
2626
  const newDir = process.cwd();
2259
- trace('VirtualCommand', 'cd: success', { newDir });
2260
- return { stdout: newDir, code: 0 };
2627
+ trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
2628
+ return VirtualUtils.success(newDir);
2261
2629
  } catch (error) {
2262
- trace('VirtualCommand', 'cd: failed', { error: error.message });
2630
+ trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
2263
2631
  return { stderr: `cd: ${error.message}`, code: 1 };
2264
2632
  }
2265
2633
  });
2266
2634
 
2267
2635
  // pwd - print working directory
2268
- register('pwd', async (args, stdin, options) => {
2636
+ register('pwd', async ({ args, stdin, cwd }) => {
2269
2637
  // If cwd option is provided, return that instead of process.cwd()
2270
- const dir = options?.cwd || process.cwd();
2271
- trace('VirtualCommand', 'pwd: getting directory', { dir });
2272
- return { stdout: dir, code: 0 };
2638
+ const dir = cwd || process.cwd();
2639
+ trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
2640
+ return VirtualUtils.success(dir);
2273
2641
  });
2274
2642
 
2275
2643
  // echo - print arguments
2276
- register('echo', async (args) => {
2277
- trace('VirtualCommand', 'echo: processing', { argsCount: args.length });
2278
-
2644
+ register('echo', async ({ args }) => {
2645
+ trace('VirtualCommand', () => `echo: processing | ${JSON.stringify({ argsCount: args.length }, null, 2)}`);
2646
+
2279
2647
  let output = args.join(' ');
2280
2648
  if (args.includes('-n')) {
2281
2649
  // Don't add newline
2282
- traceBranch('VirtualCommand', 'echo', 'NO_NEWLINE', {});
2650
+ trace('VirtualCommand', () => `BRANCH: echo => NO_NEWLINE | ${JSON.stringify({}, null, 2)}`);
2283
2651
  output = args.filter(arg => arg !== '-n').join(' ');
2284
2652
  } else {
2285
2653
  output += '\n';
2286
2654
  }
2287
- return { stdout: output, code: 0 };
2655
+ return VirtualUtils.success(output);
2288
2656
  });
2289
2657
 
2290
2658
  // sleep - wait for specified time
2291
- register('sleep', async (args) => {
2659
+ register('sleep', async ({ args }) => {
2292
2660
  const seconds = parseFloat(args[0] || 0);
2293
- trace('VirtualCommand', 'sleep: starting', { seconds });
2294
-
2661
+ trace('VirtualCommand', () => `sleep: starting | ${JSON.stringify({ seconds }, null, 2)}`);
2662
+
2295
2663
  if (isNaN(seconds) || seconds < 0) {
2296
- trace('VirtualCommand', 'sleep: invalid interval', { input: args[0] });
2664
+ trace('VirtualCommand', () => `sleep: invalid interval | ${JSON.stringify({ input: args[0] }, null, 2)}`);
2297
2665
  return { stderr: 'sleep: invalid time interval', code: 1 };
2298
2666
  }
2299
-
2667
+
2300
2668
  await new Promise(resolve => setTimeout(resolve, seconds * 1000));
2301
- trace('VirtualCommand', 'sleep: completed', { seconds });
2669
+ trace('VirtualCommand', () => `sleep: completed | ${JSON.stringify({ seconds }, null, 2)}`);
2302
2670
  return { stdout: '', code: 0 };
2303
2671
  });
2304
2672
 
2305
2673
  // true - always succeed
2306
2674
  register('true', async () => {
2307
- return { stdout: '', code: 0 };
2675
+ return VirtualUtils.success();
2308
2676
  });
2309
2677
 
2310
2678
  // false - always fail
@@ -2313,38 +2681,35 @@ function registerBuiltins() {
2313
2681
  });
2314
2682
 
2315
2683
  // which - locate command
2316
- register('which', async (args) => {
2317
- if (args.length === 0) {
2318
- return { stderr: 'which: missing operand', code: 1 };
2319
- }
2320
-
2684
+ register('which', async ({ args }) => {
2685
+ const argError = VirtualUtils.validateArgs(args, 1, 'which');
2686
+ if (argError) return argError;
2687
+
2321
2688
  const cmd = args[0];
2322
-
2323
- // Check virtual commands first
2689
+
2324
2690
  if (virtualCommands.has(cmd)) {
2325
- return { stdout: `${cmd}: shell builtin\n`, code: 0 };
2691
+ return VirtualUtils.success(`${cmd}: shell builtin\n`);
2326
2692
  }
2327
-
2328
- // Check PATH for system commands
2693
+
2329
2694
  const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
2330
2695
  const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
2331
-
2332
- for (const path of paths) {
2696
+
2697
+ for (const pathDir of paths) {
2333
2698
  for (const ext of extensions) {
2334
- const fullPath = require('path').join(path, cmd + ext);
2699
+ const fullPath = path.join(pathDir, cmd + ext);
2335
2700
  try {
2336
- if (require('fs').statSync(fullPath).isFile()) {
2337
- return { stdout: fullPath, code: 0 };
2701
+ if (fs.statSync(fullPath).isFile()) {
2702
+ return VirtualUtils.success(fullPath + '\n');
2338
2703
  }
2339
- } catch {}
2704
+ } catch { }
2340
2705
  }
2341
2706
  }
2342
-
2343
- return { stderr: `which: no ${cmd} in PATH`, code: 1 };
2707
+
2708
+ return VirtualUtils.error(`which: no ${cmd} in PATH`);
2344
2709
  });
2345
2710
 
2346
2711
  // exit - exit with code
2347
- register('exit', async (args) => {
2712
+ register('exit', async ({ args }) => {
2348
2713
  const code = parseInt(args[0] || 0);
2349
2714
  if (globalShellSettings.errexit || code !== 0) {
2350
2715
  // For virtual commands, we simulate exit by returning the code
@@ -2354,54 +2719,50 @@ function registerBuiltins() {
2354
2719
  });
2355
2720
 
2356
2721
  // env - print environment variables
2357
- register('env', async (args, stdin, options) => {
2722
+ register('env', async ({ args, stdin, env }) => {
2358
2723
  if (args.length === 0) {
2359
2724
  // Use custom env if provided, otherwise use process.env
2360
- const env = options?.env || process.env;
2361
- const output = Object.entries(env)
2725
+ const envVars = env || process.env;
2726
+ const output = Object.entries(envVars)
2362
2727
  .map(([key, value]) => `${key}=${value}`)
2363
2728
  .join('\n') + '\n';
2364
2729
  return { stdout: output, code: 0 };
2365
2730
  }
2366
-
2731
+
2367
2732
  // TODO: Support env VAR=value command syntax
2368
2733
  return { stderr: 'env: command execution not yet supported', code: 1 };
2369
2734
  });
2370
2735
 
2371
2736
  // cat - read and display file contents
2372
- register('cat', async (args, stdin, options) => {
2737
+ register('cat', async ({ args, stdin, cwd }) => {
2373
2738
  if (args.length === 0) {
2374
2739
  // Read from stdin if no files specified
2375
2740
  return { stdout: stdin || '', code: 0 };
2376
2741
  }
2377
-
2742
+
2378
2743
  try {
2379
- const fs = await import('fs');
2380
- const path = await import('path');
2381
2744
  let output = '';
2382
-
2745
+
2383
2746
  for (const filename of args) {
2384
- // Handle special flags
2385
2747
  if (filename === '-n') continue; // Line numbering (basic support)
2386
-
2748
+
2387
2749
  try {
2388
2750
  // Resolve path relative to cwd if provided
2389
- const basePath = options?.cwd || process.cwd();
2751
+ const basePath = cwd || process.cwd();
2390
2752
  const fullPath = path.isAbsolute(filename) ? filename : path.join(basePath, filename);
2391
-
2753
+
2392
2754
  const content = fs.readFileSync(fullPath, 'utf8');
2393
2755
  output += content;
2394
2756
  } catch (error) {
2395
- // Format error message to match bash/sh style
2396
2757
  const errorMsg = error.code === 'ENOENT' ? 'No such file or directory' : error.message;
2397
- return {
2398
- stderr: `cat: ${filename}: ${errorMsg}`,
2758
+ return {
2759
+ stderr: `cat: ${filename}: ${errorMsg}`,
2399
2760
  stdout: output,
2400
- code: 1
2761
+ code: 1
2401
2762
  };
2402
2763
  }
2403
2764
  }
2404
-
2765
+
2405
2766
  return { stdout: output, code: 0 };
2406
2767
  } catch (error) {
2407
2768
  return { stderr: `cat: ${error.message}`, code: 1 };
@@ -2409,37 +2770,34 @@ function registerBuiltins() {
2409
2770
  });
2410
2771
 
2411
2772
  // ls - list directory contents
2412
- register('ls', async (args, stdin, options) => {
2773
+ register('ls', async ({ args, stdin, cwd }) => {
2413
2774
  try {
2414
- const fs = await import('fs');
2415
- const path = await import('path');
2416
-
2417
- // Parse flags and paths
2775
+
2418
2776
  const flags = args.filter(arg => arg.startsWith('-'));
2419
2777
  const paths = args.filter(arg => !arg.startsWith('-'));
2420
2778
  const isLongFormat = flags.includes('-l');
2421
2779
  const showAll = flags.includes('-a');
2422
2780
  const showAlmostAll = flags.includes('-A');
2423
-
2781
+
2424
2782
  // Default to current directory if no paths specified
2425
2783
  const targetPaths = paths.length > 0 ? paths : ['.'];
2426
-
2784
+
2427
2785
  let output = '';
2428
-
2786
+
2429
2787
  for (const targetPath of targetPaths) {
2430
2788
  // Resolve path relative to cwd if provided
2431
- const basePath = options?.cwd || process.cwd();
2789
+ const basePath = cwd || process.cwd();
2432
2790
  const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(basePath, targetPath);
2433
-
2791
+
2434
2792
  try {
2435
2793
  const stat = fs.statSync(fullPath);
2436
-
2794
+
2437
2795
  if (stat.isFile()) {
2438
2796
  // Just show the file name if it's a file
2439
2797
  output += path.basename(targetPath) + '\n';
2440
2798
  } else if (stat.isDirectory()) {
2441
2799
  const entries = fs.readdirSync(fullPath);
2442
-
2800
+
2443
2801
  // Filter hidden files unless -a or -A is specified
2444
2802
  let filteredEntries = entries;
2445
2803
  if (!showAll && !showAlmostAll) {
@@ -2447,9 +2805,8 @@ function registerBuiltins() {
2447
2805
  } else if (showAlmostAll) {
2448
2806
  filteredEntries = entries.filter(entry => entry !== '.' && entry !== '..');
2449
2807
  }
2450
-
2808
+
2451
2809
  if (isLongFormat) {
2452
- // Long format: permissions, links, owner, group, size, date, name
2453
2810
  for (const entry of filteredEntries) {
2454
2811
  const entryPath = path.join(fullPath, entry);
2455
2812
  try {
@@ -2464,56 +2821,51 @@ function registerBuiltins() {
2464
2821
  }
2465
2822
  }
2466
2823
  } else {
2467
- // Simple format: just names
2468
2824
  output += filteredEntries.join('\n') + (filteredEntries.length > 0 ? '\n' : '');
2469
2825
  }
2470
2826
  }
2471
2827
  } catch (error) {
2472
- return {
2473
- stderr: `ls: cannot access '${targetPath}': ${error.message}`,
2474
- code: 2
2828
+ return {
2829
+ stderr: `ls: cannot access '${targetPath}': ${error.message}`,
2830
+ code: 2
2475
2831
  };
2476
2832
  }
2477
2833
  }
2478
-
2834
+
2479
2835
  return { stdout: output, code: 0 };
2480
2836
  } catch (error) {
2481
2837
  return { stderr: `ls: ${error.message}`, code: 1 };
2482
2838
  }
2483
2839
  });
2484
2840
 
2485
- // mkdir - create directories
2486
- register('mkdir', async (args, stdin, options) => {
2487
- if (args.length === 0) {
2488
- return { stderr: 'mkdir: missing operand', code: 1 };
2489
- }
2490
-
2841
+ register('mkdir', async ({ args, stdin, cwd }) => {
2842
+ const argError = VirtualUtils.validateArgs(args, 1, 'mkdir');
2843
+ if (argError) return argError;
2844
+
2491
2845
  try {
2492
- const fs = await import('fs');
2493
- const path = await import('path');
2494
-
2846
+
2495
2847
  const flags = args.filter(arg => arg.startsWith('-'));
2496
2848
  const dirs = args.filter(arg => !arg.startsWith('-'));
2497
2849
  const recursive = flags.includes('-p');
2498
-
2850
+
2499
2851
  for (const dir of dirs) {
2500
2852
  try {
2501
- const basePath = options?.cwd || process.cwd();
2853
+ const basePath = cwd || process.cwd();
2502
2854
  const fullPath = path.isAbsolute(dir) ? dir : path.join(basePath, dir);
2503
-
2855
+
2504
2856
  if (recursive) {
2505
2857
  fs.mkdirSync(fullPath, { recursive: true });
2506
2858
  } else {
2507
2859
  fs.mkdirSync(fullPath);
2508
2860
  }
2509
2861
  } catch (error) {
2510
- return {
2511
- stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
2512
- code: 1
2862
+ return {
2863
+ stderr: `mkdir: cannot create directory '${dir}': ${error.message}`,
2864
+ code: 1
2513
2865
  };
2514
2866
  }
2515
2867
  }
2516
-
2868
+
2517
2869
  return { stdout: '', code: 0 };
2518
2870
  } catch (error) {
2519
2871
  return { stderr: `mkdir: ${error.message}`, code: 1 };
@@ -2521,32 +2873,29 @@ function registerBuiltins() {
2521
2873
  });
2522
2874
 
2523
2875
  // rm - remove files and directories
2524
- register('rm', async (args, stdin, options) => {
2525
- if (args.length === 0) {
2526
- return { stderr: 'rm: missing operand', code: 1 };
2527
- }
2528
-
2876
+ register('rm', async ({ args, stdin, cwd }) => {
2877
+ const argError = VirtualUtils.validateArgs(args, 1, 'rm');
2878
+ if (argError) return argError;
2879
+
2529
2880
  try {
2530
- const fs = await import('fs');
2531
- const path = await import('path');
2532
-
2881
+
2533
2882
  const flags = args.filter(arg => arg.startsWith('-'));
2534
2883
  const targets = args.filter(arg => !arg.startsWith('-'));
2535
2884
  const recursive = flags.includes('-r') || flags.includes('-R');
2536
2885
  const force = flags.includes('-f');
2537
-
2886
+
2538
2887
  for (const target of targets) {
2539
2888
  try {
2540
- const basePath = options?.cwd || process.cwd();
2889
+ const basePath = cwd || process.cwd();
2541
2890
  const fullPath = path.isAbsolute(target) ? target : path.join(basePath, target);
2542
-
2891
+
2543
2892
  const stat = fs.statSync(fullPath);
2544
-
2893
+
2545
2894
  if (stat.isDirectory()) {
2546
2895
  if (!recursive) {
2547
- return {
2548
- stderr: `rm: cannot remove '${target}': Is a directory`,
2549
- code: 1
2896
+ return {
2897
+ stderr: `rm: cannot remove '${target}': Is a directory`,
2898
+ code: 1
2550
2899
  };
2551
2900
  }
2552
2901
  fs.rmSync(fullPath, { recursive: true, force });
@@ -2555,14 +2904,14 @@ function registerBuiltins() {
2555
2904
  }
2556
2905
  } catch (error) {
2557
2906
  if (!force) {
2558
- return {
2559
- stderr: `rm: cannot remove '${target}': ${error.message}`,
2560
- code: 1
2907
+ return {
2908
+ stderr: `rm: cannot remove '${target}': ${error.message}`,
2909
+ code: 1
2561
2910
  };
2562
2911
  }
2563
2912
  }
2564
2913
  }
2565
-
2914
+
2566
2915
  return { stdout: '', code: 0 };
2567
2916
  } catch (error) {
2568
2917
  return { stderr: `rm: ${error.message}`, code: 1 };
@@ -2570,25 +2919,21 @@ function registerBuiltins() {
2570
2919
  });
2571
2920
 
2572
2921
  // mv - move/rename files and directories
2573
- register('mv', async (args, stdin, options) => {
2574
- if (args.length < 2) {
2575
- return { stderr: 'mv: missing destination file operand', code: 1 };
2576
- }
2577
-
2922
+ register('mv', async ({ args, stdin, cwd }) => {
2923
+ const argError = VirtualUtils.validateArgs(args, 2, 'mv');
2924
+ if (argError) return VirtualUtils.invalidArgumentError('mv', 'missing destination file operand');
2925
+
2578
2926
  try {
2579
- const fs = await import('fs');
2580
- const path = await import('path');
2581
-
2582
- const basePath = options?.cwd || process.cwd();
2583
-
2927
+
2928
+ const basePath = cwd || process.cwd();
2929
+
2584
2930
  if (args.length === 2) {
2585
2931
  // Simple rename/move
2586
2932
  const [source, dest] = args;
2587
2933
  const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
2588
2934
  let destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2589
-
2935
+
2590
2936
  try {
2591
- // Check if destination is an existing directory
2592
2937
  try {
2593
2938
  const destStat = fs.statSync(destPath);
2594
2939
  if (destStat.isDirectory()) {
@@ -2599,12 +2944,12 @@ function registerBuiltins() {
2599
2944
  } catch {
2600
2945
  // Destination doesn't exist, proceed with direct rename
2601
2946
  }
2602
-
2947
+
2603
2948
  fs.renameSync(sourcePath, destPath);
2604
2949
  } catch (error) {
2605
- return {
2606
- stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2607
- code: 1
2950
+ return {
2951
+ stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2952
+ code: 1
2608
2953
  };
2609
2954
  }
2610
2955
  } else {
@@ -2612,23 +2957,22 @@ function registerBuiltins() {
2612
2957
  const sources = args.slice(0, -1);
2613
2958
  const dest = args[args.length - 1];
2614
2959
  const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2615
-
2616
- // Check if destination is a directory
2960
+
2617
2961
  try {
2618
2962
  const destStat = fs.statSync(destPath);
2619
2963
  if (!destStat.isDirectory()) {
2620
- return {
2621
- stderr: `mv: target '${dest}' is not a directory`,
2622
- code: 1
2964
+ return {
2965
+ stderr: `mv: target '${dest}' is not a directory`,
2966
+ code: 1
2623
2967
  };
2624
2968
  }
2625
2969
  } catch {
2626
- return {
2627
- stderr: `mv: cannot access '${dest}': No such file or directory`,
2628
- code: 1
2970
+ return {
2971
+ stderr: `mv: cannot access '${dest}': No such file or directory`,
2972
+ code: 1
2629
2973
  };
2630
2974
  }
2631
-
2975
+
2632
2976
  for (const source of sources) {
2633
2977
  try {
2634
2978
  const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
@@ -2636,14 +2980,14 @@ function registerBuiltins() {
2636
2980
  const newDestPath = path.join(destPath, fileName);
2637
2981
  fs.renameSync(sourcePath, newDestPath);
2638
2982
  } catch (error) {
2639
- return {
2640
- stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2641
- code: 1
2983
+ return {
2984
+ stderr: `mv: cannot move '${source}' to '${dest}': ${error.message}`,
2985
+ code: 1
2642
2986
  };
2643
2987
  }
2644
2988
  }
2645
2989
  }
2646
-
2990
+
2647
2991
  return { stdout: '', code: 0 };
2648
2992
  } catch (error) {
2649
2993
  return { stderr: `mv: ${error.message}`, code: 1 };
@@ -2651,35 +2995,32 @@ function registerBuiltins() {
2651
2995
  });
2652
2996
 
2653
2997
  // cp - copy files and directories
2654
- register('cp', async (args, stdin, options) => {
2655
- if (args.length < 2) {
2656
- return { stderr: 'cp: missing destination file operand', code: 1 };
2657
- }
2658
-
2998
+ register('cp', async ({ args, stdin, cwd }) => {
2999
+ const argError = VirtualUtils.validateArgs(args, 2, 'cp');
3000
+ if (argError) return VirtualUtils.invalidArgumentError('cp', 'missing destination file operand');
3001
+
2659
3002
  try {
2660
- const fs = await import('fs');
2661
- const path = await import('path');
2662
-
3003
+
2663
3004
  const flags = args.filter(arg => arg.startsWith('-'));
2664
3005
  const paths = args.filter(arg => !arg.startsWith('-'));
2665
3006
  const recursive = flags.includes('-r') || flags.includes('-R');
2666
-
2667
- const basePath = options?.cwd || process.cwd();
2668
-
3007
+
3008
+ const basePath = cwd || process.cwd();
3009
+
2669
3010
  if (paths.length === 2) {
2670
3011
  // Simple copy
2671
3012
  const [source, dest] = paths;
2672
3013
  const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
2673
3014
  const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2674
-
3015
+
2675
3016
  try {
2676
3017
  const sourceStat = fs.statSync(sourcePath);
2677
-
3018
+
2678
3019
  if (sourceStat.isDirectory()) {
2679
3020
  if (!recursive) {
2680
- return {
2681
- stderr: `cp: -r not specified; omitting directory '${source}'`,
2682
- code: 1
3021
+ return {
3022
+ stderr: `cp: -r not specified; omitting directory '${source}'`,
3023
+ code: 1
2683
3024
  };
2684
3025
  }
2685
3026
  fs.cpSync(sourcePath, destPath, { recursive: true });
@@ -2687,9 +3028,9 @@ function registerBuiltins() {
2687
3028
  fs.copyFileSync(sourcePath, destPath);
2688
3029
  }
2689
3030
  } catch (error) {
2690
- return {
2691
- stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
2692
- code: 1
3031
+ return {
3032
+ stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
3033
+ code: 1
2693
3034
  };
2694
3035
  }
2695
3036
  } else {
@@ -2697,35 +3038,34 @@ function registerBuiltins() {
2697
3038
  const sources = paths.slice(0, -1);
2698
3039
  const dest = paths[paths.length - 1];
2699
3040
  const destPath = path.isAbsolute(dest) ? dest : path.join(basePath, dest);
2700
-
2701
- // Check if destination is a directory
3041
+
2702
3042
  try {
2703
3043
  const destStat = fs.statSync(destPath);
2704
3044
  if (!destStat.isDirectory()) {
2705
- return {
2706
- stderr: `cp: target '${dest}' is not a directory`,
2707
- code: 1
3045
+ return {
3046
+ stderr: `cp: target '${dest}' is not a directory`,
3047
+ code: 1
2708
3048
  };
2709
3049
  }
2710
3050
  } catch {
2711
- return {
2712
- stderr: `cp: cannot access '${dest}': No such file or directory`,
2713
- code: 1
3051
+ return {
3052
+ stderr: `cp: cannot access '${dest}': No such file or directory`,
3053
+ code: 1
2714
3054
  };
2715
3055
  }
2716
-
3056
+
2717
3057
  for (const source of sources) {
2718
3058
  try {
2719
3059
  const sourcePath = path.isAbsolute(source) ? source : path.join(basePath, source);
2720
3060
  const fileName = path.basename(source);
2721
3061
  const newDestPath = path.join(destPath, fileName);
2722
-
3062
+
2723
3063
  const sourceStat = fs.statSync(sourcePath);
2724
3064
  if (sourceStat.isDirectory()) {
2725
3065
  if (!recursive) {
2726
- return {
2727
- stderr: `cp: -r not specified; omitting directory '${source}'`,
2728
- code: 1
3066
+ return {
3067
+ stderr: `cp: -r not specified; omitting directory '${source}'`,
3068
+ code: 1
2729
3069
  };
2730
3070
  }
2731
3071
  fs.cpSync(sourcePath, newDestPath, { recursive: true });
@@ -2733,14 +3073,14 @@ function registerBuiltins() {
2733
3073
  fs.copyFileSync(sourcePath, newDestPath);
2734
3074
  }
2735
3075
  } catch (error) {
2736
- return {
2737
- stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
2738
- code: 1
3076
+ return {
3077
+ stderr: `cp: cannot copy '${source}' to '${dest}': ${error.message}`,
3078
+ code: 1
2739
3079
  };
2740
3080
  }
2741
3081
  }
2742
3082
  }
2743
-
3083
+
2744
3084
  return { stdout: '', code: 0 };
2745
3085
  } catch (error) {
2746
3086
  return { stderr: `cp: ${error.message}`, code: 1 };
@@ -2748,37 +3088,33 @@ function registerBuiltins() {
2748
3088
  });
2749
3089
 
2750
3090
  // touch - create or update file timestamps
2751
- register('touch', async (args, stdin, options) => {
2752
- if (args.length === 0) {
2753
- return { stderr: 'touch: missing file operand', code: 1 };
2754
- }
2755
-
3091
+ register('touch', async ({ args, stdin, cwd }) => {
3092
+ const argError = VirtualUtils.validateArgs(args, 1, 'touch');
3093
+ if (argError) return VirtualUtils.missingOperandError('touch', 'touch: missing file operand');
3094
+
2756
3095
  try {
2757
- const fs = await import('fs');
2758
- const path = await import('path');
2759
-
2760
- const basePath = options?.cwd || process.cwd();
2761
-
3096
+
3097
+ const basePath = cwd || process.cwd();
3098
+
2762
3099
  for (const file of args) {
2763
3100
  try {
2764
3101
  const fullPath = path.isAbsolute(file) ? file : path.join(basePath, file);
2765
-
3102
+
2766
3103
  // Try to update timestamps if file exists
2767
3104
  try {
2768
3105
  const now = new Date();
2769
3106
  fs.utimesSync(fullPath, now, now);
2770
3107
  } catch {
2771
- // File doesn't exist, create it
2772
3108
  fs.writeFileSync(fullPath, '', { flag: 'w' });
2773
3109
  }
2774
3110
  } catch (error) {
2775
- return {
2776
- stderr: `touch: cannot touch '${file}': ${error.message}`,
2777
- code: 1
3111
+ return {
3112
+ stderr: `touch: cannot touch '${file}': ${error.message}`,
3113
+ code: 1
2778
3114
  };
2779
3115
  }
2780
3116
  }
2781
-
3117
+
2782
3118
  return { stdout: '', code: 0 };
2783
3119
  } catch (error) {
2784
3120
  return { stderr: `touch: ${error.message}`, code: 1 };
@@ -2786,24 +3122,22 @@ function registerBuiltins() {
2786
3122
  });
2787
3123
 
2788
3124
  // basename - extract filename from path
2789
- register('basename', async (args) => {
2790
- if (args.length === 0) {
2791
- return { stderr: 'basename: missing operand', code: 1 };
2792
- }
2793
-
3125
+ register('basename', async ({ args }) => {
3126
+ const argError = VirtualUtils.validateArgs(args, 1, 'basename');
3127
+ if (argError) return argError;
3128
+
2794
3129
  try {
2795
- const path = await import('path');
2796
-
3130
+
2797
3131
  const pathname = args[0];
2798
3132
  const suffix = args[1];
2799
-
3133
+
2800
3134
  let result = path.basename(pathname);
2801
-
3135
+
2802
3136
  // Remove suffix if provided
2803
3137
  if (suffix && result.endsWith(suffix)) {
2804
3138
  result = result.slice(0, -suffix.length);
2805
3139
  }
2806
-
3140
+
2807
3141
  return { stdout: result + '\n', code: 0 };
2808
3142
  } catch (error) {
2809
3143
  return { stderr: `basename: ${error.message}`, code: 1 };
@@ -2811,17 +3145,15 @@ function registerBuiltins() {
2811
3145
  });
2812
3146
 
2813
3147
  // dirname - extract directory from path
2814
- register('dirname', async (args) => {
2815
- if (args.length === 0) {
2816
- return { stderr: 'dirname: missing operand', code: 1 };
2817
- }
2818
-
3148
+ register('dirname', async ({ args }) => {
3149
+ const argError = VirtualUtils.validateArgs(args, 1, 'dirname');
3150
+ if (argError) return argError;
3151
+
2819
3152
  try {
2820
- const path = await import('path');
2821
-
3153
+
2822
3154
  const pathname = args[0];
2823
3155
  const result = path.dirname(pathname);
2824
-
3156
+
2825
3157
  return { stdout: result + '\n', code: 0 };
2826
3158
  } catch (error) {
2827
3159
  return { stderr: `dirname: ${error.message}`, code: 1 };
@@ -2829,42 +3161,38 @@ function registerBuiltins() {
2829
3161
  });
2830
3162
 
2831
3163
  // yes - output a string repeatedly
2832
- register('yes', async function* (args, stdin, options) {
3164
+ register('yes', async function* ({ args, stdin, isCancelled, signal, ...rest }) {
2833
3165
  const output = args.length > 0 ? args.join(' ') : 'y';
2834
- trace('VirtualCommand', 'yes: starting infinite generator', { output });
2835
-
3166
+ trace('VirtualCommand', () => `yes: starting infinite generator | ${JSON.stringify({ output }, null, 2)}`);
3167
+
2836
3168
  // Generate infinite stream of the output
2837
3169
  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
- }
3170
+ if (isCancelled && isCancelled()) {
3171
+ trace('VirtualCommand', () => 'yes: cancelled via function');
3172
+ return;
3173
+ }
3174
+ if (signal && signal.aborted) {
3175
+ trace('VirtualCommand', () => 'yes: cancelled via abort signal');
3176
+ return;
2848
3177
  }
2849
-
3178
+
2850
3179
  yield output + '\n';
2851
-
2852
- // Small delay with abort signal support
3180
+
2853
3181
  try {
2854
3182
  await new Promise((resolve, reject) => {
2855
3183
  const timeout = setTimeout(resolve, 0);
2856
-
3184
+
2857
3185
  // Listen for abort signal if available
2858
- if (options && options.signal) {
3186
+ if (signal) {
2859
3187
  const abortHandler = () => {
2860
3188
  clearTimeout(timeout);
2861
3189
  reject(new Error('Aborted'));
2862
3190
  };
2863
-
2864
- if (options.signal.aborted) {
3191
+
3192
+ if (signal.aborted) {
2865
3193
  abortHandler();
2866
3194
  } else {
2867
- options.signal.addEventListener('abort', abortHandler, { once: true });
3195
+ signal.addEventListener('abort', abortHandler, { once: true });
2868
3196
  }
2869
3197
  }
2870
3198
  });
@@ -2876,14 +3204,13 @@ function registerBuiltins() {
2876
3204
  });
2877
3205
 
2878
3206
  // seq - generate sequence of numbers
2879
- register('seq', async (args) => {
2880
- if (args.length === 0) {
2881
- return { stderr: 'seq: missing operand', code: 1 };
2882
- }
2883
-
3207
+ register('seq', async ({ args }) => {
3208
+ const argError = VirtualUtils.validateArgs(args, 1, 'seq');
3209
+ if (argError) return argError;
3210
+
2884
3211
  try {
2885
3212
  let start, step, end;
2886
-
3213
+
2887
3214
  if (args.length === 1) {
2888
3215
  start = 1;
2889
3216
  step = 1;
@@ -2899,11 +3226,11 @@ function registerBuiltins() {
2899
3226
  } else {
2900
3227
  return { stderr: 'seq: too many operands', code: 1 };
2901
3228
  }
2902
-
3229
+
2903
3230
  if (isNaN(start) || isNaN(step) || isNaN(end)) {
2904
3231
  return { stderr: 'seq: invalid number', code: 1 };
2905
3232
  }
2906
-
3233
+
2907
3234
  let output = '';
2908
3235
  if (step > 0) {
2909
3236
  for (let i = start; i <= end; i += step) {
@@ -2916,7 +3243,7 @@ function registerBuiltins() {
2916
3243
  } else {
2917
3244
  return { stderr: 'seq: invalid increment', code: 1 };
2918
3245
  }
2919
-
3246
+
2920
3247
  return { stdout: output, code: 0 };
2921
3248
  } catch (error) {
2922
3249
  return { stderr: `seq: ${error.message}`, code: 1 };
@@ -2924,38 +3251,114 @@ function registerBuiltins() {
2924
3251
  });
2925
3252
 
2926
3253
  // test - test file conditions (basic implementation)
2927
- register('test', async (args) => {
3254
+ register('test', async ({ args }) => {
2928
3255
  if (args.length === 0) {
2929
3256
  return { stdout: '', code: 1 };
2930
3257
  }
2931
-
3258
+
2932
3259
  // Very basic test implementation
2933
3260
  const arg = args[0];
2934
-
3261
+
2935
3262
  try {
2936
3263
  if (arg === '-d' && args[1]) {
2937
3264
  // Test if directory
2938
- const stat = require('fs').statSync(args[1]);
3265
+ const stat = fs.statSync(args[1]);
2939
3266
  return { stdout: '', code: stat.isDirectory() ? 0 : 1 };
2940
3267
  } else if (arg === '-f' && args[1]) {
2941
3268
  // Test if file
2942
- const stat = require('fs').statSync(args[1]);
3269
+ const stat = fs.statSync(args[1]);
2943
3270
  return { stdout: '', code: stat.isFile() ? 0 : 1 };
2944
3271
  } else if (arg === '-e' && args[1]) {
2945
3272
  // Test if exists
2946
- require('fs').statSync(args[1]);
3273
+ fs.statSync(args[1]);
2947
3274
  return { stdout: '', code: 0 };
2948
3275
  }
2949
3276
  } catch {
2950
3277
  return { stdout: '', code: 1 };
2951
3278
  }
2952
-
3279
+
2953
3280
  return { stdout: '', code: 1 };
2954
3281
  });
2955
3282
  }
2956
3283
 
3284
+ // ANSI control character utilities
3285
+ const AnsiUtils = {
3286
+ stripAnsi(text) {
3287
+ if (typeof text !== 'string') return text;
3288
+ return text.replace(/\x1b\[[0-9;]*[mGKHFJ]/g, '');
3289
+ },
3290
+
3291
+ stripControlChars(text) {
3292
+ if (typeof text !== 'string') return text;
3293
+ return text.replace(/[\x00-\x1F\x7F]/g, '');
3294
+ },
3295
+
3296
+ stripAll(text) {
3297
+ if (typeof text !== 'string') return text;
3298
+ return text.replace(/[\x00-\x1F\x7F]|\x1b\[[0-9;]*[mGKHFJ]/g, '');
3299
+ },
3300
+
3301
+ cleanForProcessing(data) {
3302
+ if (Buffer.isBuffer(data)) {
3303
+ return Buffer.from(this.stripAll(data.toString('utf8')));
3304
+ }
3305
+ return this.stripAll(data);
3306
+ }
3307
+ };
3308
+
3309
+ let globalAnsiConfig = {
3310
+ preserveAnsi: true,
3311
+ preserveControlChars: true
3312
+ };
3313
+
3314
+ function configureAnsi(options = {}) {
3315
+ globalAnsiConfig = { ...globalAnsiConfig, ...options };
3316
+ return globalAnsiConfig;
3317
+ }
3318
+
3319
+ function getAnsiConfig() {
3320
+ return { ...globalAnsiConfig };
3321
+ }
3322
+
3323
+ function processOutput(data, options = {}) {
3324
+ const config = { ...globalAnsiConfig, ...options };
3325
+ if (!config.preserveAnsi && !config.preserveControlChars) {
3326
+ return AnsiUtils.cleanForProcessing(data);
3327
+ } else if (!config.preserveAnsi) {
3328
+ return Buffer.isBuffer(data)
3329
+ ? Buffer.from(AnsiUtils.stripAnsi(data.toString('utf8')))
3330
+ : AnsiUtils.stripAnsi(data);
3331
+ } else if (!config.preserveControlChars) {
3332
+ return Buffer.isBuffer(data)
3333
+ ? Buffer.from(AnsiUtils.stripControlChars(data.toString('utf8')))
3334
+ : AnsiUtils.stripControlChars(data);
3335
+ }
3336
+ return data;
3337
+ }
3338
+
2957
3339
  // Initialize built-in commands
2958
3340
  registerBuiltins();
2959
3341
 
2960
- export { $tagged as $, sh, exec, run, quote, create, raw, ProcessRunner, shell, set, unset, register, unregister, listCommands, enableVirtualCommands, disableVirtualCommands };
3342
+ export {
3343
+ $tagged as $,
3344
+ sh,
3345
+ exec,
3346
+ run,
3347
+ quote,
3348
+ create,
3349
+ raw,
3350
+ ProcessRunner,
3351
+ shell,
3352
+ set,
3353
+ unset,
3354
+ register,
3355
+ unregister,
3356
+ listCommands,
3357
+ enableVirtualCommands,
3358
+ disableVirtualCommands,
3359
+ AnsiUtils,
3360
+ configureAnsi,
3361
+ getAnsiConfig,
3362
+ processOutput
3363
+ };
2961
3364
  export default $tagged;