command-stream 0.2.0 → 0.3.1

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