command-stream 0.4.0 → 0.6.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 +221 -56
- package/package.json +1 -1
- package/src/$.mjs +381 -49
- 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** | ✅
|
|
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** | ✅ Smart 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** | ✅ **
|
|
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** | ✅ **518+ tests, 1165+ 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
|
|
|
@@ -71,7 +81,7 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
|
|
|
71
81
|
- **🐚 Shell Replacement**: Dynamic error handling with `set -e`/`set +e` equivalents for .sh file replacement
|
|
72
82
|
- **⚡ Bun Optimized**: Designed for Bun with Node.js fallback compatibility
|
|
73
83
|
- **💾 Memory Efficient**: Streaming prevents large buffer accumulation
|
|
74
|
-
- **🛡️ Production Ready**: **
|
|
84
|
+
- **🛡️ Production Ready**: **518+ tests, 1165+ assertions** with comprehensive coverage including CI reliability
|
|
75
85
|
- **🎯 Advanced Signal Handling**: Robust SIGINT/SIGTERM forwarding with proper child process cleanup
|
|
76
86
|
- **🔍 Debug-Friendly**: Comprehensive VERBOSE tracing for CI debugging and troubleshooting
|
|
77
87
|
|
|
@@ -141,6 +151,54 @@ npm install command-stream
|
|
|
141
151
|
bun add command-stream
|
|
142
152
|
```
|
|
143
153
|
|
|
154
|
+
## Smart Quoting & Security
|
|
155
|
+
|
|
156
|
+
Command-stream provides intelligent auto-quoting to protect against shell injection while avoiding unnecessary quotes for safe strings:
|
|
157
|
+
|
|
158
|
+
### Smart Quoting Behavior
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
import { $ } from 'command-stream';
|
|
162
|
+
|
|
163
|
+
// Safe strings are NOT quoted (performance optimization)
|
|
164
|
+
await $`echo ${name}`; // name = "hello" → echo hello
|
|
165
|
+
await $`${cmd} --version`; // cmd = "/usr/bin/node" → /usr/bin/node --version
|
|
166
|
+
|
|
167
|
+
// Dangerous strings are automatically quoted for safety
|
|
168
|
+
await $`echo ${userInput}`; // userInput = "test; rm -rf /" → echo 'test; rm -rf /'
|
|
169
|
+
await $`echo ${pathWithSpaces}`; // pathWithSpaces = "/my path/file" → echo '/my path/file'
|
|
170
|
+
|
|
171
|
+
// Special characters that trigger auto-quoting:
|
|
172
|
+
// Spaces, $, ;, |, &, >, <, `, *, ?, [, ], {, }, (, ), !, #, and others
|
|
173
|
+
|
|
174
|
+
// User-provided quotes are preserved
|
|
175
|
+
const quotedPath = "'/path with spaces/file'";
|
|
176
|
+
await $`cat ${quotedPath}`; // → cat '/path with spaces/file' (no double-quoting!)
|
|
177
|
+
|
|
178
|
+
const doubleQuoted = '"/path with spaces/file"';
|
|
179
|
+
await $`cat ${doubleQuoted}`; // → cat '"/path with spaces/file"' (preserves intent)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Shell Injection Protection
|
|
183
|
+
|
|
184
|
+
All interpolated values are automatically secured:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// ✅ SAFE - All these injection attempts are neutralized
|
|
188
|
+
const dangerous = "'; rm -rf /; echo '";
|
|
189
|
+
await $`echo ${dangerous}`; // → echo ''\'' rm -rf /; echo '\'''
|
|
190
|
+
|
|
191
|
+
const cmdSubstitution = "$(whoami)";
|
|
192
|
+
await $`echo ${cmdSubstitution}`; // → echo '$(whoami)' (literal text, not executed)
|
|
193
|
+
|
|
194
|
+
const varExpansion = "$HOME";
|
|
195
|
+
await $`echo ${varExpansion}`; // → echo '$HOME' (literal text, not expanded)
|
|
196
|
+
|
|
197
|
+
// ✅ SAFE - Even complex injection attempts
|
|
198
|
+
const complex = "`cat /etc/passwd`";
|
|
199
|
+
await $`echo ${complex}`; // → echo '`cat /etc/passwd`' (literal text)
|
|
200
|
+
```
|
|
201
|
+
|
|
144
202
|
## Usage Patterns
|
|
145
203
|
|
|
146
204
|
### Classic Await (Backward Compatible)
|
|
@@ -174,6 +232,11 @@ await $withEnv`printenv MY_VAR`; // Prints: value
|
|
|
174
232
|
const $inTmp = $({ cwd: '/tmp' });
|
|
175
233
|
await $inTmp`pwd`; // Prints: /tmp
|
|
176
234
|
|
|
235
|
+
// Interactive mode for TTY commands (requires TTY environment)
|
|
236
|
+
const $interactive = $({ interactive: true });
|
|
237
|
+
await $interactive`vim myfile.txt`; // Full TTY access for editor
|
|
238
|
+
await $interactive`less README.md`; // Proper pager interaction
|
|
239
|
+
|
|
177
240
|
// Combine multiple options
|
|
178
241
|
const $custom = $({
|
|
179
242
|
stdin: 'test data',
|
|
@@ -291,6 +354,83 @@ syncCmd.on('end', result => {
|
|
|
291
354
|
const syncResult = syncCmd.sync();
|
|
292
355
|
```
|
|
293
356
|
|
|
357
|
+
### Streaming Interfaces
|
|
358
|
+
|
|
359
|
+
Advanced streaming interfaces for fine-grained process control:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
import { $ } from 'command-stream';
|
|
363
|
+
|
|
364
|
+
// 🎯 STDIN CONTROL: Send data to interactive commands (real-time)
|
|
365
|
+
const grepCmd = $`grep "important"`;
|
|
366
|
+
const stdin = await grepCmd.streams.stdin; // Available immediately
|
|
367
|
+
|
|
368
|
+
stdin.write('ignore this line\n');
|
|
369
|
+
stdin.write('important message\n');
|
|
370
|
+
stdin.write('skip this too\n');
|
|
371
|
+
stdin.end();
|
|
372
|
+
|
|
373
|
+
const result = await grepCmd;
|
|
374
|
+
console.log(result.stdout); // "important message\n"
|
|
375
|
+
|
|
376
|
+
// 🔧 BINARY DATA: Access raw buffers (after command finishes)
|
|
377
|
+
const cmd = $`echo "Hello World"`;
|
|
378
|
+
const buffer = await cmd.buffers.stdout; // Complete snapshot
|
|
379
|
+
console.log(buffer.length); // 12
|
|
380
|
+
|
|
381
|
+
// 📝 TEXT DATA: Access as strings (after command finishes)
|
|
382
|
+
const textCmd = $`echo "Hello World"`;
|
|
383
|
+
const text = await textCmd.strings.stdout; // Complete snapshot
|
|
384
|
+
console.log(text.trim()); // "Hello World"
|
|
385
|
+
|
|
386
|
+
// ⚡ PROCESS CONTROL: Kill commands that ignore stdin
|
|
387
|
+
const pingCmd = $`ping google.com`;
|
|
388
|
+
|
|
389
|
+
// Some commands ignore stdin input
|
|
390
|
+
const pingStdin = await pingCmd.streams.stdin;
|
|
391
|
+
if (pingStdin) {
|
|
392
|
+
pingStdin.write('q\n'); // ping ignores this
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Use kill() for forceful termination
|
|
396
|
+
setTimeout(() => pingCmd.kill(), 2000);
|
|
397
|
+
const pingResult = await pingCmd;
|
|
398
|
+
console.log('Ping stopped with code:', pingResult.code); // 143 (SIGTERM)
|
|
399
|
+
|
|
400
|
+
// 🔄 MIXED STDOUT/STDERR: Handle both streams (complete snapshots)
|
|
401
|
+
const mixedCmd = $`sh -c 'echo "out" && echo "err" >&2'`;
|
|
402
|
+
const [stdout, stderr] = await Promise.all([
|
|
403
|
+
mixedCmd.strings.stdout, // Available after finish
|
|
404
|
+
mixedCmd.strings.stderr // Available after finish
|
|
405
|
+
]);
|
|
406
|
+
console.log('Out:', stdout.trim()); // "out"
|
|
407
|
+
console.log('Err:', stderr.trim()); // "err"
|
|
408
|
+
|
|
409
|
+
// 🏃♂️ AUTO-START: Streams auto-start processes when accessed
|
|
410
|
+
const cmd = $`echo "test"`;
|
|
411
|
+
console.log('Started?', cmd.started); // false
|
|
412
|
+
|
|
413
|
+
const output = await cmd.streams.stdout; // Auto-starts, immediate access
|
|
414
|
+
console.log('Started?', cmd.started); // true
|
|
415
|
+
|
|
416
|
+
// 🔙 BACKWARD COMPATIBLE: Traditional await still works
|
|
417
|
+
const traditional = await $`echo "still works"`;
|
|
418
|
+
console.log(traditional.stdout); // "still works\n"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Key Features:**
|
|
422
|
+
- `command.streams.stdin/stdout/stderr` - Direct access to Node.js streams
|
|
423
|
+
- `command.buffers.stdin/stdout/stderr` - Binary data as Buffer objects
|
|
424
|
+
- `command.strings.stdin/stdout/stderr` - Text data as strings
|
|
425
|
+
- `command.kill()` - Forceful process termination
|
|
426
|
+
- **Auto-start behavior:** Process starts only when accessing stream properties
|
|
427
|
+
- **Perfect for:** Interactive commands (grep, sort, bc), data processing, real-time control
|
|
428
|
+
- **Network commands (ping, wget) ignore stdin** → Use `kill()` method instead
|
|
429
|
+
|
|
430
|
+
**🚀 Streams vs Buffers/Strings:**
|
|
431
|
+
- **`streams.*`** - Available **immediately** when command starts, for real-time interaction
|
|
432
|
+
- **`buffers.*` & `strings.*`** - Complete **snapshots** available only **after** command finishes
|
|
433
|
+
|
|
294
434
|
### Shell Replacement (.sh → .mjs)
|
|
295
435
|
|
|
296
436
|
Replace bash scripts with JavaScript while keeping shell semantics:
|
|
@@ -367,7 +507,7 @@ Create custom commands that work seamlessly alongside built-ins:
|
|
|
367
507
|
import { $, register, unregister, listCommands } from 'command-stream';
|
|
368
508
|
|
|
369
509
|
// Register a custom command
|
|
370
|
-
register('greet', async (args, stdin) => {
|
|
510
|
+
register('greet', async ({ args, stdin }) => {
|
|
371
511
|
const name = args[0] || 'World';
|
|
372
512
|
return { stdout: `Hello, ${name}!\n`, code: 0 };
|
|
373
513
|
});
|
|
@@ -377,7 +517,7 @@ await $`greet Alice`; // → "Hello, Alice!"
|
|
|
377
517
|
await $`echo "Bob" | greet`; // → "Hello, Bob!"
|
|
378
518
|
|
|
379
519
|
// Streaming virtual commands with async generators
|
|
380
|
-
register('countdown', async function* (args) {
|
|
520
|
+
register('countdown', async function* ({ args }) {
|
|
381
521
|
const start = parseInt(args[0] || 5);
|
|
382
522
|
for (let i = start; i >= 0; i--) {
|
|
383
523
|
yield `${i}\n`;
|
|
@@ -414,7 +554,7 @@ await execa('node', ['script.js']); // execa: separate processes
|
|
|
414
554
|
await $`node script.js`; // zx: shell commands only
|
|
415
555
|
|
|
416
556
|
// ✅ command-stream: JavaScript functions AS shell commands
|
|
417
|
-
register('deploy', async (args) => {
|
|
557
|
+
register('deploy', async ({ args }) => {
|
|
418
558
|
const env = args[0] || 'staging';
|
|
419
559
|
await deployToEnvironment(env);
|
|
420
560
|
return { stdout: `Deployed to ${env}!\n`, code: 0 };
|
|
@@ -450,11 +590,11 @@ await $`seq 1 5 | cat > numbers.txt`;
|
|
|
450
590
|
await $`git log --oneline | head -n 5`;
|
|
451
591
|
|
|
452
592
|
// 🚀 UNIQUE: Virtual command piping
|
|
453
|
-
register('uppercase', async (args, stdin) => {
|
|
593
|
+
register('uppercase', async ({ args, stdin }) => {
|
|
454
594
|
return { stdout: stdin.toUpperCase(), code: 0 };
|
|
455
595
|
});
|
|
456
596
|
|
|
457
|
-
register('reverse', async (args, stdin) => {
|
|
597
|
+
register('reverse', async ({ args, stdin }) => {
|
|
458
598
|
return { stdout: stdin.split('').reverse().join(''), code: 0 };
|
|
459
599
|
});
|
|
460
600
|
|
|
@@ -482,12 +622,12 @@ import { $, register } from 'command-stream';
|
|
|
482
622
|
const result = await $`echo "hello"`.pipe($`echo "World: $(cat)"`);
|
|
483
623
|
|
|
484
624
|
// 🌟 Virtual command chaining
|
|
485
|
-
register('add-prefix', async (args, stdin) => {
|
|
625
|
+
register('add-prefix', async ({ args, stdin }) => {
|
|
486
626
|
const prefix = args[0] || 'PREFIX:';
|
|
487
627
|
return { stdout: `${prefix} ${stdin.trim()}\n`, code: 0 };
|
|
488
628
|
});
|
|
489
629
|
|
|
490
|
-
register('add-suffix', async (args, stdin) => {
|
|
630
|
+
register('add-suffix', async ({ args, stdin }) => {
|
|
491
631
|
const suffix = args[0] || 'SUFFIX';
|
|
492
632
|
return { stdout: `${stdin.trim()} ${suffix}\n`, code: 0 };
|
|
493
633
|
});
|
|
@@ -512,7 +652,7 @@ try {
|
|
|
512
652
|
}
|
|
513
653
|
|
|
514
654
|
// ✅ Complex data processing
|
|
515
|
-
register('json-parse', async (args, stdin) => {
|
|
655
|
+
register('json-parse', async ({ args, stdin }) => {
|
|
516
656
|
try {
|
|
517
657
|
const data = JSON.parse(stdin);
|
|
518
658
|
return { stdout: JSON.stringify(data, null, 2), code: 0 };
|
|
@@ -521,7 +661,7 @@ register('json-parse', async (args, stdin) => {
|
|
|
521
661
|
}
|
|
522
662
|
});
|
|
523
663
|
|
|
524
|
-
register('extract-field', async (args, stdin) => {
|
|
664
|
+
register('extract-field', async ({ args, stdin }) => {
|
|
525
665
|
const field = args[0];
|
|
526
666
|
try {
|
|
527
667
|
const data = JSON.parse(stdin);
|
|
@@ -695,9 +835,10 @@ The enhanced `$` function returns a `ProcessRunner` instance that extends `Event
|
|
|
695
835
|
|
|
696
836
|
```javascript
|
|
697
837
|
{
|
|
698
|
-
mirror: true,
|
|
699
|
-
capture: true,
|
|
700
|
-
stdin: 'inherit'
|
|
838
|
+
mirror: true, // Live output to terminal (stdout→stdout, stderr→stderr)
|
|
839
|
+
capture: true, // Capture output for programmatic access
|
|
840
|
+
stdin: 'inherit', // Inherit stdin from parent process
|
|
841
|
+
interactive: false // Explicitly request TTY forwarding for interactive commands
|
|
701
842
|
}
|
|
702
843
|
```
|
|
703
844
|
|
|
@@ -705,6 +846,7 @@ The enhanced `$` function returns a `ProcessRunner` instance that extends `Event
|
|
|
705
846
|
- `mirror: boolean` - Whether to pipe output to terminal in real-time
|
|
706
847
|
- `capture: boolean` - Whether to capture output in result object
|
|
707
848
|
- `stdin: 'inherit' | 'ignore' | string | Buffer` - How to handle stdin
|
|
849
|
+
- `interactive: boolean` - Enable TTY forwarding for interactive commands (requires `stdin: 'inherit'` and TTY environment)
|
|
708
850
|
- `cwd: string` - Working directory for command
|
|
709
851
|
- `env: object` - Environment variables
|
|
710
852
|
|
|
@@ -778,9 +920,9 @@ Control and extend the command system with custom JavaScript functions:
|
|
|
778
920
|
import { $, register } from 'command-stream';
|
|
779
921
|
|
|
780
922
|
// ✅ Cancellation support with AbortController
|
|
781
|
-
register('cancellable', async function* (args, stdin,
|
|
923
|
+
register('cancellable', async function* ({ args, stdin, abortSignal }) {
|
|
782
924
|
for (let i = 0; i < 10; i++) {
|
|
783
|
-
if (
|
|
925
|
+
if (abortSignal?.aborted) {
|
|
784
926
|
break; // Proper cancellation handling
|
|
785
927
|
}
|
|
786
928
|
yield `Count: ${i}\n`;
|
|
@@ -789,22 +931,28 @@ register('cancellable', async function* (args, stdin, options) {
|
|
|
789
931
|
});
|
|
790
932
|
|
|
791
933
|
// ✅ Access to all process options
|
|
792
|
-
|
|
934
|
+
// All original options (built-in + custom) are available in the 'options' object
|
|
935
|
+
// Common options like cwd, env are also available at top level for convenience
|
|
936
|
+
// Runtime additions: isCancelled function, abortSignal
|
|
937
|
+
register('debug-info', async ({ args, stdin, cwd, env, options, isCancelled }) => {
|
|
793
938
|
return {
|
|
794
939
|
stdout: JSON.stringify({
|
|
795
940
|
args,
|
|
796
|
-
cwd
|
|
797
|
-
env: Object.keys(
|
|
798
|
-
stdinLength: stdin
|
|
799
|
-
|
|
800
|
-
|
|
941
|
+
cwd, // Available at top level for convenience
|
|
942
|
+
env: Object.keys(env || {}), // Available at top level for convenience
|
|
943
|
+
stdinLength: stdin?.length || 0,
|
|
944
|
+
allOptions: options, // All original options (built-in + custom)
|
|
945
|
+
mirror: options.mirror, // Built-in option from options object
|
|
946
|
+
capture: options.capture, // Built-in option from options object
|
|
947
|
+
customOption: options.customOption || 'not provided', // Custom option
|
|
948
|
+
isCancelledAvailable: typeof isCancelled === 'function'
|
|
801
949
|
}, null, 2),
|
|
802
950
|
code: 0
|
|
803
951
|
};
|
|
804
952
|
});
|
|
805
953
|
|
|
806
954
|
// ✅ Error handling and non-zero exit codes
|
|
807
|
-
register('maybe-fail', async (args) => {
|
|
955
|
+
register('maybe-fail', async ({ args }) => {
|
|
808
956
|
if (Math.random() > 0.5) {
|
|
809
957
|
return {
|
|
810
958
|
stdout: 'Success!\n',
|
|
@@ -818,13 +966,27 @@ register('maybe-fail', async (args) => {
|
|
|
818
966
|
};
|
|
819
967
|
}
|
|
820
968
|
});
|
|
969
|
+
|
|
970
|
+
// ✅ Example: User options flow through to virtual commands
|
|
971
|
+
register('show-options', async ({ args, stdin, options, cwd }) => {
|
|
972
|
+
return {
|
|
973
|
+
stdout: `Custom: ${options.customValue || 'none'}, CWD: ${cwd || options.cwd || 'default'}\n`,
|
|
974
|
+
code: 0
|
|
975
|
+
};
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Usage example showing options passed to virtual command:
|
|
979
|
+
const result = await $({ customValue: 'hello world', cwd: '/tmp' })`show-options`;
|
|
980
|
+
console.log(result.stdout); // Output: Custom: hello world, CWD: /tmp
|
|
821
981
|
```
|
|
822
982
|
|
|
823
983
|
#### Handler Function Signature
|
|
824
984
|
|
|
825
985
|
```javascript
|
|
826
986
|
// Regular async function
|
|
827
|
-
async function handler(args, stdin, options) {
|
|
987
|
+
async function handler({ args, stdin, abortSignal, cwd, env, options, isCancelled }) {
|
|
988
|
+
// All original options available in 'options': options.mirror, options.capture, options.customValue, etc.
|
|
989
|
+
// Common options like cwd, env also available at top level for convenience
|
|
828
990
|
return {
|
|
829
991
|
code: 0, // Exit code (number)
|
|
830
992
|
stdout: "output", // Standard output (string)
|
|
@@ -833,10 +995,13 @@ async function handler(args, stdin, options) {
|
|
|
833
995
|
}
|
|
834
996
|
|
|
835
997
|
// Async generator for streaming
|
|
836
|
-
async function
|
|
998
|
+
async function streamingHandler({ args, stdin, abortSignal, cwd, env, options, isCancelled }) {
|
|
999
|
+
// Access both built-in and custom options from 'options' object
|
|
1000
|
+
if (options.customFlag) {
|
|
1001
|
+
yield "custom behavior\n";
|
|
1002
|
+
}
|
|
837
1003
|
yield "chunk1\n";
|
|
838
1004
|
yield "chunk2\n";
|
|
839
|
-
// Each yield sends a chunk in real-time
|
|
840
1005
|
}
|
|
841
1006
|
```
|
|
842
1007
|
|
|
@@ -973,7 +1138,7 @@ try {
|
|
|
973
1138
|
|
|
974
1139
|
- **🎯 Smart Detection**: Only forwards CTRL+C when child processes are active
|
|
975
1140
|
- **🛡️ Non-Interference**: Preserves user SIGINT handlers when no children running
|
|
976
|
-
- **⚡ Interactive Commands**:
|
|
1141
|
+
- **⚡ Interactive Commands**: Use `interactive: true` option for commands like `vim`, `less`, `top` to enable proper TTY forwarding and signal handling
|
|
977
1142
|
- **🔄 Process Groups**: Detached spawning ensures proper signal isolation
|
|
978
1143
|
- **🧹 TTY Cleanup**: Raw terminal mode properly restored on interruption
|
|
979
1144
|
- **📊 Standard Exit Codes**:
|
|
@@ -1066,7 +1231,7 @@ $`npm install`
|
|
|
1066
1231
|
## Testing
|
|
1067
1232
|
|
|
1068
1233
|
```bash
|
|
1069
|
-
# Run comprehensive test suite (
|
|
1234
|
+
# Run comprehensive test suite (518+ tests)
|
|
1070
1235
|
bun test
|
|
1071
1236
|
|
|
1072
1237
|
# Run tests with coverage report
|
|
@@ -1117,7 +1282,7 @@ bun test # Run the full test suite
|
|
|
1117
1282
|
|
|
1118
1283
|
### 🧪 **Running Tests**
|
|
1119
1284
|
```bash
|
|
1120
|
-
bun test # All
|
|
1285
|
+
bun test # All 518+ tests
|
|
1121
1286
|
bun test tests/pipe.test.mjs # Specific test file
|
|
1122
1287
|
npm run test:builtin # Built-in commands only
|
|
1123
1288
|
```
|
package/package.json
CHANGED
package/src/$.mjs
CHANGED
|
@@ -12,13 +12,6 @@ const isBun = typeof globalThis.Bun !== 'undefined';
|
|
|
12
12
|
|
|
13
13
|
const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';
|
|
14
14
|
|
|
15
|
-
// Interactive commands that need TTY forwarding by default
|
|
16
|
-
const INTERACTIVE_COMMANDS = new Set([
|
|
17
|
-
'top', 'htop', 'btop', 'less', 'more', 'vi', 'vim', 'nano', 'emacs',
|
|
18
|
-
'man', 'pager', 'watch', 'tmux', 'screen', 'ssh', 'ftp', 'sftp',
|
|
19
|
-
'mysql', 'psql', 'redis-cli', 'mongo', 'sqlite3', 'irb', 'python',
|
|
20
|
-
'node', 'repl', 'gdb', 'lldb', 'bc', 'dc', 'ed'
|
|
21
|
-
]);
|
|
22
15
|
|
|
23
16
|
// Trace function for verbose logging
|
|
24
17
|
function trace(category, messageOrFunc) {
|
|
@@ -28,24 +21,6 @@ function trace(category, messageOrFunc) {
|
|
|
28
21
|
console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
// Check if a command is interactive and needs TTY forwarding
|
|
32
|
-
function isInteractiveCommand(command) {
|
|
33
|
-
if (!command || typeof command !== 'string') return false;
|
|
34
|
-
|
|
35
|
-
// Extract command and arguments from shell command string
|
|
36
|
-
const parts = command.trim().split(/\s+/);
|
|
37
|
-
const commandName = parts[0];
|
|
38
|
-
const baseName = path.basename(commandName);
|
|
39
|
-
|
|
40
|
-
// Special handling for commands that are only interactive when run without arguments/scripts
|
|
41
|
-
if (baseName === 'node' || baseName === 'python' || baseName === 'python3') {
|
|
42
|
-
// These are only interactive when run without a script file
|
|
43
|
-
// If there are additional arguments (like a script file), they're not interactive
|
|
44
|
-
return parts.length === 1;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return INTERACTIVE_COMMANDS.has(baseName);
|
|
48
|
-
}
|
|
49
24
|
|
|
50
25
|
|
|
51
26
|
// Track parent stream state for graceful shutdown
|
|
@@ -513,6 +488,14 @@ class StreamEmitter {
|
|
|
513
488
|
return this;
|
|
514
489
|
}
|
|
515
490
|
|
|
491
|
+
once(event, listener) {
|
|
492
|
+
const onceWrapper = (...args) => {
|
|
493
|
+
this.off(event, onceWrapper);
|
|
494
|
+
listener(...args);
|
|
495
|
+
};
|
|
496
|
+
return this.on(event, onceWrapper);
|
|
497
|
+
}
|
|
498
|
+
|
|
516
499
|
emit(event, ...args) {
|
|
517
500
|
const eventListeners = this.listeners.get(event);
|
|
518
501
|
trace('StreamEmitter', () => `Emitting event | ${JSON.stringify({
|
|
@@ -521,7 +504,9 @@ class StreamEmitter {
|
|
|
521
504
|
listenerCount: eventListeners?.length || 0
|
|
522
505
|
})}`);
|
|
523
506
|
if (eventListeners) {
|
|
524
|
-
|
|
507
|
+
// Create a copy to avoid issues if listeners modify the array
|
|
508
|
+
const listenersToCall = [...eventListeners];
|
|
509
|
+
for (const listener of listenersToCall) {
|
|
525
510
|
listener(...args);
|
|
526
511
|
}
|
|
527
512
|
}
|
|
@@ -545,6 +530,36 @@ function quote(value) {
|
|
|
545
530
|
if (Array.isArray(value)) return value.map(quote).join(' ');
|
|
546
531
|
if (typeof value !== 'string') value = String(value);
|
|
547
532
|
if (value === '') return "''";
|
|
533
|
+
|
|
534
|
+
// If the value is already properly quoted and doesn't need further escaping,
|
|
535
|
+
// check if we can use it as-is or with simpler quoting
|
|
536
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
537
|
+
// If it's already single-quoted and doesn't contain unescaped single quotes in the middle,
|
|
538
|
+
// we can potentially use it as-is
|
|
539
|
+
const inner = value.slice(1, -1);
|
|
540
|
+
if (!inner.includes("'")) {
|
|
541
|
+
// The inner content has no single quotes, so the original quoting is fine
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length > 2) {
|
|
547
|
+
// If it's already double-quoted, wrap it in single quotes to preserve it
|
|
548
|
+
return `'${value}'`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Check if the string needs quoting at all
|
|
552
|
+
// Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus
|
|
553
|
+
// This regex matches strings that DON'T need quoting
|
|
554
|
+
const safePattern = /^[a-zA-Z0-9_\-./=,+@:]+$/;
|
|
555
|
+
|
|
556
|
+
if (safePattern.test(value)) {
|
|
557
|
+
// The string is safe and doesn't need quoting
|
|
558
|
+
return value;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Default behavior: wrap in single quotes and escape any internal single quotes
|
|
562
|
+
// This handles spaces, special shell characters, etc.
|
|
548
563
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
549
564
|
}
|
|
550
565
|
|
|
@@ -604,6 +619,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
604
619
|
stdin: 'inherit',
|
|
605
620
|
cwd: undefined,
|
|
606
621
|
env: undefined,
|
|
622
|
+
interactive: false, // Explicitly request TTY forwarding for interactive commands
|
|
607
623
|
...options
|
|
608
624
|
};
|
|
609
625
|
|
|
@@ -655,6 +671,281 @@ class ProcessRunner extends StreamEmitter {
|
|
|
655
671
|
return this.child ? this.child.stdin : null;
|
|
656
672
|
}
|
|
657
673
|
|
|
674
|
+
// Issue #33: New streaming interfaces
|
|
675
|
+
_autoStartIfNeeded(reason) {
|
|
676
|
+
if (!this.started && !this.finished) {
|
|
677
|
+
trace('ProcessRunner', () => `Auto-starting process due to ${reason}`);
|
|
678
|
+
this.start({ mode: 'async', stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
get streams() {
|
|
683
|
+
const self = this;
|
|
684
|
+
return {
|
|
685
|
+
get stdin() {
|
|
686
|
+
trace('ProcessRunner.streams', () => `stdin access | ${JSON.stringify({
|
|
687
|
+
hasChild: !!self.child,
|
|
688
|
+
hasStdin: !!(self.child && self.child.stdin),
|
|
689
|
+
started: self.started,
|
|
690
|
+
finished: self.finished,
|
|
691
|
+
hasPromise: !!self.promise,
|
|
692
|
+
command: self.spec?.command?.slice(0, 50)
|
|
693
|
+
}, null, 2)}`);
|
|
694
|
+
|
|
695
|
+
self._autoStartIfNeeded('streams.stdin access');
|
|
696
|
+
|
|
697
|
+
// Streams are available immediately after spawn, or null if not piped
|
|
698
|
+
// Return the stream directly if available, otherwise ensure process starts
|
|
699
|
+
if (self.child && self.child.stdin) {
|
|
700
|
+
trace('ProcessRunner.streams', () => 'stdin: returning existing stream');
|
|
701
|
+
return self.child.stdin;
|
|
702
|
+
}
|
|
703
|
+
if (self.finished) {
|
|
704
|
+
trace('ProcessRunner.streams', () => 'stdin: process finished, returning null');
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// For virtual commands, there's no child process
|
|
709
|
+
// Exception: virtual commands with stdin: "pipe" will fallback to real commands
|
|
710
|
+
const isVirtualCommand = self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]));
|
|
711
|
+
const willFallbackToReal = isVirtualCommand && self.options.stdin === 'pipe';
|
|
712
|
+
|
|
713
|
+
if (isVirtualCommand && !willFallbackToReal) {
|
|
714
|
+
trace('ProcessRunner.streams', () => 'stdin: virtual command, returning null');
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// If not started, start it and wait for child to be created (not for completion!)
|
|
719
|
+
if (!self.started) {
|
|
720
|
+
trace('ProcessRunner.streams', () => 'stdin: not started, starting and waiting for child');
|
|
721
|
+
// Start the process
|
|
722
|
+
self._startAsync();
|
|
723
|
+
// Wait for child to be created using async iteration
|
|
724
|
+
return new Promise((resolve) => {
|
|
725
|
+
const checkForChild = () => {
|
|
726
|
+
if (self.child && self.child.stdin) {
|
|
727
|
+
resolve(self.child.stdin);
|
|
728
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
729
|
+
resolve(null);
|
|
730
|
+
} else {
|
|
731
|
+
// Use setImmediate to check again in next event loop iteration
|
|
732
|
+
setImmediate(checkForChild);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
setImmediate(checkForChild);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Process is starting - wait for child to appear
|
|
740
|
+
if (self.promise && !self.child) {
|
|
741
|
+
trace('ProcessRunner.streams', () => 'stdin: process starting, waiting for child');
|
|
742
|
+
return new Promise((resolve) => {
|
|
743
|
+
const checkForChild = () => {
|
|
744
|
+
if (self.child && self.child.stdin) {
|
|
745
|
+
resolve(self.child.stdin);
|
|
746
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
747
|
+
resolve(null);
|
|
748
|
+
} else {
|
|
749
|
+
setImmediate(checkForChild);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
setImmediate(checkForChild);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
trace('ProcessRunner.streams', () => 'stdin: returning null (no conditions met)');
|
|
757
|
+
return null;
|
|
758
|
+
},
|
|
759
|
+
get stdout() {
|
|
760
|
+
trace('ProcessRunner.streams', () => `stdout access | ${JSON.stringify({
|
|
761
|
+
hasChild: !!self.child,
|
|
762
|
+
hasStdout: !!(self.child && self.child.stdout),
|
|
763
|
+
started: self.started,
|
|
764
|
+
finished: self.finished,
|
|
765
|
+
hasPromise: !!self.promise,
|
|
766
|
+
command: self.spec?.command?.slice(0, 50)
|
|
767
|
+
}, null, 2)}`);
|
|
768
|
+
|
|
769
|
+
self._autoStartIfNeeded('streams.stdout access');
|
|
770
|
+
|
|
771
|
+
if (self.child && self.child.stdout) {
|
|
772
|
+
trace('ProcessRunner.streams', () => 'stdout: returning existing stream');
|
|
773
|
+
return self.child.stdout;
|
|
774
|
+
}
|
|
775
|
+
if (self.finished) {
|
|
776
|
+
trace('ProcessRunner.streams', () => 'stdout: process finished, returning null');
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// For virtual commands, there's no child process
|
|
781
|
+
if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
|
|
782
|
+
trace('ProcessRunner.streams', () => 'stdout: virtual command, returning null');
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (!self.started) {
|
|
787
|
+
trace('ProcessRunner.streams', () => 'stdout: not started, starting and waiting for child');
|
|
788
|
+
self._startAsync();
|
|
789
|
+
return new Promise((resolve) => {
|
|
790
|
+
const checkForChild = () => {
|
|
791
|
+
if (self.child && self.child.stdout) {
|
|
792
|
+
resolve(self.child.stdout);
|
|
793
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
794
|
+
resolve(null);
|
|
795
|
+
} else {
|
|
796
|
+
setImmediate(checkForChild);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
setImmediate(checkForChild);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (self.promise && !self.child) {
|
|
804
|
+
trace('ProcessRunner.streams', () => 'stdout: process starting, waiting for child');
|
|
805
|
+
return new Promise((resolve) => {
|
|
806
|
+
const checkForChild = () => {
|
|
807
|
+
if (self.child && self.child.stdout) {
|
|
808
|
+
resolve(self.child.stdout);
|
|
809
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
810
|
+
resolve(null);
|
|
811
|
+
} else {
|
|
812
|
+
setImmediate(checkForChild);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
setImmediate(checkForChild);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
trace('ProcessRunner.streams', () => 'stdout: returning null (no conditions met)');
|
|
820
|
+
return null;
|
|
821
|
+
},
|
|
822
|
+
get stderr() {
|
|
823
|
+
trace('ProcessRunner.streams', () => `stderr access | ${JSON.stringify({
|
|
824
|
+
hasChild: !!self.child,
|
|
825
|
+
hasStderr: !!(self.child && self.child.stderr),
|
|
826
|
+
started: self.started,
|
|
827
|
+
finished: self.finished,
|
|
828
|
+
hasPromise: !!self.promise,
|
|
829
|
+
command: self.spec?.command?.slice(0, 50)
|
|
830
|
+
}, null, 2)}`);
|
|
831
|
+
|
|
832
|
+
self._autoStartIfNeeded('streams.stderr access');
|
|
833
|
+
|
|
834
|
+
if (self.child && self.child.stderr) {
|
|
835
|
+
trace('ProcessRunner.streams', () => 'stderr: returning existing stream');
|
|
836
|
+
return self.child.stderr;
|
|
837
|
+
}
|
|
838
|
+
if (self.finished) {
|
|
839
|
+
trace('ProcessRunner.streams', () => 'stderr: process finished, returning null');
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// For virtual commands, there's no child process
|
|
844
|
+
if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
|
|
845
|
+
trace('ProcessRunner.streams', () => 'stderr: virtual command, returning null');
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (!self.started) {
|
|
850
|
+
trace('ProcessRunner.streams', () => 'stderr: not started, starting and waiting for child');
|
|
851
|
+
self._startAsync();
|
|
852
|
+
return new Promise((resolve) => {
|
|
853
|
+
const checkForChild = () => {
|
|
854
|
+
if (self.child && self.child.stderr) {
|
|
855
|
+
resolve(self.child.stderr);
|
|
856
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
857
|
+
resolve(null);
|
|
858
|
+
} else {
|
|
859
|
+
setImmediate(checkForChild);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
setImmediate(checkForChild);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (self.promise && !self.child) {
|
|
867
|
+
trace('ProcessRunner.streams', () => 'stderr: process starting, waiting for child');
|
|
868
|
+
return new Promise((resolve) => {
|
|
869
|
+
const checkForChild = () => {
|
|
870
|
+
if (self.child && self.child.stderr) {
|
|
871
|
+
resolve(self.child.stderr);
|
|
872
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
873
|
+
resolve(null);
|
|
874
|
+
} else {
|
|
875
|
+
setImmediate(checkForChild);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
setImmediate(checkForChild);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
trace('ProcessRunner.streams', () => 'stderr: returning null (no conditions met)');
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
get buffers() {
|
|
889
|
+
const self = this;
|
|
890
|
+
return {
|
|
891
|
+
get stdin() {
|
|
892
|
+
self._autoStartIfNeeded('buffers.stdin access');
|
|
893
|
+
if (self.finished && self.result) {
|
|
894
|
+
return Buffer.from(self.result.stdin || '', 'utf8');
|
|
895
|
+
}
|
|
896
|
+
// Return promise if not finished
|
|
897
|
+
return self.then ? self.then(result => Buffer.from(result.stdin || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
898
|
+
},
|
|
899
|
+
get stdout() {
|
|
900
|
+
self._autoStartIfNeeded('buffers.stdout access');
|
|
901
|
+
if (self.finished && self.result) {
|
|
902
|
+
return Buffer.from(self.result.stdout || '', 'utf8');
|
|
903
|
+
}
|
|
904
|
+
// Return promise if not finished
|
|
905
|
+
return self.then ? self.then(result => Buffer.from(result.stdout || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
906
|
+
},
|
|
907
|
+
get stderr() {
|
|
908
|
+
self._autoStartIfNeeded('buffers.stderr access');
|
|
909
|
+
if (self.finished && self.result) {
|
|
910
|
+
return Buffer.from(self.result.stderr || '', 'utf8');
|
|
911
|
+
}
|
|
912
|
+
// Return promise if not finished
|
|
913
|
+
return self.then ? self.then(result => Buffer.from(result.stderr || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
get strings() {
|
|
919
|
+
const self = this;
|
|
920
|
+
return {
|
|
921
|
+
get stdin() {
|
|
922
|
+
self._autoStartIfNeeded('strings.stdin access');
|
|
923
|
+
if (self.finished && self.result) {
|
|
924
|
+
return self.result.stdin || '';
|
|
925
|
+
}
|
|
926
|
+
// Return promise if not finished
|
|
927
|
+
return self.then ? self.then(result => result.stdin || '') : Promise.resolve('');
|
|
928
|
+
},
|
|
929
|
+
get stdout() {
|
|
930
|
+
self._autoStartIfNeeded('strings.stdout access');
|
|
931
|
+
if (self.finished && self.result) {
|
|
932
|
+
return self.result.stdout || '';
|
|
933
|
+
}
|
|
934
|
+
// Return promise if not finished
|
|
935
|
+
return self.then ? self.then(result => result.stdout || '') : Promise.resolve('');
|
|
936
|
+
},
|
|
937
|
+
get stderr() {
|
|
938
|
+
self._autoStartIfNeeded('strings.stderr access');
|
|
939
|
+
if (self.finished && self.result) {
|
|
940
|
+
return self.result.stderr || '';
|
|
941
|
+
}
|
|
942
|
+
// Return promise if not finished
|
|
943
|
+
return self.then ? self.then(result => result.stderr || '') : Promise.resolve('');
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
|
|
658
949
|
// Centralized method to properly finish a process with correct event emission order
|
|
659
950
|
finish(result) {
|
|
660
951
|
trace('ProcessRunner', () => `finish() called | ${JSON.stringify({
|
|
@@ -814,14 +1105,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
814
1105
|
writer.close().catch(() => { }); // Ignore close errors
|
|
815
1106
|
}
|
|
816
1107
|
|
|
817
|
-
|
|
1108
|
+
// Use setImmediate for deferred termination instead of setTimeout
|
|
1109
|
+
setImmediate(() => {
|
|
818
1110
|
if (this.child && !this.finished) {
|
|
819
1111
|
trace('ProcessRunner', () => 'Terminating child process after parent stream closure');
|
|
820
1112
|
if (typeof this.child.kill === 'function') {
|
|
821
1113
|
this.child.kill('SIGTERM');
|
|
822
1114
|
}
|
|
823
1115
|
}
|
|
824
|
-
}
|
|
1116
|
+
});
|
|
825
1117
|
|
|
826
1118
|
} catch (error) {
|
|
827
1119
|
trace('ProcessRunner', () => `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
@@ -939,7 +1231,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
939
1231
|
start(options = {}) {
|
|
940
1232
|
const mode = options.mode || 'async';
|
|
941
1233
|
|
|
942
|
-
trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({
|
|
1234
|
+
trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({
|
|
1235
|
+
mode,
|
|
1236
|
+
options,
|
|
1237
|
+
started: this.started,
|
|
1238
|
+
hasPromise: !!this.promise,
|
|
1239
|
+
hasChild: !!this.child,
|
|
1240
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1241
|
+
}, null, 2)}`);
|
|
943
1242
|
|
|
944
1243
|
// Merge new options with existing options before starting
|
|
945
1244
|
if (Object.keys(options).length > 0 && !this.started) {
|
|
@@ -1115,16 +1414,17 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1115
1414
|
commandCount: parsed.commands?.length
|
|
1116
1415
|
}, null, 2)}`);
|
|
1117
1416
|
return await this._runPipeline(parsed.commands);
|
|
1118
|
-
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
1417
|
+
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd) && !this.options._bypassVirtual) {
|
|
1119
1418
|
// For built-in virtual commands that have real counterparts (like sleep),
|
|
1120
1419
|
// skip the virtual version when custom stdin is provided to ensure proper process handling
|
|
1121
1420
|
const hasCustomStdin = this.options.stdin &&
|
|
1122
1421
|
this.options.stdin !== 'inherit' &&
|
|
1123
1422
|
this.options.stdin !== 'ignore';
|
|
1124
1423
|
|
|
1125
|
-
//
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1424
|
+
// Only bypass for commands that truly need real process behavior with custom stdin
|
|
1425
|
+
// Most commands like 'echo' work fine with virtual implementations even with stdin
|
|
1426
|
+
const commandsThatNeedRealStdin = ['sleep', 'cat']; // Only these really need real processes for stdin
|
|
1427
|
+
const shouldBypassVirtual = hasCustomStdin && commandsThatNeedRealStdin.includes(parsed.cmd);
|
|
1128
1428
|
|
|
1129
1429
|
if (shouldBypassVirtual) {
|
|
1130
1430
|
trace('ProcessRunner', () => `Bypassing built-in virtual command due to custom stdin | ${JSON.stringify({
|
|
@@ -1168,12 +1468,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1168
1468
|
}
|
|
1169
1469
|
|
|
1170
1470
|
// Detect if this is an interactive command that needs direct TTY access
|
|
1171
|
-
// Only activate for interactive commands when we have a real TTY and
|
|
1471
|
+
// Only activate for interactive commands when we have a real TTY and interactive mode is explicitly requested
|
|
1172
1472
|
const isInteractive = stdin === 'inherit' &&
|
|
1173
1473
|
process.stdin.isTTY === true &&
|
|
1174
1474
|
process.stdout.isTTY === true &&
|
|
1175
1475
|
process.stderr.isTTY === true &&
|
|
1176
|
-
|
|
1476
|
+
this.options.interactive === true;
|
|
1177
1477
|
|
|
1178
1478
|
trace('ProcessRunner', () => `Interactive command detection | ${JSON.stringify({
|
|
1179
1479
|
isInteractive,
|
|
@@ -1181,7 +1481,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1181
1481
|
stdinTTY: process.stdin.isTTY,
|
|
1182
1482
|
stdoutTTY: process.stdout.isTTY,
|
|
1183
1483
|
stderrTTY: process.stderr.isTTY,
|
|
1184
|
-
|
|
1484
|
+
interactiveOption: this.options.interactive
|
|
1185
1485
|
}, null, 2)}`);
|
|
1186
1486
|
|
|
1187
1487
|
const spawnBun = (argv) => {
|
|
@@ -1380,6 +1680,10 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1380
1680
|
this.child.stdin.end();
|
|
1381
1681
|
trace('ProcessRunner', () => `stdin: Child stdin closed successfully`);
|
|
1382
1682
|
}
|
|
1683
|
+
} else if (stdin === 'pipe') {
|
|
1684
|
+
trace('ProcessRunner', () => `stdin: Using pipe mode - leaving stdin open for manual control`);
|
|
1685
|
+
// Leave stdin open for manual writing via streams.stdin
|
|
1686
|
+
stdinPumpPromise = Promise.resolve();
|
|
1383
1687
|
} else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
|
|
1384
1688
|
const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
|
|
1385
1689
|
trace('ProcessRunner', () => `stdin: Writing buffer to child | ${JSON.stringify({
|
|
@@ -1675,7 +1979,23 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1675
1979
|
try {
|
|
1676
1980
|
// Prepare stdin
|
|
1677
1981
|
let stdinData = '';
|
|
1678
|
-
|
|
1982
|
+
|
|
1983
|
+
// Special handling for streaming mode (stdin: "pipe")
|
|
1984
|
+
if (this.options.stdin === 'pipe') {
|
|
1985
|
+
// For streaming interfaces, virtual commands should fallback to real commands
|
|
1986
|
+
// because virtual commands don't support true streaming
|
|
1987
|
+
trace('ProcessRunner', () => `Virtual command fallback for streaming | ${JSON.stringify({ cmd }, null, 2)}`);
|
|
1988
|
+
|
|
1989
|
+
// Create a new ProcessRunner for the real command with properly merged options
|
|
1990
|
+
// Preserve main options but use appropriate stdin for the real command
|
|
1991
|
+
const modifiedOptions = {
|
|
1992
|
+
...this.options,
|
|
1993
|
+
stdin: 'pipe', // Keep pipe but ensure it doesn't trigger virtual command fallback
|
|
1994
|
+
_bypassVirtual: true // Flag to prevent virtual command recursion
|
|
1995
|
+
};
|
|
1996
|
+
const realRunner = new ProcessRunner({ mode: 'shell', command: originalCommand || cmd }, modifiedOptions);
|
|
1997
|
+
return await realRunner._doStartAsync();
|
|
1998
|
+
} else if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
1679
1999
|
stdinData = this.options.stdin;
|
|
1680
2000
|
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
1681
2001
|
stdinData = this.options.stdin.toString('utf8');
|
|
@@ -1698,22 +2018,28 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1698
2018
|
const chunks = [];
|
|
1699
2019
|
|
|
1700
2020
|
const commandOptions = {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2021
|
+
// Commonly used options at top level for convenience
|
|
2022
|
+
cwd: this.options.cwd,
|
|
2023
|
+
env: this.options.env,
|
|
2024
|
+
// All original options (built-in + custom) in options object
|
|
2025
|
+
options: this.options,
|
|
2026
|
+
isCancelled: () => this._cancelled
|
|
1704
2027
|
};
|
|
1705
2028
|
|
|
1706
2029
|
trace('ProcessRunner', () => `_runVirtual signal details | ${JSON.stringify({
|
|
1707
2030
|
cmd,
|
|
1708
2031
|
hasAbortController: !!this._abortController,
|
|
1709
2032
|
signalAborted: this._abortController?.signal?.aborted,
|
|
1710
|
-
signalExists: !!commandOptions.signal,
|
|
1711
|
-
commandOptionsSignalAborted: commandOptions.signal?.aborted,
|
|
1712
2033
|
optionsSignalExists: !!this.options.signal,
|
|
1713
2034
|
optionsSignalAborted: this.options.signal?.aborted
|
|
1714
2035
|
}, null, 2)}`);
|
|
1715
2036
|
|
|
1716
|
-
const generator = handler({
|
|
2037
|
+
const generator = handler({
|
|
2038
|
+
args: argValues,
|
|
2039
|
+
stdin: stdinData,
|
|
2040
|
+
abortSignal: this._abortController?.signal,
|
|
2041
|
+
...commandOptions
|
|
2042
|
+
});
|
|
1717
2043
|
this._virtualGenerator = generator;
|
|
1718
2044
|
|
|
1719
2045
|
const cancelPromise = new Promise(resolve => {
|
|
@@ -1802,22 +2128,28 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1802
2128
|
} else {
|
|
1803
2129
|
// Regular async function - race with abort signal
|
|
1804
2130
|
const commandOptions = {
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2131
|
+
// Commonly used options at top level for convenience
|
|
2132
|
+
cwd: this.options.cwd,
|
|
2133
|
+
env: this.options.env,
|
|
2134
|
+
// All original options (built-in + custom) in options object
|
|
2135
|
+
options: this.options,
|
|
2136
|
+
isCancelled: () => this._cancelled
|
|
1808
2137
|
};
|
|
1809
2138
|
|
|
1810
2139
|
trace('ProcessRunner', () => `_runVirtual signal details (non-generator) | ${JSON.stringify({
|
|
1811
2140
|
cmd,
|
|
1812
2141
|
hasAbortController: !!this._abortController,
|
|
1813
2142
|
signalAborted: this._abortController?.signal?.aborted,
|
|
1814
|
-
signalExists: !!commandOptions.signal,
|
|
1815
|
-
commandOptionsSignalAborted: commandOptions.signal?.aborted,
|
|
1816
2143
|
optionsSignalExists: !!this.options.signal,
|
|
1817
2144
|
optionsSignalAborted: this.options.signal?.aborted
|
|
1818
2145
|
}, null, 2)}`);
|
|
1819
2146
|
|
|
1820
|
-
const handlerPromise = handler({
|
|
2147
|
+
const handlerPromise = handler({
|
|
2148
|
+
args: argValues,
|
|
2149
|
+
stdin: stdinData,
|
|
2150
|
+
abortSignal: this._abortController?.signal,
|
|
2151
|
+
...commandOptions
|
|
2152
|
+
});
|
|
1821
2153
|
|
|
1822
2154
|
// Create an abort promise that rejects when cancelled
|
|
1823
2155
|
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
|
}
|