command-stream 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  [![npm](https://img.shields.io/npm/v/command-stream.svg)](https://npmjs.com/command-stream)
2
2
  [![License](https://img.shields.io/badge/license-Unlicense-blue.svg)](https://github.com/link-foundation/command-stream/blob/main/LICENSE)
3
+ [![GitHub stars](https://img.shields.io/github/stars/link-foundation/command-stream?style=social)](https://github.com/link-foundation/command-stream/stargazers)
3
4
 
4
5
  [![Open in Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-f29718?logo=gitpod)](https://gitpod.io/#https://github.com/link-foundation/command-stream)
5
6
  [![Open in GitHub Codespaces](https://img.shields.io/badge/GitHub%20Codespaces-Open-181717?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=link-foundation/command-stream)
@@ -26,39 +27,48 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
26
27
 
27
28
  ## Comparison with Other Libraries
28
29
 
29
- | Feature | [command-stream](https://github.com/link-foundation/command-stream) | [Bun.$](https://bun.sh/docs/runtime/shell) | [execa](https://github.com/sindresorhus/execa) | [zx](https://github.com/google/zx) | [ShellJS](https://github.com/shelljs/shelljs) | [cross-spawn](https://github.com/moxystudio/node-cross-spawn) |
30
+ | Feature | [**command-stream**](https://github.com/link-foundation/command-stream) | [**execa**](https://github.com/sindresorhus/execa) | [**cross-spawn**](https://github.com/moxystudio/node-cross-spawn) | [**Bun.$**](https://github.com/oven-sh/bun) | [**ShellJS**](https://github.com/shelljs/shelljs) | [**zx**](https://github.com/google/zx) |
30
31
  |---------|----------------|-------|-------|-----|-------|-------|
31
- | **Runtime Support** | ✅ Bun + Node.js | 🟡 Bun only | ✅ Node.js | Node.js | ✅ Node.js | ✅ Node.js |
32
- | **Template Literals** | `` $`cmd` `` | `` $`cmd` `` | `` $`cmd` `` | `` $`cmd` `` | Function calls | Function calls |
33
- | **Real-time Streaming** | Live output | Buffer only | 🟡 Limited | Buffer only | Buffer only | Buffer only |
34
- | **Synchronous Execution** | `.sync()` with events | No | `execaSync` | No | Sync by default | `spawnSync` |
32
+ | **📦 NPM Package** | [![npm](https://img.shields.io/npm/v/command-stream.svg)](https://www.npmjs.com/package/command-stream) | [![npm](https://img.shields.io/npm/v/execa.svg)](https://www.npmjs.com/package/execa) | [![npm](https://img.shields.io/npm/v/cross-spawn.svg)](https://www.npmjs.com/package/cross-spawn) | N/A (Built-in) | [![npm](https://img.shields.io/npm/v/shelljs.svg)](https://www.npmjs.com/package/shelljs) | [![npm](https://img.shields.io/npm/v/zx.svg)](https://www.npmjs.com/package/zx) |
33
+ | **⭐ GitHub Stars** | [**⭐ 2** (Please us!)](https://github.com/link-foundation/command-stream) | [⭐ 7,264](https://github.com/sindresorhus/execa) | [⭐ 1,149](https://github.com/moxystudio/node-cross-spawn) | [⭐ 80,169](https://github.com/oven-sh/bun) (Full Runtime) | [⭐ 14,375](https://github.com/shelljs/shelljs) | [⭐ 44,569](https://github.com/google/zx) |
34
+ | **📊 Monthly Downloads** | **893** (New project!) | **381M** | **409M** | N/A (Built-in) | **35M** | **4.2M** |
35
+ | **📈 Total Downloads** | **Growing** | **6B+** | **5.4B** | N/A (Built-in) | **596M** | **37M** |
36
+ | **Runtime Support** | ✅ Bun + Node.js | ✅ Node.js | ✅ Node.js | 🟡 Bun only | ✅ Node.js | ✅ Node.js |
37
+ | **Template Literals** | ✅ `` $`cmd` `` | ✅ `` $`cmd` `` | ❌ Function calls | ✅ `` $`cmd` `` | ❌ Function calls | ✅ `` $`cmd` `` |
38
+ | **Real-time Streaming** | ✅ Live output | 🟡 Limited | ❌ Buffer only | ❌ Buffer only | ❌ Buffer only | ❌ Buffer only |
39
+ | **Synchronous Execution** | ✅ `.sync()` with events | ✅ `execaSync` | ✅ `spawnSync` | ❌ No | ✅ Sync by default | ❌ No |
35
40
  | **Async Iteration** | ✅ `for await (chunk of $.stream())` | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
36
- | **EventEmitter Pattern** | ✅ `.on('data', ...)` | No | 🟡 Limited events | ❌ No | ❌ No | 🟡 Child process events |
41
+ | **EventEmitter Pattern** | ✅ `.on('data', ...)` | 🟡 Limited events | 🟡 Child process events | ❌ No | ❌ No | No |
37
42
  | **Mixed Patterns** | ✅ Events + await/sync | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
38
- | **Bun.$ Compatibility** | ✅ `.text()` method support | Native API | ❌ No | No | ❌ No | ❌ No |
39
- | **Shell Injection Protection** | ✅ Auto-quoting | ✅ Built-in | ✅ Safe by default | ✅ Safe by default | 🟡 Manual escaping | ✅ Safe by default |
40
- | **Cross-platform** | ✅ macOS/Linux/Windows | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ **Specialized** cross-platform |
41
- | **Performance** | ⚡ Fast (Bun optimized) | Very fast | 🐌 Moderate | 🐌 Slow | 🐌 Moderate | Fast |
43
+ | **Bun.$ Compatibility** | ✅ `.text()` method support | No | ❌ No | Native API | ❌ No | ❌ No |
44
+ | **Shell Injection Protection** | ✅ Auto-quoting | ✅ Safe by default | ✅ Safe by default | ✅ Built-in | 🟡 Manual escaping | ✅ Safe by default |
45
+ | **Cross-platform** | ✅ macOS/Linux/Windows | ✅ Yes | ✅ **Specialized** cross-platform | ✅ Yes | ✅ Yes | ✅ Yes |
46
+ | **Performance** | ⚡ Fast (Bun optimized) | 🐌 Moderate | Fast | Very fast | 🐌 Moderate | 🐌 Slow |
42
47
  | **Memory Efficiency** | ✅ Streaming prevents buildup | 🟡 Buffers in memory | 🟡 Buffers in memory | 🟡 Buffers in memory | 🟡 Buffers in memory | 🟡 Buffers in memory |
43
- | **Error Handling** | ✅ Configurable (`set -e`/`set +e`, non-zero OK by default) | ✅ Throws on error | Throws on error | ✅ Throws on error | ✅ Configurable | Basic (exit codes) |
48
+ | **Error Handling** | ✅ Configurable (`set -e`/`set +e`, non-zero OK by default) | ✅ Throws on error | Basic (exit codes) | ✅ Throws on error | ✅ Configurable | Throws on error |
44
49
  | **Shell Settings** | ✅ `set -e`/`set +e` equivalent | ❌ No | ❌ No | ❌ No | 🟡 Limited (`set()`) | ❌ No |
45
- | **Stdout Support** | ✅ Real-time streaming + events | ✅ Shell redirection + buffered | ✅ Node.js streams + interleaved | ✅ Readable streams + `.pipe.stdout` | ✅ Direct output | ✅ Inherited/buffered |
46
- | **Stderr Support** | ✅ Real-time streaming + events | ✅ Redirection + `.quiet()` access | ✅ Streams + interleaved output | ✅ Readable streams + `.pipe.stderr` | ✅ Error output | ✅ Inherited/buffered |
47
- | **Stdin Support** | ✅ string/Buffer/inherit/ignore | ✅ Pipe operations | ✅ Input/output streams | ✅ Basic stdin | 🟡 Basic | ✅ Full stdio support |
48
- | **Built-in Commands** | ✅ **18 commands**: cat, ls, mkdir, rm, mv, cp, touch, basename, dirname, seq, yes + all Bun.$ commands | echo, cd, etc. | ❌ Uses system | Uses system | ✅ **20+ commands**: cat, ls, mkdir, rm, mv, cp, etc. | ❌ Uses system |
49
- | **Virtual Commands Engine** | ✅ **Revolutionary**: Register JavaScript functions as shell commands with full pipeline support | ❌ No extensibility | ❌ No custom commands | ❌ No custom commands | ❌ No custom commands | ❌ No custom commands |
50
- | **Pipeline/Piping Support** | ✅ **Advanced**: System + Built-ins + Virtual + Mixed + `.pipe()` method | ✅ Standard shell piping | ✅ Programmatic `.pipe()` + multi-destination | ✅ Shell piping + `.pipe()` method | ✅ Shell piping + `.to()` method | ❌ No piping |
51
- | **Bundle Size** | 📦 **~20KB gzipped** | 🎯 0KB (built-in) | 📦 ~400KB+ (packagephobia) | 📦 ~50KB+ (estimated) | 📦 ~15KB gzipped | 📦 ~2KB gzipped |
52
- | **Signal Handling** | ✅ **Advanced SIGINT/SIGTERM forwarding** with cleanup | 🟡 Basic | 🟡 Basic | 🟡 Basic | 🟡 Basic | **Excellent** cross-platform |
53
- | **Process Management** | ✅ **Robust child process lifecycle** with proper termination | Basic | ✅ Good | 🟡 Limited | 🟡 Limited | **Excellent** spawn wrapper |
54
- | **Debug Tracing** | ✅ **Comprehensive VERBOSE logging** for CI/debugging | No | 🟡 Limited | ❌ No | 🟡 Basic | ❌ No |
55
- | **Test Coverage** | ✅ **410 tests, 909 assertions** | 🟡 Good coverage | ✅ Excellent | 🟡 Good | ✅ Good | Good |
56
- | **CI Reliability** | ✅ **Platform-specific handling** (macOS/Ubuntu) | 🟡 Basic | ✅ Good | 🟡 Basic | ✅ Good | **Excellent** |
57
- | **Documentation** | ✅ **Comprehensive examples + guides** | ✅ Good | Excellent | 🟡 Limited | ✅ Good | 🟡 Basic |
58
- | **TypeScript** | 🔄 Coming soon | ✅ Built-in | ✅ Full support | ✅ Full support | 🟡 Community types | ✅ Built-in |
59
- | **License** | ✅ **Unlicense (Public Domain)** | 🟡 MIT (+ LGPL dependencies) | 🟡 MIT | 🟡 Apache 2.0 | 🟡 BSD-3-Clause | 🟡 MIT |
60
-
61
- **📊 Popularity (Weekly Downloads 2024):** [cross-spawn](https://www.npmjs.com/package/cross-spawn): 102M+ • [execa](https://www.npmjs.com/package/execa): 98M+ • [ShellJS](https://www.npmjs.com/package/shelljs): 9M+ • [zx](https://www.npmjs.com/package/zx): Growing fast
50
+ | **Stdout Support** | ✅ Real-time streaming + events | ✅ Node.js streams + interleaved | ✅ Inherited/buffered | ✅ Shell redirection + buffered | ✅ Direct output | ✅ Readable streams + `.pipe.stdout` |
51
+ | **Stderr Support** | ✅ Real-time streaming + events | ✅ Streams + interleaved output | ✅ Inherited/buffered | ✅ Redirection + `.quiet()` access | ✅ Error output | ✅ Readable streams + `.pipe.stderr` |
52
+ | **Stdin Support** | ✅ string/Buffer/inherit/ignore | ✅ Input/output streams | ✅ Full stdio support | ✅ Pipe operations | 🟡 Basic | ✅ Basic stdin |
53
+ | **Built-in Commands** | ✅ **18 commands**: cat, ls, mkdir, rm, mv, cp, touch, basename, dirname, seq, yes + all Bun.$ commands | Uses system | ❌ Uses system | echo, cd, etc. | ✅ **20+ commands**: cat, ls, mkdir, rm, mv, cp, etc. | ❌ Uses system |
54
+ | **Virtual Commands Engine** | ✅ **Revolutionary**: Register JavaScript functions as shell commands with full pipeline support | ❌ No custom commands | ❌ No custom commands | ❌ No extensibility | ❌ No custom commands | ❌ No custom commands |
55
+ | **Pipeline/Piping Support** | ✅ **Advanced**: System + Built-ins + Virtual + Mixed + `.pipe()` method | ✅ Programmatic `.pipe()` + multi-destination | ❌ No piping | Standard shell piping | ✅ Shell piping + `.to()` method | ✅ Shell piping + `.pipe()` method |
56
+ | **Bundle Size** | 📦 **~20KB gzipped** | 📦 ~400KB+ (packagephobia) | 📦 ~2KB gzipped | 🎯 0KB (built-in) | 📦 ~15KB gzipped | 📦 ~50KB+ (estimated) |
57
+ | **Signal Handling** | ✅ **Advanced SIGINT/SIGTERM forwarding** with cleanup | 🟡 Basic | **Excellent** cross-platform | 🟡 Basic | 🟡 Basic | 🟡 Basic |
58
+ | **Process Management** | ✅ **Robust child process lifecycle** with proper termination | Good | ✅ **Excellent** spawn wrapper | Basic | 🟡 Limited | 🟡 Limited |
59
+ | **Debug Tracing** | ✅ **Comprehensive VERBOSE logging** for CI/debugging | 🟡 Limited | No | ❌ No | 🟡 Basic | ❌ No |
60
+ | **Test Coverage** | ✅ **410 tests, 909 assertions** | Excellent | ✅ Good | 🟡 Good coverage | ✅ Good | 🟡 Good |
61
+ | **CI Reliability** | ✅ **Platform-specific handling** (macOS/Ubuntu) | Good | ✅ **Excellent** | 🟡 Basic | ✅ Good | 🟡 Basic |
62
+ | **Documentation** | ✅ **Comprehensive examples + guides** | ✅ Excellent | 🟡 Basic | Good | ✅ Good | 🟡 Limited |
63
+ | **TypeScript** | 🔄 Coming soon | ✅ Full support | ✅ Built-in | ✅ Built-in | 🟡 Community types | ✅ Full support |
64
+ | **License** | ✅ **Unlicense (Public Domain)** | 🟡 MIT | 🟡 MIT | 🟡 MIT (+ LGPL dependencies) | 🟡 BSD-3-Clause | 🟡 Apache 2.0 |
65
+
66
+ **📊 Popularity & Adoption:**
67
+ - **⭐ GitHub Stars:** [Bun: 80,169](https://github.com/oven-sh/bun) • [zx: 44,569](https://github.com/google/zx) • [ShellJS: 14,375](https://github.com/shelljs/shelljs) • [execa: 7,264](https://github.com/sindresorhus/execa) • [cross-spawn: 1,149](https://github.com/moxystudio/node-cross-spawn) • [**command-stream: 2 ⭐ us!**](https://github.com/link-foundation/command-stream)
68
+ - **📈 Total Downloads:** [execa: 6B+](https://www.npmjs.com/package/execa) • [cross-spawn: 5.4B](https://www.npmjs.com/package/cross-spawn) • [ShellJS: 596M](https://www.npmjs.com/package/shelljs) • [zx: 37M](https://www.npmjs.com/package/zx) • [command-stream: Growing](https://www.npmjs.com/package/command-stream)
69
+ - **📊 Monthly Downloads:** [cross-spawn: 409M](https://www.npmjs.com/package/cross-spawn) • [execa: 381M](https://www.npmjs.com/package/execa) • [ShellJS: 35M](https://www.npmjs.com/package/shelljs) • [zx: 4.2M](https://www.npmjs.com/package/zx) • [command-stream: 893 (growing!)](https://www.npmjs.com/package/command-stream)
70
+
71
+ **⭐ Help Us Grow!** If command-stream's **revolutionary virtual commands** and **advanced streaming capabilities** help your project, [**please star us on GitHub**](https://github.com/link-foundation/command-stream) to help the project grow!
62
72
 
63
73
  ### Why Choose command-stream?
64
74
 
@@ -291,6 +301,83 @@ syncCmd.on('end', result => {
291
301
  const syncResult = syncCmd.sync();
292
302
  ```
293
303
 
304
+ ### Streaming Interfaces
305
+
306
+ Advanced streaming interfaces for fine-grained process control:
307
+
308
+ ```javascript
309
+ import { $ } from 'command-stream';
310
+
311
+ // 🎯 STDIN CONTROL: Send data to interactive commands (real-time)
312
+ const grepCmd = $`grep "important"`;
313
+ const stdin = await grepCmd.streams.stdin; // Available immediately
314
+
315
+ stdin.write('ignore this line\n');
316
+ stdin.write('important message\n');
317
+ stdin.write('skip this too\n');
318
+ stdin.end();
319
+
320
+ const result = await grepCmd;
321
+ console.log(result.stdout); // "important message\n"
322
+
323
+ // 🔧 BINARY DATA: Access raw buffers (after command finishes)
324
+ const cmd = $`echo "Hello World"`;
325
+ const buffer = await cmd.buffers.stdout; // Complete snapshot
326
+ console.log(buffer.length); // 12
327
+
328
+ // 📝 TEXT DATA: Access as strings (after command finishes)
329
+ const textCmd = $`echo "Hello World"`;
330
+ const text = await textCmd.strings.stdout; // Complete snapshot
331
+ console.log(text.trim()); // "Hello World"
332
+
333
+ // ⚡ PROCESS CONTROL: Kill commands that ignore stdin
334
+ const pingCmd = $`ping google.com`;
335
+
336
+ // Some commands ignore stdin input
337
+ const pingStdin = await pingCmd.streams.stdin;
338
+ if (pingStdin) {
339
+ pingStdin.write('q\n'); // ping ignores this
340
+ }
341
+
342
+ // Use kill() for forceful termination
343
+ setTimeout(() => pingCmd.kill(), 2000);
344
+ const pingResult = await pingCmd;
345
+ console.log('Ping stopped with code:', pingResult.code); // 143 (SIGTERM)
346
+
347
+ // 🔄 MIXED STDOUT/STDERR: Handle both streams (complete snapshots)
348
+ const mixedCmd = $`sh -c 'echo "out" && echo "err" >&2'`;
349
+ const [stdout, stderr] = await Promise.all([
350
+ mixedCmd.strings.stdout, // Available after finish
351
+ mixedCmd.strings.stderr // Available after finish
352
+ ]);
353
+ console.log('Out:', stdout.trim()); // "out"
354
+ console.log('Err:', stderr.trim()); // "err"
355
+
356
+ // 🏃‍♂️ AUTO-START: Streams auto-start processes when accessed
357
+ const cmd = $`echo "test"`;
358
+ console.log('Started?', cmd.started); // false
359
+
360
+ const output = await cmd.streams.stdout; // Auto-starts, immediate access
361
+ console.log('Started?', cmd.started); // true
362
+
363
+ // 🔙 BACKWARD COMPATIBLE: Traditional await still works
364
+ const traditional = await $`echo "still works"`;
365
+ console.log(traditional.stdout); // "still works\n"
366
+ ```
367
+
368
+ **Key Features:**
369
+ - `command.streams.stdin/stdout/stderr` - Direct access to Node.js streams
370
+ - `command.buffers.stdin/stdout/stderr` - Binary data as Buffer objects
371
+ - `command.strings.stdin/stdout/stderr` - Text data as strings
372
+ - `command.kill()` - Forceful process termination
373
+ - **Auto-start behavior:** Process starts only when accessing stream properties
374
+ - **Perfect for:** Interactive commands (grep, sort, bc), data processing, real-time control
375
+ - **Network commands (ping, wget) ignore stdin** → Use `kill()` method instead
376
+
377
+ **🚀 Streams vs Buffers/Strings:**
378
+ - **`streams.*`** - Available **immediately** when command starts, for real-time interaction
379
+ - **`buffers.*` & `strings.*`** - Complete **snapshots** available only **after** command finishes
380
+
294
381
  ### Shell Replacement (.sh → .mjs)
295
382
 
296
383
  Replace bash scripts with JavaScript while keeping shell semantics:
@@ -367,7 +454,7 @@ Create custom commands that work seamlessly alongside built-ins:
367
454
  import { $, register, unregister, listCommands } from 'command-stream';
368
455
 
369
456
  // Register a custom command
370
- register('greet', async (args, stdin) => {
457
+ register('greet', async ({ args, stdin }) => {
371
458
  const name = args[0] || 'World';
372
459
  return { stdout: `Hello, ${name}!\n`, code: 0 };
373
460
  });
@@ -377,7 +464,7 @@ await $`greet Alice`; // → "Hello, Alice!"
377
464
  await $`echo "Bob" | greet`; // → "Hello, Bob!"
378
465
 
379
466
  // Streaming virtual commands with async generators
380
- register('countdown', async function* (args) {
467
+ register('countdown', async function* ({ args }) {
381
468
  const start = parseInt(args[0] || 5);
382
469
  for (let i = start; i >= 0; i--) {
383
470
  yield `${i}\n`;
@@ -414,7 +501,7 @@ await execa('node', ['script.js']); // execa: separate processes
414
501
  await $`node script.js`; // zx: shell commands only
415
502
 
416
503
  // ✅ command-stream: JavaScript functions AS shell commands
417
- register('deploy', async (args) => {
504
+ register('deploy', async ({ args }) => {
418
505
  const env = args[0] || 'staging';
419
506
  await deployToEnvironment(env);
420
507
  return { stdout: `Deployed to ${env}!\n`, code: 0 };
@@ -450,11 +537,11 @@ await $`seq 1 5 | cat > numbers.txt`;
450
537
  await $`git log --oneline | head -n 5`;
451
538
 
452
539
  // 🚀 UNIQUE: Virtual command piping
453
- register('uppercase', async (args, stdin) => {
540
+ register('uppercase', async ({ args, stdin }) => {
454
541
  return { stdout: stdin.toUpperCase(), code: 0 };
455
542
  });
456
543
 
457
- register('reverse', async (args, stdin) => {
544
+ register('reverse', async ({ args, stdin }) => {
458
545
  return { stdout: stdin.split('').reverse().join(''), code: 0 };
459
546
  });
460
547
 
@@ -482,12 +569,12 @@ import { $, register } from 'command-stream';
482
569
  const result = await $`echo "hello"`.pipe($`echo "World: $(cat)"`);
483
570
 
484
571
  // 🌟 Virtual command chaining
485
- register('add-prefix', async (args, stdin) => {
572
+ register('add-prefix', async ({ args, stdin }) => {
486
573
  const prefix = args[0] || 'PREFIX:';
487
574
  return { stdout: `${prefix} ${stdin.trim()}\n`, code: 0 };
488
575
  });
489
576
 
490
- register('add-suffix', async (args, stdin) => {
577
+ register('add-suffix', async ({ args, stdin }) => {
491
578
  const suffix = args[0] || 'SUFFIX';
492
579
  return { stdout: `${stdin.trim()} ${suffix}\n`, code: 0 };
493
580
  });
@@ -512,7 +599,7 @@ try {
512
599
  }
513
600
 
514
601
  // ✅ Complex data processing
515
- register('json-parse', async (args, stdin) => {
602
+ register('json-parse', async ({ args, stdin }) => {
516
603
  try {
517
604
  const data = JSON.parse(stdin);
518
605
  return { stdout: JSON.stringify(data, null, 2), code: 0 };
@@ -521,7 +608,7 @@ register('json-parse', async (args, stdin) => {
521
608
  }
522
609
  });
523
610
 
524
- register('extract-field', async (args, stdin) => {
611
+ register('extract-field', async ({ args, stdin }) => {
525
612
  const field = args[0];
526
613
  try {
527
614
  const data = JSON.parse(stdin);
@@ -778,9 +865,9 @@ Control and extend the command system with custom JavaScript functions:
778
865
  import { $, register } from 'command-stream';
779
866
 
780
867
  // ✅ Cancellation support with AbortController
781
- register('cancellable', async function* (args, stdin, options) {
868
+ register('cancellable', async function* ({ args, stdin, abortSignal }) {
782
869
  for (let i = 0; i < 10; i++) {
783
- if (options.signal?.aborted) {
870
+ if (abortSignal?.aborted) {
784
871
  break; // Proper cancellation handling
785
872
  }
786
873
  yield `Count: ${i}\n`;
@@ -789,22 +876,28 @@ register('cancellable', async function* (args, stdin, options) {
789
876
  });
790
877
 
791
878
  // ✅ Access to all process options
792
- register('debug-info', async (args, stdin, options) => {
879
+ // All original options (built-in + custom) are available in the 'options' object
880
+ // Common options like cwd, env are also available at top level for convenience
881
+ // Runtime additions: isCancelled function, abortSignal
882
+ register('debug-info', async ({ args, stdin, cwd, env, options, isCancelled }) => {
793
883
  return {
794
884
  stdout: JSON.stringify({
795
885
  args,
796
- cwd: options.cwd,
797
- env: Object.keys(options.env || {}),
798
- stdinLength: stdin.length,
799
- mirror: options.mirror,
800
- capture: options.capture
886
+ cwd, // Available at top level for convenience
887
+ env: Object.keys(env || {}), // Available at top level for convenience
888
+ stdinLength: stdin?.length || 0,
889
+ allOptions: options, // All original options (built-in + custom)
890
+ mirror: options.mirror, // Built-in option from options object
891
+ capture: options.capture, // Built-in option from options object
892
+ customOption: options.customOption || 'not provided', // Custom option
893
+ isCancelledAvailable: typeof isCancelled === 'function'
801
894
  }, null, 2),
802
895
  code: 0
803
896
  };
804
897
  });
805
898
 
806
899
  // ✅ Error handling and non-zero exit codes
807
- register('maybe-fail', async (args) => {
900
+ register('maybe-fail', async ({ args }) => {
808
901
  if (Math.random() > 0.5) {
809
902
  return {
810
903
  stdout: 'Success!\n',
@@ -818,13 +911,27 @@ register('maybe-fail', async (args) => {
818
911
  };
819
912
  }
820
913
  });
914
+
915
+ // ✅ Example: User options flow through to virtual commands
916
+ register('show-options', async ({ args, stdin, options, cwd }) => {
917
+ return {
918
+ stdout: `Custom: ${options.customValue || 'none'}, CWD: ${cwd || options.cwd || 'default'}\n`,
919
+ code: 0
920
+ };
921
+ });
922
+
923
+ // Usage example showing options passed to virtual command:
924
+ const result = await $({ customValue: 'hello world', cwd: '/tmp' })`show-options`;
925
+ console.log(result.stdout); // Output: Custom: hello world, CWD: /tmp
821
926
  ```
822
927
 
823
928
  #### Handler Function Signature
824
929
 
825
930
  ```javascript
826
931
  // Regular async function
827
- async function handler(args, stdin, options) {
932
+ async function handler({ args, stdin, abortSignal, cwd, env, options, isCancelled }) {
933
+ // All original options available in 'options': options.mirror, options.capture, options.customValue, etc.
934
+ // Common options like cwd, env also available at top level for convenience
828
935
  return {
829
936
  code: 0, // Exit code (number)
830
937
  stdout: "output", // Standard output (string)
@@ -833,10 +940,13 @@ async function handler(args, stdin, options) {
833
940
  }
834
941
 
835
942
  // Async generator for streaming
836
- async function* streamingHandler(args, stdin, options) {
943
+ async function streamingHandler({ args, stdin, abortSignal, cwd, env, options, isCancelled }) {
944
+ // Access both built-in and custom options from 'options' object
945
+ if (options.customFlag) {
946
+ yield "custom behavior\n";
947
+ }
837
948
  yield "chunk1\n";
838
949
  yield "chunk2\n";
839
- // Each yield sends a chunk in real-time
840
950
  }
841
951
  ```
842
952
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "command-stream",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
5
5
  "type": "module",
6
6
  "main": "src/$.mjs",
package/src/$.mjs CHANGED
@@ -513,6 +513,14 @@ class StreamEmitter {
513
513
  return this;
514
514
  }
515
515
 
516
+ once(event, listener) {
517
+ const onceWrapper = (...args) => {
518
+ this.off(event, onceWrapper);
519
+ listener(...args);
520
+ };
521
+ return this.on(event, onceWrapper);
522
+ }
523
+
516
524
  emit(event, ...args) {
517
525
  const eventListeners = this.listeners.get(event);
518
526
  trace('StreamEmitter', () => `Emitting event | ${JSON.stringify({
@@ -521,7 +529,9 @@ class StreamEmitter {
521
529
  listenerCount: eventListeners?.length || 0
522
530
  })}`);
523
531
  if (eventListeners) {
524
- for (const listener of eventListeners) {
532
+ // Create a copy to avoid issues if listeners modify the array
533
+ const listenersToCall = [...eventListeners];
534
+ for (const listener of listenersToCall) {
525
535
  listener(...args);
526
536
  }
527
537
  }
@@ -655,6 +665,281 @@ class ProcessRunner extends StreamEmitter {
655
665
  return this.child ? this.child.stdin : null;
656
666
  }
657
667
 
668
+ // Issue #33: New streaming interfaces
669
+ _autoStartIfNeeded(reason) {
670
+ if (!this.started && !this.finished) {
671
+ trace('ProcessRunner', () => `Auto-starting process due to ${reason}`);
672
+ this.start({ mode: 'async', stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
673
+ }
674
+ }
675
+
676
+ get streams() {
677
+ const self = this;
678
+ return {
679
+ get stdin() {
680
+ trace('ProcessRunner.streams', () => `stdin access | ${JSON.stringify({
681
+ hasChild: !!self.child,
682
+ hasStdin: !!(self.child && self.child.stdin),
683
+ started: self.started,
684
+ finished: self.finished,
685
+ hasPromise: !!self.promise,
686
+ command: self.spec?.command?.slice(0, 50)
687
+ }, null, 2)}`);
688
+
689
+ self._autoStartIfNeeded('streams.stdin access');
690
+
691
+ // Streams are available immediately after spawn, or null if not piped
692
+ // Return the stream directly if available, otherwise ensure process starts
693
+ if (self.child && self.child.stdin) {
694
+ trace('ProcessRunner.streams', () => 'stdin: returning existing stream');
695
+ return self.child.stdin;
696
+ }
697
+ if (self.finished) {
698
+ trace('ProcessRunner.streams', () => 'stdin: process finished, returning null');
699
+ return null;
700
+ }
701
+
702
+ // For virtual commands, there's no child process
703
+ // Exception: virtual commands with stdin: "pipe" will fallback to real commands
704
+ const isVirtualCommand = self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]));
705
+ const willFallbackToReal = isVirtualCommand && self.options.stdin === 'pipe';
706
+
707
+ if (isVirtualCommand && !willFallbackToReal) {
708
+ trace('ProcessRunner.streams', () => 'stdin: virtual command, returning null');
709
+ return null;
710
+ }
711
+
712
+ // If not started, start it and wait for child to be created (not for completion!)
713
+ if (!self.started) {
714
+ trace('ProcessRunner.streams', () => 'stdin: not started, starting and waiting for child');
715
+ // Start the process
716
+ self._startAsync();
717
+ // Wait for child to be created using async iteration
718
+ return new Promise((resolve) => {
719
+ const checkForChild = () => {
720
+ if (self.child && self.child.stdin) {
721
+ resolve(self.child.stdin);
722
+ } else if (self.finished || self._virtualGenerator) {
723
+ resolve(null);
724
+ } else {
725
+ // Use setImmediate to check again in next event loop iteration
726
+ setImmediate(checkForChild);
727
+ }
728
+ };
729
+ setImmediate(checkForChild);
730
+ });
731
+ }
732
+
733
+ // Process is starting - wait for child to appear
734
+ if (self.promise && !self.child) {
735
+ trace('ProcessRunner.streams', () => 'stdin: process starting, waiting for child');
736
+ return new Promise((resolve) => {
737
+ const checkForChild = () => {
738
+ if (self.child && self.child.stdin) {
739
+ resolve(self.child.stdin);
740
+ } else if (self.finished || self._virtualGenerator) {
741
+ resolve(null);
742
+ } else {
743
+ setImmediate(checkForChild);
744
+ }
745
+ };
746
+ setImmediate(checkForChild);
747
+ });
748
+ }
749
+
750
+ trace('ProcessRunner.streams', () => 'stdin: returning null (no conditions met)');
751
+ return null;
752
+ },
753
+ get stdout() {
754
+ trace('ProcessRunner.streams', () => `stdout access | ${JSON.stringify({
755
+ hasChild: !!self.child,
756
+ hasStdout: !!(self.child && self.child.stdout),
757
+ started: self.started,
758
+ finished: self.finished,
759
+ hasPromise: !!self.promise,
760
+ command: self.spec?.command?.slice(0, 50)
761
+ }, null, 2)}`);
762
+
763
+ self._autoStartIfNeeded('streams.stdout access');
764
+
765
+ if (self.child && self.child.stdout) {
766
+ trace('ProcessRunner.streams', () => 'stdout: returning existing stream');
767
+ return self.child.stdout;
768
+ }
769
+ if (self.finished) {
770
+ trace('ProcessRunner.streams', () => 'stdout: process finished, returning null');
771
+ return null;
772
+ }
773
+
774
+ // For virtual commands, there's no child process
775
+ if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
776
+ trace('ProcessRunner.streams', () => 'stdout: virtual command, returning null');
777
+ return null;
778
+ }
779
+
780
+ if (!self.started) {
781
+ trace('ProcessRunner.streams', () => 'stdout: not started, starting and waiting for child');
782
+ self._startAsync();
783
+ return new Promise((resolve) => {
784
+ const checkForChild = () => {
785
+ if (self.child && self.child.stdout) {
786
+ resolve(self.child.stdout);
787
+ } else if (self.finished || self._virtualGenerator) {
788
+ resolve(null);
789
+ } else {
790
+ setImmediate(checkForChild);
791
+ }
792
+ };
793
+ setImmediate(checkForChild);
794
+ });
795
+ }
796
+
797
+ if (self.promise && !self.child) {
798
+ trace('ProcessRunner.streams', () => 'stdout: process starting, waiting for child');
799
+ return new Promise((resolve) => {
800
+ const checkForChild = () => {
801
+ if (self.child && self.child.stdout) {
802
+ resolve(self.child.stdout);
803
+ } else if (self.finished || self._virtualGenerator) {
804
+ resolve(null);
805
+ } else {
806
+ setImmediate(checkForChild);
807
+ }
808
+ };
809
+ setImmediate(checkForChild);
810
+ });
811
+ }
812
+
813
+ trace('ProcessRunner.streams', () => 'stdout: returning null (no conditions met)');
814
+ return null;
815
+ },
816
+ get stderr() {
817
+ trace('ProcessRunner.streams', () => `stderr access | ${JSON.stringify({
818
+ hasChild: !!self.child,
819
+ hasStderr: !!(self.child && self.child.stderr),
820
+ started: self.started,
821
+ finished: self.finished,
822
+ hasPromise: !!self.promise,
823
+ command: self.spec?.command?.slice(0, 50)
824
+ }, null, 2)}`);
825
+
826
+ self._autoStartIfNeeded('streams.stderr access');
827
+
828
+ if (self.child && self.child.stderr) {
829
+ trace('ProcessRunner.streams', () => 'stderr: returning existing stream');
830
+ return self.child.stderr;
831
+ }
832
+ if (self.finished) {
833
+ trace('ProcessRunner.streams', () => 'stderr: process finished, returning null');
834
+ return null;
835
+ }
836
+
837
+ // For virtual commands, there's no child process
838
+ if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
839
+ trace('ProcessRunner.streams', () => 'stderr: virtual command, returning null');
840
+ return null;
841
+ }
842
+
843
+ if (!self.started) {
844
+ trace('ProcessRunner.streams', () => 'stderr: not started, starting and waiting for child');
845
+ self._startAsync();
846
+ return new Promise((resolve) => {
847
+ const checkForChild = () => {
848
+ if (self.child && self.child.stderr) {
849
+ resolve(self.child.stderr);
850
+ } else if (self.finished || self._virtualGenerator) {
851
+ resolve(null);
852
+ } else {
853
+ setImmediate(checkForChild);
854
+ }
855
+ };
856
+ setImmediate(checkForChild);
857
+ });
858
+ }
859
+
860
+ if (self.promise && !self.child) {
861
+ trace('ProcessRunner.streams', () => 'stderr: process starting, waiting for child');
862
+ return new Promise((resolve) => {
863
+ const checkForChild = () => {
864
+ if (self.child && self.child.stderr) {
865
+ resolve(self.child.stderr);
866
+ } else if (self.finished || self._virtualGenerator) {
867
+ resolve(null);
868
+ } else {
869
+ setImmediate(checkForChild);
870
+ }
871
+ };
872
+ setImmediate(checkForChild);
873
+ });
874
+ }
875
+
876
+ trace('ProcessRunner.streams', () => 'stderr: returning null (no conditions met)');
877
+ return null;
878
+ }
879
+ };
880
+ }
881
+
882
+ get buffers() {
883
+ const self = this;
884
+ return {
885
+ get stdin() {
886
+ self._autoStartIfNeeded('buffers.stdin access');
887
+ if (self.finished && self.result) {
888
+ return Buffer.from(self.result.stdin || '', 'utf8');
889
+ }
890
+ // Return promise if not finished
891
+ return self.then ? self.then(result => Buffer.from(result.stdin || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
892
+ },
893
+ get stdout() {
894
+ self._autoStartIfNeeded('buffers.stdout access');
895
+ if (self.finished && self.result) {
896
+ return Buffer.from(self.result.stdout || '', 'utf8');
897
+ }
898
+ // Return promise if not finished
899
+ return self.then ? self.then(result => Buffer.from(result.stdout || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
900
+ },
901
+ get stderr() {
902
+ self._autoStartIfNeeded('buffers.stderr access');
903
+ if (self.finished && self.result) {
904
+ return Buffer.from(self.result.stderr || '', 'utf8');
905
+ }
906
+ // Return promise if not finished
907
+ return self.then ? self.then(result => Buffer.from(result.stderr || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
908
+ }
909
+ };
910
+ }
911
+
912
+ get strings() {
913
+ const self = this;
914
+ return {
915
+ get stdin() {
916
+ self._autoStartIfNeeded('strings.stdin access');
917
+ if (self.finished && self.result) {
918
+ return self.result.stdin || '';
919
+ }
920
+ // Return promise if not finished
921
+ return self.then ? self.then(result => result.stdin || '') : Promise.resolve('');
922
+ },
923
+ get stdout() {
924
+ self._autoStartIfNeeded('strings.stdout access');
925
+ if (self.finished && self.result) {
926
+ return self.result.stdout || '';
927
+ }
928
+ // Return promise if not finished
929
+ return self.then ? self.then(result => result.stdout || '') : Promise.resolve('');
930
+ },
931
+ get stderr() {
932
+ self._autoStartIfNeeded('strings.stderr access');
933
+ if (self.finished && self.result) {
934
+ return self.result.stderr || '';
935
+ }
936
+ // Return promise if not finished
937
+ return self.then ? self.then(result => result.stderr || '') : Promise.resolve('');
938
+ }
939
+ };
940
+ }
941
+
942
+
658
943
  // Centralized method to properly finish a process with correct event emission order
659
944
  finish(result) {
660
945
  trace('ProcessRunner', () => `finish() called | ${JSON.stringify({
@@ -814,14 +1099,15 @@ class ProcessRunner extends StreamEmitter {
814
1099
  writer.close().catch(() => { }); // Ignore close errors
815
1100
  }
816
1101
 
817
- setTimeout(() => {
1102
+ // Use setImmediate for deferred termination instead of setTimeout
1103
+ setImmediate(() => {
818
1104
  if (this.child && !this.finished) {
819
1105
  trace('ProcessRunner', () => 'Terminating child process after parent stream closure');
820
1106
  if (typeof this.child.kill === 'function') {
821
1107
  this.child.kill('SIGTERM');
822
1108
  }
823
1109
  }
824
- }, 100);
1110
+ });
825
1111
 
826
1112
  } catch (error) {
827
1113
  trace('ProcessRunner', () => `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`);
@@ -939,7 +1225,14 @@ class ProcessRunner extends StreamEmitter {
939
1225
  start(options = {}) {
940
1226
  const mode = options.mode || 'async';
941
1227
 
942
- trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({ mode, options, started: this.started }, null, 2)}`);
1228
+ trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({
1229
+ mode,
1230
+ options,
1231
+ started: this.started,
1232
+ hasPromise: !!this.promise,
1233
+ hasChild: !!this.child,
1234
+ command: this.spec?.command?.slice(0, 50)
1235
+ }, null, 2)}`);
943
1236
 
944
1237
  // Merge new options with existing options before starting
945
1238
  if (Object.keys(options).length > 0 && !this.started) {
@@ -1115,16 +1408,17 @@ class ProcessRunner extends StreamEmitter {
1115
1408
  commandCount: parsed.commands?.length
1116
1409
  }, null, 2)}`);
1117
1410
  return await this._runPipeline(parsed.commands);
1118
- } else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
1411
+ } else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd) && !this.options._bypassVirtual) {
1119
1412
  // For built-in virtual commands that have real counterparts (like sleep),
1120
1413
  // skip the virtual version when custom stdin is provided to ensure proper process handling
1121
1414
  const hasCustomStdin = this.options.stdin &&
1122
1415
  this.options.stdin !== 'inherit' &&
1123
1416
  this.options.stdin !== 'ignore';
1124
1417
 
1125
- // List of built-in virtual commands that should fallback to real commands with custom stdin
1126
- const builtinCommands = ['sleep', 'echo', 'pwd', 'true', 'false', 'yes', 'cat', 'ls', 'which'];
1127
- const shouldBypassVirtual = hasCustomStdin && builtinCommands.includes(parsed.cmd);
1418
+ // Only bypass for commands that truly need real process behavior with custom stdin
1419
+ // Most commands like 'echo' work fine with virtual implementations even with stdin
1420
+ const commandsThatNeedRealStdin = ['sleep', 'cat']; // Only these really need real processes for stdin
1421
+ const shouldBypassVirtual = hasCustomStdin && commandsThatNeedRealStdin.includes(parsed.cmd);
1128
1422
 
1129
1423
  if (shouldBypassVirtual) {
1130
1424
  trace('ProcessRunner', () => `Bypassing built-in virtual command due to custom stdin | ${JSON.stringify({
@@ -1380,6 +1674,10 @@ class ProcessRunner extends StreamEmitter {
1380
1674
  this.child.stdin.end();
1381
1675
  trace('ProcessRunner', () => `stdin: Child stdin closed successfully`);
1382
1676
  }
1677
+ } else if (stdin === 'pipe') {
1678
+ trace('ProcessRunner', () => `stdin: Using pipe mode - leaving stdin open for manual control`);
1679
+ // Leave stdin open for manual writing via streams.stdin
1680
+ stdinPumpPromise = Promise.resolve();
1383
1681
  } else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
1384
1682
  const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
1385
1683
  trace('ProcessRunner', () => `stdin: Writing buffer to child | ${JSON.stringify({
@@ -1675,7 +1973,23 @@ class ProcessRunner extends StreamEmitter {
1675
1973
  try {
1676
1974
  // Prepare stdin
1677
1975
  let stdinData = '';
1678
- if (this.options.stdin && typeof this.options.stdin === 'string') {
1976
+
1977
+ // Special handling for streaming mode (stdin: "pipe")
1978
+ if (this.options.stdin === 'pipe') {
1979
+ // For streaming interfaces, virtual commands should fallback to real commands
1980
+ // because virtual commands don't support true streaming
1981
+ trace('ProcessRunner', () => `Virtual command fallback for streaming | ${JSON.stringify({ cmd }, null, 2)}`);
1982
+
1983
+ // Create a new ProcessRunner for the real command with properly merged options
1984
+ // Preserve main options but use appropriate stdin for the real command
1985
+ const modifiedOptions = {
1986
+ ...this.options,
1987
+ stdin: 'pipe', // Keep pipe but ensure it doesn't trigger virtual command fallback
1988
+ _bypassVirtual: true // Flag to prevent virtual command recursion
1989
+ };
1990
+ const realRunner = new ProcessRunner({ mode: 'shell', command: originalCommand || cmd }, modifiedOptions);
1991
+ return await realRunner._doStartAsync();
1992
+ } else if (this.options.stdin && typeof this.options.stdin === 'string') {
1679
1993
  stdinData = this.options.stdin;
1680
1994
  } else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
1681
1995
  stdinData = this.options.stdin.toString('utf8');
@@ -1698,22 +2012,28 @@ class ProcessRunner extends StreamEmitter {
1698
2012
  const chunks = [];
1699
2013
 
1700
2014
  const commandOptions = {
1701
- ...this.options,
1702
- isCancelled: () => this._cancelled,
1703
- signal: this._abortController?.signal
2015
+ // Commonly used options at top level for convenience
2016
+ cwd: this.options.cwd,
2017
+ env: this.options.env,
2018
+ // All original options (built-in + custom) in options object
2019
+ options: this.options,
2020
+ isCancelled: () => this._cancelled
1704
2021
  };
1705
2022
 
1706
2023
  trace('ProcessRunner', () => `_runVirtual signal details | ${JSON.stringify({
1707
2024
  cmd,
1708
2025
  hasAbortController: !!this._abortController,
1709
2026
  signalAborted: this._abortController?.signal?.aborted,
1710
- signalExists: !!commandOptions.signal,
1711
- commandOptionsSignalAborted: commandOptions.signal?.aborted,
1712
2027
  optionsSignalExists: !!this.options.signal,
1713
2028
  optionsSignalAborted: this.options.signal?.aborted
1714
2029
  }, null, 2)}`);
1715
2030
 
1716
- const generator = handler({ args: argValues, stdin: stdinData, ...commandOptions });
2031
+ const generator = handler({
2032
+ args: argValues,
2033
+ stdin: stdinData,
2034
+ abortSignal: this._abortController?.signal,
2035
+ ...commandOptions
2036
+ });
1717
2037
  this._virtualGenerator = generator;
1718
2038
 
1719
2039
  const cancelPromise = new Promise(resolve => {
@@ -1802,22 +2122,28 @@ class ProcessRunner extends StreamEmitter {
1802
2122
  } else {
1803
2123
  // Regular async function - race with abort signal
1804
2124
  const commandOptions = {
1805
- ...this.options,
1806
- isCancelled: () => this._cancelled,
1807
- signal: this._abortController?.signal
2125
+ // Commonly used options at top level for convenience
2126
+ cwd: this.options.cwd,
2127
+ env: this.options.env,
2128
+ // All original options (built-in + custom) in options object
2129
+ options: this.options,
2130
+ isCancelled: () => this._cancelled
1808
2131
  };
1809
2132
 
1810
2133
  trace('ProcessRunner', () => `_runVirtual signal details (non-generator) | ${JSON.stringify({
1811
2134
  cmd,
1812
2135
  hasAbortController: !!this._abortController,
1813
2136
  signalAborted: this._abortController?.signal?.aborted,
1814
- signalExists: !!commandOptions.signal,
1815
- commandOptionsSignalAborted: commandOptions.signal?.aborted,
1816
2137
  optionsSignalExists: !!this.options.signal,
1817
2138
  optionsSignalAborted: this.options.signal?.aborted
1818
2139
  }, null, 2)}`);
1819
2140
 
1820
- const handlerPromise = handler({ args: argValues, stdin: stdinData, ...commandOptions });
2141
+ const handlerPromise = handler({
2142
+ args: argValues,
2143
+ stdin: stdinData,
2144
+ abortSignal: this._abortController?.signal,
2145
+ ...commandOptions
2146
+ });
1821
2147
 
1822
2148
  // Create an abort promise that rejects when cancelled
1823
2149
  const abortPromise = new Promise((_, reject) => {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import { trace, VirtualUtils } from '../$.utils.mjs';
3
3
 
4
- export default async function cat({ args, stdin, cwd, isCancelled, signal }) {
4
+ export default async function cat({ args, stdin, cwd, isCancelled, abortSignal }) {
5
5
  if (args.length === 0) {
6
6
  // Read from stdin if no files specified
7
7
  if (stdin !== undefined && stdin !== '') {
@@ -14,7 +14,7 @@ export default async function cat({ args, stdin, cwd, isCancelled, signal }) {
14
14
  const outputs = [];
15
15
  for (const file of args) {
16
16
  // Check for cancellation before processing each file
17
- if (isCancelled?.() || signal?.aborted) {
17
+ if (isCancelled?.() || abortSignal?.aborted) {
18
18
  trace('VirtualCommand', () => `cat: cancelled while processing files`);
19
19
  return { code: 130, stdout: '', stderr: '' }; // SIGINT exit code
20
20
  }
@@ -1,11 +1,11 @@
1
1
  import { trace } from '../$.utils.mjs';
2
2
 
3
- export default async function sleep({ args, signal, isCancelled }) {
3
+ export default async function sleep({ args, abortSignal, isCancelled }) {
4
4
  const seconds = parseFloat(args[0] || 0);
5
5
  trace('VirtualCommand', () => `sleep: starting | ${JSON.stringify({
6
6
  seconds,
7
- hasSignal: !!signal,
8
- signalAborted: signal?.aborted,
7
+ hasSignal: !!abortSignal,
8
+ signalAborted: abortSignal?.aborted,
9
9
  hasIsCancelled: !!isCancelled
10
10
  }, null, 2)}`);
11
11
 
@@ -18,30 +18,30 @@ export default async function sleep({ args, signal, isCancelled }) {
18
18
  await new Promise((resolve, reject) => {
19
19
  const timeoutId = setTimeout(resolve, seconds * 1000);
20
20
 
21
- // Handle cancellation via signal
22
- if (signal) {
21
+ // Handle cancellation via abort signal
22
+ if (abortSignal) {
23
23
  trace('VirtualCommand', () => `sleep: setting up abort signal listener | ${JSON.stringify({
24
- signalAborted: signal.aborted
24
+ signalAborted: abortSignal.aborted
25
25
  }, null, 2)}`);
26
26
 
27
- signal.addEventListener('abort', () => {
27
+ abortSignal.addEventListener('abort', () => {
28
28
  trace('VirtualCommand', () => `sleep: abort signal received | ${JSON.stringify({
29
29
  seconds,
30
- signalAborted: signal.aborted
30
+ signalAborted: abortSignal.aborted
31
31
  }, null, 2)}`);
32
32
  clearTimeout(timeoutId);
33
33
  reject(new Error('Sleep cancelled'));
34
34
  });
35
35
 
36
36
  // Check if already aborted
37
- if (signal.aborted) {
37
+ if (abortSignal.aborted) {
38
38
  trace('VirtualCommand', () => `sleep: signal already aborted | ${JSON.stringify({ seconds }, null, 2)}`);
39
39
  clearTimeout(timeoutId);
40
40
  reject(new Error('Sleep cancelled'));
41
41
  return;
42
42
  }
43
43
  } else {
44
- trace('VirtualCommand', () => `sleep: no signal provided | ${JSON.stringify({ seconds }, null, 2)}`);
44
+ trace('VirtualCommand', () => `sleep: no abort signal provided | ${JSON.stringify({ seconds }, null, 2)}`);
45
45
  }
46
46
 
47
47
  // Also check isCancelled periodically for quicker response
@@ -1,11 +1,11 @@
1
1
  import { trace } from '../$.utils.mjs';
2
2
 
3
- export default async function* yes({ args, stdin, isCancelled, signal, ...rest }) {
3
+ export default async function* yes({ args, stdin, isCancelled, abortSignal, ...rest }) {
4
4
  const output = args.length > 0 ? args.join(' ') : 'y';
5
5
  trace('VirtualCommand', () => `yes: starting infinite generator | ${JSON.stringify({
6
6
  output,
7
7
  hasIsCancelled: !!isCancelled,
8
- hasSignal: !!signal
8
+ hasAbortSignal: !!abortSignal
9
9
  }, null, 2)}`);
10
10
 
11
11
  let iteration = 0;
@@ -14,12 +14,12 @@ export default async function* yes({ args, stdin, isCancelled, signal, ...rest }
14
14
  while (!isCancelled?.() && iteration < MAX_ITERATIONS) {
15
15
  trace('VirtualCommand', () => `yes: iteration ${iteration} starting | ${JSON.stringify({
16
16
  isCancelled: isCancelled?.(),
17
- signalAborted: signal?.aborted
17
+ abortSignalAborted: abortSignal?.aborted
18
18
  }, null, 2)}`);
19
19
 
20
20
  // Check for abort signal
21
- if (signal?.aborted) {
22
- trace('VirtualCommand', () => `yes: aborted via signal | ${JSON.stringify({ iteration }, null, 2)}`);
21
+ if (abortSignal?.aborted) {
22
+ trace('VirtualCommand', () => `yes: aborted via abort signal | ${JSON.stringify({ iteration }, null, 2)}`);
23
23
  break;
24
24
  }
25
25
 
@@ -43,6 +43,6 @@ export default async function* yes({ args, stdin, isCancelled, signal, ...rest }
43
43
  trace('VirtualCommand', () => `yes: generator completed | ${JSON.stringify({
44
44
  iteration,
45
45
  wasCancelled: isCancelled?.(),
46
- wasAborted: signal?.aborted
46
+ wasAborted: abortSignal?.aborted
47
47
  }, null, 2)}`);
48
48
  }