command-stream 0.5.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 +64 -9
- package/package.json +1 -1
- package/src/$.mjs +34 -28
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
|
|
|
41
41
|
| **EventEmitter Pattern** | β
`.on('data', ...)` | π‘ Limited events | π‘ Child process events | β No | β No | β No |
|
|
42
42
|
| **Mixed Patterns** | β
Events + await/sync | β No | β No | β No | β No | β No |
|
|
43
43
|
| **Bun.$ Compatibility** | β
`.text()` method support | β No | β No | β
Native API | β No | β No |
|
|
44
|
-
| **Shell Injection Protection** | β
|
|
44
|
+
| **Shell Injection Protection** | β
Smart auto-quoting | β
Safe by default | β
Safe by default | β
Built-in | π‘ Manual escaping | β
Safe by default |
|
|
45
45
|
| **Cross-platform** | β
macOS/Linux/Windows | β
Yes | β
**Specialized** cross-platform | β
Yes | β
Yes | β
Yes |
|
|
46
46
|
| **Performance** | β‘ Fast (Bun optimized) | π Moderate | β‘ Fast | β‘ Very fast | π Moderate | π Slow |
|
|
47
47
|
| **Memory Efficiency** | β
Streaming prevents buildup | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory |
|
|
@@ -57,7 +57,7 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
|
|
|
57
57
|
| **Signal Handling** | β
**Advanced SIGINT/SIGTERM forwarding** with cleanup | π‘ Basic | β
**Excellent** cross-platform | π‘ Basic | π‘ Basic | π‘ Basic |
|
|
58
58
|
| **Process Management** | β
**Robust child process lifecycle** with proper termination | β
Good | β
**Excellent** spawn wrapper | β Basic | π‘ Limited | π‘ Limited |
|
|
59
59
|
| **Debug Tracing** | β
**Comprehensive VERBOSE logging** for CI/debugging | π‘ Limited | β No | β No | π‘ Basic | β No |
|
|
60
|
-
| **Test Coverage** | β
**
|
|
60
|
+
| **Test Coverage** | β
**518+ tests, 1165+ assertions** | β
Excellent | β
Good | π‘ Good coverage | β
Good | π‘ Good |
|
|
61
61
|
| **CI Reliability** | β
**Platform-specific handling** (macOS/Ubuntu) | β
Good | β
**Excellent** | π‘ Basic | β
Good | π‘ Basic |
|
|
62
62
|
| **Documentation** | β
**Comprehensive examples + guides** | β
Excellent | π‘ Basic | β
Good | β
Good | π‘ Limited |
|
|
63
63
|
| **TypeScript** | π Coming soon | β
Full support | β
Built-in | β
Built-in | π‘ Community types | β
Full support |
|
|
@@ -81,7 +81,7 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
|
|
|
81
81
|
- **π Shell Replacement**: Dynamic error handling with `set -e`/`set +e` equivalents for .sh file replacement
|
|
82
82
|
- **β‘ Bun Optimized**: Designed for Bun with Node.js fallback compatibility
|
|
83
83
|
- **πΎ Memory Efficient**: Streaming prevents large buffer accumulation
|
|
84
|
-
- **π‘οΈ Production Ready**: **
|
|
84
|
+
- **π‘οΈ Production Ready**: **518+ tests, 1165+ assertions** with comprehensive coverage including CI reliability
|
|
85
85
|
- **π― Advanced Signal Handling**: Robust SIGINT/SIGTERM forwarding with proper child process cleanup
|
|
86
86
|
- **π Debug-Friendly**: Comprehensive VERBOSE tracing for CI debugging and troubleshooting
|
|
87
87
|
|
|
@@ -151,6 +151,54 @@ npm install command-stream
|
|
|
151
151
|
bun add command-stream
|
|
152
152
|
```
|
|
153
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
|
+
|
|
154
202
|
## Usage Patterns
|
|
155
203
|
|
|
156
204
|
### Classic Await (Backward Compatible)
|
|
@@ -184,6 +232,11 @@ await $withEnv`printenv MY_VAR`; // Prints: value
|
|
|
184
232
|
const $inTmp = $({ cwd: '/tmp' });
|
|
185
233
|
await $inTmp`pwd`; // Prints: /tmp
|
|
186
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
|
+
|
|
187
240
|
// Combine multiple options
|
|
188
241
|
const $custom = $({
|
|
189
242
|
stdin: 'test data',
|
|
@@ -782,9 +835,10 @@ The enhanced `$` function returns a `ProcessRunner` instance that extends `Event
|
|
|
782
835
|
|
|
783
836
|
```javascript
|
|
784
837
|
{
|
|
785
|
-
mirror: true,
|
|
786
|
-
capture: true,
|
|
787
|
-
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
|
|
788
842
|
}
|
|
789
843
|
```
|
|
790
844
|
|
|
@@ -792,6 +846,7 @@ The enhanced `$` function returns a `ProcessRunner` instance that extends `Event
|
|
|
792
846
|
- `mirror: boolean` - Whether to pipe output to terminal in real-time
|
|
793
847
|
- `capture: boolean` - Whether to capture output in result object
|
|
794
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)
|
|
795
850
|
- `cwd: string` - Working directory for command
|
|
796
851
|
- `env: object` - Environment variables
|
|
797
852
|
|
|
@@ -1083,7 +1138,7 @@ try {
|
|
|
1083
1138
|
|
|
1084
1139
|
- **π― Smart Detection**: Only forwards CTRL+C when child processes are active
|
|
1085
1140
|
- **π‘οΈ Non-Interference**: Preserves user SIGINT handlers when no children running
|
|
1086
|
-
- **β‘ Interactive Commands**:
|
|
1141
|
+
- **β‘ Interactive Commands**: Use `interactive: true` option for commands like `vim`, `less`, `top` to enable proper TTY forwarding and signal handling
|
|
1087
1142
|
- **π Process Groups**: Detached spawning ensures proper signal isolation
|
|
1088
1143
|
- **π§Ή TTY Cleanup**: Raw terminal mode properly restored on interruption
|
|
1089
1144
|
- **π Standard Exit Codes**:
|
|
@@ -1176,7 +1231,7 @@ $`npm install`
|
|
|
1176
1231
|
## Testing
|
|
1177
1232
|
|
|
1178
1233
|
```bash
|
|
1179
|
-
# Run comprehensive test suite (
|
|
1234
|
+
# Run comprehensive test suite (518+ tests)
|
|
1180
1235
|
bun test
|
|
1181
1236
|
|
|
1182
1237
|
# Run tests with coverage report
|
|
@@ -1227,7 +1282,7 @@ bun test # Run the full test suite
|
|
|
1227
1282
|
|
|
1228
1283
|
### π§ͺ **Running Tests**
|
|
1229
1284
|
```bash
|
|
1230
|
-
bun test # All
|
|
1285
|
+
bun test # All 518+ tests
|
|
1231
1286
|
bun test tests/pipe.test.mjs # Specific test file
|
|
1232
1287
|
npm run test:builtin # Built-in commands only
|
|
1233
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
|
|
@@ -555,6 +530,36 @@ function quote(value) {
|
|
|
555
530
|
if (Array.isArray(value)) return value.map(quote).join(' ');
|
|
556
531
|
if (typeof value !== 'string') value = String(value);
|
|
557
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.
|
|
558
563
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
559
564
|
}
|
|
560
565
|
|
|
@@ -614,6 +619,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
614
619
|
stdin: 'inherit',
|
|
615
620
|
cwd: undefined,
|
|
616
621
|
env: undefined,
|
|
622
|
+
interactive: false, // Explicitly request TTY forwarding for interactive commands
|
|
617
623
|
...options
|
|
618
624
|
};
|
|
619
625
|
|
|
@@ -1462,12 +1468,12 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1462
1468
|
}
|
|
1463
1469
|
|
|
1464
1470
|
// Detect if this is an interactive command that needs direct TTY access
|
|
1465
|
-
// 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
|
|
1466
1472
|
const isInteractive = stdin === 'inherit' &&
|
|
1467
1473
|
process.stdin.isTTY === true &&
|
|
1468
1474
|
process.stdout.isTTY === true &&
|
|
1469
1475
|
process.stderr.isTTY === true &&
|
|
1470
|
-
|
|
1476
|
+
this.options.interactive === true;
|
|
1471
1477
|
|
|
1472
1478
|
trace('ProcessRunner', () => `Interactive command detection | ${JSON.stringify({
|
|
1473
1479
|
isInteractive,
|
|
@@ -1475,7 +1481,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1475
1481
|
stdinTTY: process.stdin.isTTY,
|
|
1476
1482
|
stdoutTTY: process.stdout.isTTY,
|
|
1477
1483
|
stderrTTY: process.stderr.isTTY,
|
|
1478
|
-
|
|
1484
|
+
interactiveOption: this.options.interactive
|
|
1479
1485
|
}, null, 2)}`);
|
|
1480
1486
|
|
|
1481
1487
|
const spawnBun = (argv) => {
|