command-stream 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1334,6 +1334,69 @@ const quickResult = $`pwd`.sync();
1334
1334
  $`npm install`.on('stdout', showProgress).start();
1335
1335
  ```
1336
1336
 
1337
+ ## Common Pitfalls
1338
+
1339
+ ### Array Argument Handling
1340
+
1341
+ When passing multiple arguments, pass the array directly - **never use `.join(' ')`** before interpolation:
1342
+
1343
+ ```javascript
1344
+ import { $ } from 'command-stream';
1345
+
1346
+ // WRONG - entire string becomes ONE argument
1347
+ const args = ['file.txt', '--public', '--verbose'];
1348
+ await $`command ${args.join(' ')}`;
1349
+ // Shell receives: command 'file.txt --public --verbose' (1 argument!)
1350
+ // Error: File does not exist: "file.txt --public --verbose"
1351
+
1352
+ // CORRECT - each element becomes separate argument
1353
+ await $`command ${args}`;
1354
+ // Shell receives: command file.txt --public --verbose (3 arguments)
1355
+ ```
1356
+
1357
+ This is a common mistake that causes errors like:
1358
+
1359
+ ```
1360
+ Error: File does not exist: "/path/to/file.txt --flag --option"
1361
+ ```
1362
+
1363
+ ### Why This Happens
1364
+
1365
+ The `$` template tag handles arrays specially - each element is quoted separately:
1366
+
1367
+ ```javascript
1368
+ if (Array.isArray(value)) {
1369
+ return value.map(quote).join(' '); // Each element quoted individually
1370
+ }
1371
+ ```
1372
+
1373
+ But when you call `.join(' ')` first:
1374
+
1375
+ 1. The array becomes a string: `"file.txt --public --verbose"`
1376
+ 2. Template receives a **string**, not an array
1377
+ 3. The entire string gets quoted as one argument
1378
+ 4. Command receives one argument containing spaces
1379
+
1380
+ ### Recommended Patterns
1381
+
1382
+ ```javascript
1383
+ // Pattern 1: Direct array passing
1384
+ const args = ['file.txt', '--verbose'];
1385
+ await $`command ${args}`;
1386
+
1387
+ // Pattern 2: Separate interpolations
1388
+ const file = 'file.txt';
1389
+ const flags = ['--verbose', '--force'];
1390
+ await $`command ${file} ${flags}`;
1391
+
1392
+ // Pattern 3: Build array dynamically
1393
+ const allArgs = ['input.txt'];
1394
+ if (verbose) allArgs.push('--verbose');
1395
+ await $`command ${allArgs}`;
1396
+ ```
1397
+
1398
+ See [js/BEST-PRACTICES.md](js/BEST-PRACTICES.md) for more detailed guidance.
1399
+
1337
1400
  ## Testing
1338
1401
 
1339
1402
  ```bash
