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.
Files changed (3) hide show
  1. package/README.md +64 -9
  2. package/package.json +1 -1
  3. 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** | βœ… Auto-quoting | βœ… Safe by default | βœ… Safe by default | βœ… Built-in | 🟑 Manual escaping | βœ… Safe by default |
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** | βœ… **410 tests, 909 assertions** | βœ… Excellent | βœ… Good | 🟑 Good coverage | βœ… Good | 🟑 Good |
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**: **410 tests, 909 assertions** with comprehensive coverage including CI reliability
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, // Live output to terminal (stdout→stdout, stderr→stderr)
786
- capture: true, // Capture output for programmatic access
787
- stdin: 'inherit' // Inherit stdin from parent process
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**: Commands like `vim`, `less`, `top` work with their own signal handling
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 (270+ tests)
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 266 tests
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "command-stream",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
5
5
  "type": "module",
6
6
  "main": "src/$.mjs",
package/src/$.mjs CHANGED
@@ -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 the command is likely to need it
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
- (this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file));
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
- commandCheck: this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file)
1484
+ interactiveOption: this.options.interactive
1479
1485
  }, null, 2)}`);
1480
1486
 
1481
1487
  const spawnBun = (argv) => {