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 +159 -49
- package/package.json +1 -1
- package/src/$.mjs +347 -21
- package/src/commands/$.cat.mjs +2 -2
- package/src/commands/$.sleep.mjs +10 -10
- package/src/commands/$.yes.mjs +6 -6
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[](https://npmjs.com/command-stream)
|
|
2
2
|
[](https://github.com/link-foundation/command-stream/blob/main/LICENSE)
|
|
3
|
+
[](https://github.com/link-foundation/command-stream/stargazers)
|
|
3
4
|
|
|
4
5
|
[](https://gitpod.io/#https://github.com/link-foundation/command-stream)
|
|
5
6
|
[](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) | [
|
|
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
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
32
|
+
| **📦 NPM Package** | [](https://www.npmjs.com/package/command-stream) | [](https://www.npmjs.com/package/execa) | [](https://www.npmjs.com/package/cross-spawn) | N/A (Built-in) | [](https://www.npmjs.com/package/shelljs) | [](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', ...)` |
|
|
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 |
|
|
39
|
-
| **Shell Injection Protection** | ✅ Auto-quoting | ✅
|
|
40
|
-
| **Cross-platform** | ✅ macOS/Linux/Windows | ✅ Yes | ✅
|
|
41
|
-
| **Performance** | ⚡ Fast (Bun optimized) |
|
|
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 |
|
|
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 | ✅
|
|
46
|
-
| **Stderr Support** | ✅ Real-time streaming + events | ✅ Redirection + `.quiet()` access | ✅
|
|
47
|
-
| **Stdin Support** | ✅ string/Buffer/inherit/ignore | ✅
|
|
48
|
-
| **Built-in Commands** | ✅ **18 commands**: cat, ls, mkdir, rm, mv, cp, touch, basename, dirname, seq, yes + all Bun.$ commands |
|
|
49
|
-
| **Virtual Commands Engine** | ✅ **Revolutionary**: Register JavaScript functions as shell commands with full pipeline support | ❌ No
|
|
50
|
-
| **Pipeline/Piping Support** | ✅ **Advanced**: System + Built-ins + Virtual + Mixed + `.pipe()` method | ✅
|
|
51
|
-
| **Bundle Size** | 📦 **~20KB gzipped** |
|
|
52
|
-
| **Signal Handling** | ✅ **Advanced SIGINT/SIGTERM forwarding** with cleanup | 🟡 Basic |
|
|
53
|
-
| **Process Management** | ✅ **Robust child process lifecycle** with proper termination |
|
|
54
|
-
| **Debug Tracing** | ✅ **Comprehensive VERBOSE logging** for CI/debugging |
|
|
55
|
-
| **Test Coverage** | ✅ **410 tests, 909 assertions** |
|
|
56
|
-
| **CI Reliability** | ✅ **Platform-specific handling** (macOS/Ubuntu) |
|
|
57
|
-
| **Documentation** | ✅ **Comprehensive examples + guides** | ✅
|
|
58
|
-
| **TypeScript** | 🔄 Coming soon | ✅
|
|
59
|
-
| **License** | ✅ **Unlicense (Public Domain)** | 🟡 MIT
|
|
60
|
-
|
|
61
|
-
**📊 Popularity
|
|
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,
|
|
868
|
+
register('cancellable', async function* ({ args, stdin, abortSignal }) {
|
|
782
869
|
for (let i = 0; i < 10; i++) {
|
|
783
|
-
if (
|
|
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
|
-
|
|
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
|
|
797
|
-
env: Object.keys(
|
|
798
|
-
stdinLength: stdin
|
|
799
|
-
|
|
800
|
-
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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({
|
|
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
|
-
//
|
|
1126
|
-
|
|
1127
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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({
|
|
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
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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({
|
|
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) => {
|
package/src/commands/$.cat.mjs
CHANGED
|
@@ -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,
|
|
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?.() ||
|
|
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
|
}
|
package/src/commands/$.sleep.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { trace } from '../$.utils.mjs';
|
|
2
2
|
|
|
3
|
-
export default async function sleep({ args,
|
|
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: !!
|
|
8
|
-
signalAborted:
|
|
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 (
|
|
21
|
+
// Handle cancellation via abort signal
|
|
22
|
+
if (abortSignal) {
|
|
23
23
|
trace('VirtualCommand', () => `sleep: setting up abort signal listener | ${JSON.stringify({
|
|
24
|
-
signalAborted:
|
|
24
|
+
signalAborted: abortSignal.aborted
|
|
25
25
|
}, null, 2)}`);
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
abortSignal.addEventListener('abort', () => {
|
|
28
28
|
trace('VirtualCommand', () => `sleep: abort signal received | ${JSON.stringify({
|
|
29
29
|
seconds,
|
|
30
|
-
signalAborted:
|
|
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 (
|
|
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
|
package/src/commands/$.yes.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { trace } from '../$.utils.mjs';
|
|
2
2
|
|
|
3
|
-
export default async function* yes({ args, stdin, isCancelled,
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
abortSignalAborted: abortSignal?.aborted
|
|
18
18
|
}, null, 2)}`);
|
|
19
19
|
|
|
20
20
|
// Check for abort signal
|
|
21
|
-
if (
|
|
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:
|
|
46
|
+
wasAborted: abortSignal?.aborted
|
|
47
47
|
}, null, 2)}`);
|
|
48
48
|
}
|