@@ -0,0 +1,376 @@
1
+ # Best Practices for command-stream
2
+
3
+ This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream library.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Array Argument Handling](#array-argument-handling)
8
+ - [String Interpolation](#string-interpolation)
9
+ - [Security Best Practices](#security-best-practices)
10
+ - [Error Handling](#error-handling)
11
+ - [Performance Tips](#performance-tips)
12
+ - [Common Pitfalls](#common-pitfalls)
13
+
14
+ ---
15
+
16
+ ## Array Argument Handling
17
+
18
+ ### Pass Arrays Directly
19
+
20
+ When you have multiple arguments in an array, pass the array directly to template interpolation. The library will automatically handle proper quoting for each element.
21
+
22
+ ```javascript
23
+ import { $ } from 'command-stream';
24
+
25
+ // CORRECT: Pass array directly
26
+ const args = ['file.txt', '--public', '--verbose'];
27
+ await $`command ${args}`;
28
+ // Executed: command file.txt --public --verbose
29
+
30
+ // CORRECT: Dynamic array building
31
+ const baseArgs = ['input.txt'];
32
+ if (isVerbose) baseArgs.push('--verbose');
33
+ if (isForce) baseArgs.push('--force');
34
+ await $`mycommand ${baseArgs}`;
35
+ ```
36
+
37
+ ### Never Use .join() Before Interpolation
38
+
39
+ Calling `.join(' ')` on an array before passing to template interpolation is a common mistake that causes all elements to become a single argument.
40
+
41
+ ```javascript
42
+ // WRONG: Array becomes single argument
43
+ const args = ['file.txt', '--flag'];
44
+ await $`command ${args.join(' ')}`;
45
+ // Shell receives: ['command', 'file.txt --flag'] (1 argument!)
46
+
47
+ // CORRECT: Each element becomes separate argument
48
+ await $`command ${args}`;
49
+ // Shell receives: ['command', 'file.txt', '--flag'] (2 arguments)
50
+ ```
51
+
52
+ ### Mixed Static and Dynamic Arguments
53
+
54
+ When combining static and dynamic arguments, use separate interpolations or arrays:
55
+
56
+ ```javascript
57
+ // CORRECT: Multiple interpolations
58
+ const file = 'data.txt';
59
+ const flags = ['--verbose', '--force'];
60
+ await $`process ${file} ${flags}`;
61
+
62
+ // CORRECT: Build complete array
63
+ const allArgs = [file, ...flags];
64
+ await $`process ${allArgs}`;
65
+
66
+ // WRONG: String concatenation
67
+ await $`process ${file + ' ' + flags.join(' ')}`;
68
+ ```
69
+
70
+ ---
71
+
72
+ ## String Interpolation
73
+
74
+ ### Safe Interpolation (Default)
75
+
76
+ By default, all interpolated values are automatically quoted to prevent shell injection:
77
+
78
+ ```javascript
79
+ // User input is safely escaped
80
+ const userInput = "'; rm -rf /; echo '";
81
+ await $`echo ${userInput}`;
82
+ // Executed safely - input is quoted, not executed
83
+ ```
84
+
85
+ ### Using raw() for Trusted Commands
86
+
87
+ Only use `raw()` with trusted, hardcoded command strings:
88
+
89
+ ```javascript
90
+ import { $, raw } from 'command-stream';
91
+
92
+ // CORRECT: Trusted command template
93
+ const trustedCmd = 'git log --oneline --graph';
94
+ await $`${raw(trustedCmd)}`;
95
+
96
+ // WRONG: User input with raw (security vulnerability!)
97
+ const userInput = req.body.command;
98
+ await $`${raw(userInput)}`; // DANGER: Shell injection!
99
+ ```
100
+
101
+ ### Paths with Spaces
102
+
103
+ Paths containing spaces are automatically quoted:
104
+
105
+ ```javascript
106
+ const path = '/Users/name/My Documents/file.txt';
107
+ await $`cat ${path}`;
108
+ // Executed: cat '/Users/name/My Documents/file.txt'
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Security Best Practices
114
+
115
+ ### Never Trust User Input
116
+
117
+ Always treat external input as potentially malicious:
118
+
119
+ ```javascript
120
+ // CORRECT: Auto-escaping protects against injection
121
+ const filename = req.query.file;
122
+ await $`cat ${filename}`;
123
+
124
+ // WRONG: Bypassing safety for user input
125
+ await $`${raw(userInput)}`;
126
+ ```
127
+
128
+ ### Validate Before Execution
129
+
130
+ Add validation for critical operations:
131
+
132
+ ```javascript
133
+ import { $ } from 'command-stream';
134
+
135
+ async function deleteFile(filename) {
136
+ // Validate filename
137
+ if (filename.includes('..') || filename.startsWith('/')) {
138
+ throw new Error('Invalid filename');
139
+ }
140
+
141
+ await $`rm ${filename}`;
142
+ }
143
+ ```
144
+
145
+ ### Use Principle of Least Privilege
146
+
147
+ Run commands with minimal required permissions:
148
+
149
+ ```javascript
150
+ // Use specific paths instead of wildcards when possible
151
+ await $`rm ${specificFile}`; // Better
152
+ await $`rm ${directory}/*`; // More risky
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Error Handling
158
+
159
+ ### Check Exit Codes
160
+
161
+ By default, commands don't throw on non-zero exit codes:
162
+
163
+ ```javascript
164
+ const result = await $`ls nonexistent`;
165
+ if (result.code !== 0) {
166
+ console.error('Command failed:', result.stderr);
167
+ }
168
+ ```
169
+
170
+ ### Enable errexit for Critical Operations
171
+
172
+ Use shell settings for scripts that should fail on errors:
173
+
174
+ ```javascript
175
+ import { $, shell } from 'command-stream';
176
+
177
+ shell.errexit(true);
178
+
179
+ try {
180
+ await $`critical-operation`;
181
+ } catch (error) {
182
+ console.error('Critical operation failed:', error);
183
+ process.exit(1);
184
+ }
185
+ ```
186
+
187
+ ### Handle Specific Errors
188
+
189
+ ```javascript
190
+ const result = await $`command`;
191
+
192
+ switch (result.code) {
193
+ case 0:
194
+ console.log('Success:', result.stdout);
195
+ break;
196
+ case 1:
197
+ console.error('General error');
198
+ break;
199
+ case 127:
200
+ console.error('Command not found');
201
+ break;
202
+ default:
203
+ console.error(`Unknown error (code ${result.code})`);
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Performance Tips
210
+
211
+ ### Use Streaming for Large Outputs
212
+
213
+ For commands that produce large outputs, use streaming to avoid memory issues:
214
+
215
+ ```javascript
216
+ // Memory efficient: Process chunks as they arrive
217
+ for await (const chunk of $`cat huge-file.log`.stream()) {
218
+ processChunk(chunk.data);
219
+ }
220
+
221
+ // Memory intensive: Buffers entire output
222
+ const result = await $`cat huge-file.log`;
223
+ processAll(result.stdout);
224
+ ```
225
+
226
+ ### Parallel Execution
227
+
228
+ Run independent commands in parallel:
229
+
230
+ ```javascript
231
+ // Sequential (slower)
232
+ await $`task1`;
233
+ await $`task2`;
234
+ await $`task3`;
235
+
236
+ // Parallel (faster)
237
+ await Promise.all([$`task1`, $`task2`, $`task3`]);
238
+ ```
239
+
240
+ ### Use Built-in Commands
241
+
242
+ Built-in commands are faster as they don't spawn system processes:
243
+
244
+ ```javascript
245
+ // Fast: Built-in command (pure JavaScript)
246
+ await $`mkdir -p build/output`;
247
+
248
+ // Slower: System command
249
+ await $`/bin/mkdir -p build/output`;
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Common Pitfalls
255
+
256
+ ### 1. Array.join() Pitfall (Most Common)
257
+
258
+ **Problem:** Using `.join(' ')` before interpolation merges all arguments into one.
259
+
260
+ ```javascript
261
+ // WRONG
262
+ const args = ['file.txt', '--flag'];
263
+ await $`cmd ${args.join(' ')}`; // 1 argument: "file.txt --flag"
264
+
265
+ // CORRECT
266
+ await $`cmd ${args}`; // 2 arguments: "file.txt", "--flag"
267
+ ```
268
+
269
+ See [Case Study: Issue #153](./docs/case-studies/issue-153/README.md) for detailed analysis.
270
+
271
+ ### 2. Template String Concatenation
272
+
273
+ **Problem:** Building commands with template strings creates single arguments.
274
+
275
+ ```javascript
276
+ // WRONG
277
+ const file = 'data.txt';
278
+ const flag = '--verbose';
279
+ await $`cmd ${`${file} ${flag}`}`; // 1 argument: "data.txt --verbose"
280
+
281
+ // CORRECT
282
+ await $`cmd ${file} ${flag}`; // 2 arguments
283
+ ```
284
+
285
+ ### 3. Forgetting await
286
+
287
+ **Problem:** Commands return promises, forgetting await causes issues.
288
+
289
+ ```javascript
290
+ // WRONG: Command may not complete before next line
291
+ $`setup-task`;
292
+ $`main-task`; // May run before setup completes
293
+
294
+ // CORRECT: Wait for completion
295
+ await $`setup-task`;
296
+ await $`main-task`;
297
+ ```
298
+
299
+ ### 4. Assuming Synchronous Behavior
300
+
301
+ **Problem:** Expecting immediate results without awaiting.
302
+
303
+ ```javascript
304
+ // WRONG
305
+ const cmd = $`echo hello`;
306
+ console.log(cmd.stdout); // undefined - not yet executed!
307
+
308
+ // CORRECT
309
+ const result = await $`echo hello`;
310
+ console.log(result.stdout); // "hello\n"
311
+ ```
312
+
313
+ ### 5. Not Handling stderr
314
+
315
+ **Problem:** Only checking stdout when errors go to stderr.
316
+
317
+ ```javascript
318
+ // INCOMPLETE
319
+ const result = await $`command`;
320
+ console.log(result.stdout);
321
+
322
+ // BETTER
323
+ const result = await $`command`;
324
+ if (result.code !== 0) {
325
+ console.error('Error:', result.stderr);
326
+ } else {
327
+ console.log('Success:', result.stdout);
328
+ }
329
+ ```
330
+
331
+ ### 6. Ignoring Exit Codes
332
+
333
+ **Problem:** Assuming success without checking.
334
+
335
+ ```javascript
336
+ // WRONG
337
+ const result = await $`risky-command`;
338
+ processOutput(result.stdout); // May be empty on failure!
339
+
340
+ // CORRECT
341
+ const result = await $`risky-command`;
342
+ if (result.code === 0) {
343
+ processOutput(result.stdout);
344
+ } else {
345
+ handleError(result);
346
+ }
347
+ ```
348
+
349
+ ---
350
+
351
+ ## Quick Reference
352
+
353
+ ### Do's
354
+
355
+ - Pass arrays directly: `${args}`
356
+ - Use separate interpolations: `${file} ${flag}`
357
+ - Check exit codes after execution
358
+ - Use streaming for large outputs
359
+ - Validate user input before execution
360
+ - Use built-in commands when available
361
+
362
+ ### Don'ts
363
+
364
+ - Never use `args.join(' ')` before interpolation
365
+ - Never use `raw()` with user input
366
+ - Don't forget `await` on commands
367
+ - Don't assume success without checking
368
+ - Don't ignore stderr output
369
+
370
+ ---
371
+
372
+ ## See Also
373
+
374
+ - [README.md](../README.md) - Main documentation
375
+ - [docs/case-studies/issue-153/README.md](./docs/case-studies/issue-153/README.md) - Array.join() pitfall case study
376
+ - [src/$.quote.mjs](./src/$.quote.mjs) - Quote function implementation
@@ -0,0 +1,129 @@
1
+ # Implementation Notes for Shell Operators Support
2
+
3
+ ## Summary
4
+
5
+ The current implementation passes commands with `&&`, `||`, `;`, `()` to `sh -c`, which runs them in a subprocess. This prevents the virtual `cd` command from affecting the parent process directory.
6
+
7
+ ## Solution
8
+
9
+ We've created a shell parser (`src/shell-parser.mjs`) that can parse these operators. Now we need to integrate it into `src/$.mjs` to execute parsed commands through our virtual command system.
10
+
11
+ ## Required Changes in src/$.mjs
12
+
13
+ ### 1. Import the parser
14
+
15
+ ```javascript
16
+ import { parseShellCommand, needsRealShell } from './shell-parser.mjs';
17
+ ```
18
+
19
+ ### 2. Modify \_doStartAsync method
20
+
21
+ Before checking for pipes, check if the command contains operators we can handle:
22
+
23
+ ```javascript
24
+ async _doStartAsync() {
25
+ // ... existing code ...
26
+
27
+ // Check if command needs parsing for operators
28
+ if (this.spec.mode === 'shell' && !needsRealShell(this.spec.command)) {
29
+ const parsed = parseShellCommand(this.spec.command);
30
+ if (parsed && parsed.type === 'sequence') {
31
+ return await this._runSequence(parsed);
32
+ } else if (parsed && parsed.type === 'subshell') {
33
+ return await this._runSubshell(parsed);
34
+ }
35
+ }
36
+
37
+ // ... continue with existing pipeline check ...
38
+ }
39
+ ```
40
+
41
+ ### 3. Add \_runSequence method
42
+
43
+ ```javascript
44
+ async _runSequence(sequence) {
45
+ let lastResult = { code: 0, stdout: '', stderr: '' };
46
+ let combinedStdout = '';
47
+ let combinedStderr = '';
48
+
49
+ for (let i = 0; i < sequence.commands.length; i++) {
50
+ const command = sequence.commands[i];
51
+ const operator = i > 0 ? sequence.operators[i - 1] : null;
52
+
53
+ // Check operator conditions
54
+ if (operator === '&&' && lastResult.code !== 0) continue;
55
+ if (operator === '||' && lastResult.code === 0) continue;
56
+
57
+ // Execute command
58
+ if (command.type === 'subshell') {
59
+ lastResult = await this._runSubshell(command);
60
+ } else if (command.type === 'pipeline') {
61
+ lastResult = await this._runPipeline(command.commands);
62
+ } else if (command.type === 'simple') {
63
+ lastResult = await this._runSimpleCommand(command);
64
+ }
65
+
66
+ combinedStdout += lastResult.stdout;
67
+ combinedStderr += lastResult.stderr;
68
+ }
69
+
70
+ return {
71
+ code: lastResult.code,
72
+ stdout: combinedStdout,
73
+ stderr: combinedStderr
74
+ };
75
+ }
76
+ ```
77
+
78
+ ### 4. Add \_runSubshell method
79
+
80
+ ```javascript
81
+ async _runSubshell(subshell) {
82
+ // Save current directory
83
+ const savedCwd = process.cwd();
84
+
85
+ try {
86
+ // Execute subshell command
87
+ const result = await this._runSequence(subshell.command);
88
+ return result;
89
+ } finally {
90
+ // Restore directory
91
+ process.chdir(savedCwd);
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### 5. Add \_runSimpleCommand method
97
+
98
+ ```javascript
99
+ async _runSimpleCommand(command) {
100
+ const { cmd, args, redirects } = command;
101
+
102
+ // Check for virtual command
103
+ if (virtualCommandsEnabled && virtualCommands.has(cmd)) {
104
+ const argValues = args.map(a => a.value);
105
+ return await this._runVirtual(cmd, argValues);
106
+ }
107
+
108
+ // Fall back to real command execution
109
+ // ... handle redirects if present ...
110
+
111
+ return await this._executeRealCommand(cmd, args);
112
+ }
113
+ ```
114
+
115
+ ## Testing
116
+
117
+ After implementation:
118
+
119
+ 1. `cd /tmp && pwd` should output `/tmp`
120
+ 2. `cd /tmp` followed by `pwd` should output `/tmp`
121
+ 3. `(cd /tmp && pwd) ; pwd` should output `/tmp` then original directory
122
+ 4. All existing tests should still pass
123
+
124
+ ## Benefits
125
+
126
+ 1. Virtual commands work with shell operators
127
+ 2. `cd` behaves exactly like shell cd
128
+ 3. Better performance (no subprocess for simple operator chains)
129
+ 4. Consistent behavior across platforms
@@ -0,0 +1,101 @@
1
+ # Shell Operators Implementation Complete
2
+
3
+ ## Summary
4
+
5
+ We've successfully implemented support for shell operators (`&&`, `||`, `;`, `()`) in command-stream, allowing virtual commands like `cd` to work correctly with these operators.
6
+
7
+ ## What Was Implemented
8
+
9
+ ### 1. Shell Parser (`src/shell-parser.mjs`)
10
+
11
+ - Tokenizes and parses shell commands with operators
12
+ - Handles `&&` (AND), `||` (OR), `;` (semicolon), `()` (subshells)
13
+ - Supports pipes `|`, redirections `>`, `>>`, `<`
14
+ - Properly handles quoted strings and escaped characters
15
+ - Falls back to `sh -c` for unsupported features (globs, variable expansion, etc.)
16
+
17
+ ### 2. ProcessRunner Enhancements (`src/$.mjs`)
18
+
19
+ - `_runSequence()` - Executes command sequences with proper operator semantics
20
+ - `_runSubshell()` - Handles subshell isolation (saves/restores cwd)
21
+ - `_runSimpleCommand()` - Executes individual commands (virtual or real)
22
+ - Integration with existing pipeline support
23
+
24
+ ### 3. Fixed cd Command Behavior
25
+
26
+ - `cd` now works correctly in all contexts:
27
+ - `cd /tmp` - changes directory (persists) ✓
28
+ - `cd /tmp && ls` - both commands see /tmp ✓
29
+ - `(cd /tmp && ls)` - subshell isolation ✓
30
+ - `cd /tmp ; pwd ; cd /usr ; pwd` - sequential execution ✓
31
+
32
+ ## How It Works
33
+
34
+ 1. When a command contains operators, the enhanced parser parses it into an AST
35
+ 2. The executor traverses the AST, respecting operator semantics:
36
+ - `&&` - run next only if previous succeeds (exit code 0)
37
+ - `||` - run next only if previous fails (exit code ≠ 0)
38
+ - `;` - run next regardless
39
+ - `()` - run in subshell with saved/restored directory
40
+ 3. Virtual commands execute in-process, maintaining state
41
+ 4. Real commands spawn subprocesses as needed
42
+ 5. Falls back to `sh -c` for unsupported features
43
+
44
+ ## Examples That Now Work
45
+
46
+ ```javascript
47
+ // cd with && operator
48
+ await $`cd /tmp && pwd`; // Output: /tmp
49
+
50
+ // cd with || operator
51
+ await $`cd /nonexistent || echo "failed"`; // Output: failed
52
+
53
+ // Multiple commands with ;
54
+ await $`cd /tmp ; pwd ; cd /usr ; pwd`; // Output: /tmp\n/usr
55
+
56
+ // Subshell isolation
57
+ await $`(cd /tmp && pwd) ; pwd`; // Output: /tmp\n<original-dir>
58
+
59
+ // Complex chains
60
+ await $`cd /tmp && git init && echo "done"`;
61
+
62
+ // Nested subshells
63
+ await $`(cd /tmp && (cd /usr && pwd) && pwd)`; // Output: /usr\n/tmp
64
+ ```
65
+
66
+ ## Benefits
67
+
68
+ 1. **Correct Shell Semantics** - cd and other virtual commands behave exactly like in a real shell
69
+ 2. **Performance** - No subprocess overhead for simple command chains
70
+ 3. **Cross-platform** - Consistent behavior across platforms
71
+ 4. **Backward Compatible** - Existing code continues to work
72
+ 5. **Graceful Fallback** - Complex shell features still work via `sh -c`
73
+
74
+ ## Testing
75
+
76
+ All major shell operator scenarios are tested and working:
77
+
78
+ - ✓ AND operator (`&&`)
79
+ - ✓ OR operator (`||`)
80
+ - ✓ Semicolon operator (`;`)
81
+ - ✓ Subshells (`()`)
82
+ - ✓ Nested subshells
83
+ - ✓ Complex command chains
84
+ - ✓ Directory persistence
85
+ - ✓ Path with spaces
86
+
87
+ ## Files Modified
88
+
89
+ 1. `src/$.mjs` - Added sequence/subshell execution methods
90
+ 2. `src/shell-parser.mjs` - New file for parsing shell operators
91
+ 3. `src/commands/$.pwd.mjs` - Fixed to output newline
92
+ 4. Various test and example files
93
+
94
+ ## Future Enhancements
95
+
96
+ Possible future additions:
97
+
98
+ - Background execution (`&`)
99
+ - Here documents (`<<`)
100
+ - Process substitution (`<()`, `>()`)
101
+ - More complex redirections (`2>&1`, etc.)