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