From 5f78ead68f1be6628dba4d2b1bb64656dbf255b5 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 22:49:15 +0100 Subject: [PATCH 1/5] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/command-stream/issues/153 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..57390a3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/command-stream/issues/153 +Your prepared branch: issue-153-58c6ab4753ab +Your prepared working directory: /tmp/gh-issue-solver-1768081753441 + +Proceed. From f779985b223f82f295d8168feb7b15c12fa08a65 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 22:58:54 +0100 Subject: [PATCH 2/5] docs: Document Array.join() pitfall and add best practices (fixes #153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds comprehensive documentation about the Array.join() pitfall where calling .join(' ') before template interpolation causes all arguments to be merged into a single shell argument. Changes: - Add BEST-PRACTICES.md with detailed usage patterns - Add Common Pitfalls section to README.md explaining the issue - Add docs/case-studies/issue-153/ with real-world investigation - Add rust/BEST-PRACTICES.md for Rust-specific patterns - Add js/tests/array-interpolation.test.mjs with 34 tests covering: - Direct array passing (correct usage) - Pre-joined array (anti-pattern demonstration) - Mixed interpolation patterns - Real-world use cases (git, npm, docker, rsync) - Edge cases and documentation examples verification - Bump version to 0.9.3 The bug was discovered in production (hive-mind#1096) where CLI arguments were incorrectly merged into a file path, causing errors like: "File does not exist: /path/to/file.txt --public --verbose" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- BEST-PRACTICES.md | 376 +++++++++++++++++++++++++ README.md | 63 +++++ docs/case-studies/issue-153/README.md | 167 +++++++++++ js/tests/array-interpolation.test.mjs | 329 ++++++++++++++++++++++ package.json | 2 +- rust/BEST-PRACTICES.md | 382 ++++++++++++++++++++++++++ 6 files changed, 1318 insertions(+), 1 deletion(-) create mode 100644 BEST-PRACTICES.md create mode 100644 docs/case-studies/issue-153/README.md create mode 100644 js/tests/array-interpolation.test.mjs create mode 100644 rust/BEST-PRACTICES.md diff --git a/BEST-PRACTICES.md b/BEST-PRACTICES.md new file mode 100644 index 0000000..d792324 --- /dev/null +++ b/BEST-PRACTICES.md @@ -0,0 +1,376 @@ +# Best Practices for command-stream + +This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream library. + +## Table of Contents + +- [Array Argument Handling](#array-argument-handling) +- [String Interpolation](#string-interpolation) +- [Security Best Practices](#security-best-practices) +- [Error Handling](#error-handling) +- [Performance Tips](#performance-tips) +- [Common Pitfalls](#common-pitfalls) + +--- + +## Array Argument Handling + +### Pass Arrays Directly + +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. + +```javascript +import { $ } from 'command-stream'; + +// CORRECT: Pass array directly +const args = ['file.txt', '--public', '--verbose']; +await $`command ${args}`; +// Executed: command file.txt --public --verbose + +// CORRECT: Dynamic array building +const baseArgs = ['input.txt']; +if (isVerbose) baseArgs.push('--verbose'); +if (isForce) baseArgs.push('--force'); +await $`mycommand ${baseArgs}`; +``` + +### Never Use .join() Before Interpolation + +Calling `.join(' ')` on an array before passing to template interpolation is a common mistake that causes all elements to become a single argument. + +```javascript +// WRONG: Array becomes single argument +const args = ['file.txt', '--flag']; +await $`command ${args.join(' ')}`; +// Shell receives: ['command', 'file.txt --flag'] (1 argument!) + +// CORRECT: Each element becomes separate argument +await $`command ${args}`; +// Shell receives: ['command', 'file.txt', '--flag'] (2 arguments) +``` + +### Mixed Static and Dynamic Arguments + +When combining static and dynamic arguments, use separate interpolations or arrays: + +```javascript +// CORRECT: Multiple interpolations +const file = 'data.txt'; +const flags = ['--verbose', '--force']; +await $`process ${file} ${flags}`; + +// CORRECT: Build complete array +const allArgs = [file, ...flags]; +await $`process ${allArgs}`; + +// WRONG: String concatenation +await $`process ${file + ' ' + flags.join(' ')}`; +``` + +--- + +## String Interpolation + +### Safe Interpolation (Default) + +By default, all interpolated values are automatically quoted to prevent shell injection: + +```javascript +// User input is safely escaped +const userInput = "'; rm -rf /; echo '"; +await $`echo ${userInput}`; +// Executed safely - input is quoted, not executed +``` + +### Using raw() for Trusted Commands + +Only use `raw()` with trusted, hardcoded command strings: + +```javascript +import { $, raw } from 'command-stream'; + +// CORRECT: Trusted command template +const trustedCmd = 'git log --oneline --graph'; +await $`${raw(trustedCmd)}`; + +// WRONG: User input with raw (security vulnerability!) +const userInput = req.body.command; +await $`${raw(userInput)}`; // DANGER: Shell injection! +``` + +### Paths with Spaces + +Paths containing spaces are automatically quoted: + +```javascript +const path = '/Users/name/My Documents/file.txt'; +await $`cat ${path}`; +// Executed: cat '/Users/name/My Documents/file.txt' +``` + +--- + +## Security Best Practices + +### Never Trust User Input + +Always treat external input as potentially malicious: + +```javascript +// CORRECT: Auto-escaping protects against injection +const filename = req.query.file; +await $`cat ${filename}`; + +// WRONG: Bypassing safety for user input +await $`${raw(userInput)}`; +``` + +### Validate Before Execution + +Add validation for critical operations: + +```javascript +import { $ } from 'command-stream'; + +async function deleteFile(filename) { + // Validate filename + if (filename.includes('..') || filename.startsWith('/')) { + throw new Error('Invalid filename'); + } + + await $`rm ${filename}`; +} +``` + +### Use Principle of Least Privilege + +Run commands with minimal required permissions: + +```javascript +// Use specific paths instead of wildcards when possible +await $`rm ${specificFile}`; // Better +await $`rm ${directory}/*`; // More risky +``` + +--- + +## Error Handling + +### Check Exit Codes + +By default, commands don't throw on non-zero exit codes: + +```javascript +const result = await $`ls nonexistent`; +if (result.code !== 0) { + console.error('Command failed:', result.stderr); +} +``` + +### Enable errexit for Critical Operations + +Use shell settings for scripts that should fail on errors: + +```javascript +import { $, shell } from 'command-stream'; + +shell.errexit(true); + +try { + await $`critical-operation`; +} catch (error) { + console.error('Critical operation failed:', error); + process.exit(1); +} +``` + +### Handle Specific Errors + +```javascript +const result = await $`command`; + +switch (result.code) { + case 0: + console.log('Success:', result.stdout); + break; + case 1: + console.error('General error'); + break; + case 127: + console.error('Command not found'); + break; + default: + console.error(`Unknown error (code ${result.code})`); +} +``` + +--- + +## Performance Tips + +### Use Streaming for Large Outputs + +For commands that produce large outputs, use streaming to avoid memory issues: + +```javascript +// Memory efficient: Process chunks as they arrive +for await (const chunk of $`cat huge-file.log`.stream()) { + processChunk(chunk.data); +} + +// Memory intensive: Buffers entire output +const result = await $`cat huge-file.log`; +processAll(result.stdout); +``` + +### Parallel Execution + +Run independent commands in parallel: + +```javascript +// Sequential (slower) +await $`task1`; +await $`task2`; +await $`task3`; + +// Parallel (faster) +await Promise.all([$`task1`, $`task2`, $`task3`]); +``` + +### Use Built-in Commands + +Built-in commands are faster as they don't spawn system processes: + +```javascript +// Fast: Built-in command (pure JavaScript) +await $`mkdir -p build/output`; + +// Slower: System command +await $`/bin/mkdir -p build/output`; +``` + +--- + +## Common Pitfalls + +### 1. Array.join() Pitfall (Most Common) + +**Problem:** Using `.join(' ')` before interpolation merges all arguments into one. + +```javascript +// WRONG +const args = ['file.txt', '--flag']; +await $`cmd ${args.join(' ')}`; // 1 argument: "file.txt --flag" + +// CORRECT +await $`cmd ${args}`; // 2 arguments: "file.txt", "--flag" +``` + +See [Case Study: Issue #153](docs/case-studies/issue-153/README.md) for detailed analysis. + +### 2. Template String Concatenation + +**Problem:** Building commands with template strings creates single arguments. + +```javascript +// WRONG +const file = 'data.txt'; +const flag = '--verbose'; +await $`cmd ${`${file} ${flag}`}`; // 1 argument: "data.txt --verbose" + +// CORRECT +await $`cmd ${file} ${flag}`; // 2 arguments +``` + +### 3. Forgetting await + +**Problem:** Commands return promises, forgetting await causes issues. + +```javascript +// WRONG: Command may not complete before next line +$`setup-task`; +$`main-task`; // May run before setup completes + +// CORRECT: Wait for completion +await $`setup-task`; +await $`main-task`; +``` + +### 4. Assuming Synchronous Behavior + +**Problem:** Expecting immediate results without awaiting. + +```javascript +// WRONG +const cmd = $`echo hello`; +console.log(cmd.stdout); // undefined - not yet executed! + +// CORRECT +const result = await $`echo hello`; +console.log(result.stdout); // "hello\n" +``` + +### 5. Not Handling stderr + +**Problem:** Only checking stdout when errors go to stderr. + +```javascript +// INCOMPLETE +const result = await $`command`; +console.log(result.stdout); + +// BETTER +const result = await $`command`; +if (result.code !== 0) { + console.error('Error:', result.stderr); +} else { + console.log('Success:', result.stdout); +} +``` + +### 6. Ignoring Exit Codes + +**Problem:** Assuming success without checking. + +```javascript +// WRONG +const result = await $`risky-command`; +processOutput(result.stdout); // May be empty on failure! + +// CORRECT +const result = await $`risky-command`; +if (result.code === 0) { + processOutput(result.stdout); +} else { + handleError(result); +} +``` + +--- + +## Quick Reference + +### Do's + +- Pass arrays directly: `${args}` +- Use separate interpolations: `${file} ${flag}` +- Check exit codes after execution +- Use streaming for large outputs +- Validate user input before execution +- Use built-in commands when available + +### Don'ts + +- Never use `args.join(' ')` before interpolation +- Never use `raw()` with user input +- Don't forget `await` on commands +- Don't assume success without checking +- Don't ignore stderr output + +--- + +## See Also + +- [README.md](README.md) - Main documentation +- [docs/case-studies/issue-153/README.md](docs/case-studies/issue-153/README.md) - Array.join() pitfall case study +- [js/src/$.quote.mjs](js/src/$.quote.mjs) - Quote function implementation diff --git a/README.md b/README.md index 26ba11b..4e21a86 100644 --- a/README.md +++ b/README.md @@ -1334,6 +1334,69 @@ const quickResult = $`pwd`.sync(); $`npm install`.on('stdout', showProgress).start(); ``` +## Common Pitfalls + +### Array Argument Handling + +When passing multiple arguments, pass the array directly - **never use `.join(' ')`** before interpolation: + +```javascript +import { $ } from 'command-stream'; + +// WRONG - entire string becomes ONE argument +const args = ['file.txt', '--public', '--verbose']; +await $`command ${args.join(' ')}`; +// Shell receives: command 'file.txt --public --verbose' (1 argument!) +// Error: File does not exist: "file.txt --public --verbose" + +// CORRECT - each element becomes separate argument +await $`command ${args}`; +// Shell receives: command file.txt --public --verbose (3 arguments) +``` + +This is a common mistake that causes errors like: + +``` +Error: File does not exist: "/path/to/file.txt --flag --option" +``` + +### Why This Happens + +The `$` template tag handles arrays specially - each element is quoted separately: + +```javascript +if (Array.isArray(value)) { + return value.map(quote).join(' '); // Each element quoted individually +} +``` + +But when you call `.join(' ')` first: + +1. The array becomes a string: `"file.txt --public --verbose"` +2. Template receives a **string**, not an array +3. The entire string gets quoted as one argument +4. Command receives one argument containing spaces + +### Recommended Patterns + +```javascript +// Pattern 1: Direct array passing +const args = ['file.txt', '--verbose']; +await $`command ${args}`; + +// Pattern 2: Separate interpolations +const file = 'file.txt'; +const flags = ['--verbose', '--force']; +await $`command ${file} ${flags}`; + +// Pattern 3: Build array dynamically +const allArgs = ['input.txt']; +if (verbose) allArgs.push('--verbose'); +await $`command ${allArgs}`; +``` + +See [BEST-PRACTICES.md](BEST-PRACTICES.md) for more detailed guidance. + ## Testing ```bash diff --git a/docs/case-studies/issue-153/README.md b/docs/case-studies/issue-153/README.md new file mode 100644 index 0000000..0c862bd --- /dev/null +++ b/docs/case-studies/issue-153/README.md @@ -0,0 +1,167 @@ +# Case Study: Array.join() Pitfall Causes Arguments to Merge (Issue #153) + +## Summary + +This document provides a comprehensive analysis of the `Array.join()` pitfall in command-stream, where calling `.join(' ')` on an array before template interpolation causes all elements to be treated as a single argument instead of multiple separate arguments. + +## Timeline of Events + +### January 10, 2026 + +1. **~00:10 UTC** - Production bug discovered in `hive-mind` repository (issue link-assistant/hive-mind#1096) +2. **~00:10 UTC** - Log upload command failed with error: `File does not exist: "/tmp/solution-draft-log-pr-1768003849690.txt" --public --verbose` +3. **~21:47 UTC** - Issue #153 created to document the pitfall and improve documentation + +## Real-World Impact + +### Production Bug in hive-mind#1096 + +The bug manifested in a log upload workflow where CLI arguments were incorrectly joined: + +**Error Message:** + +``` +Error: File does not exist: "/tmp/solution-draft-log-pr-1768003849690.txt" --public --verbose +``` + +**Root Cause:** +The flags `--public` and `--verbose` were incorrectly merged into the file path as a single string argument. The gh-upload-log command received: + +- Expected: 3 arguments: `"/tmp/solution-draft-log-pr-1768003849690.txt"`, `--public`, `--verbose` +- Actual: 1 argument: `"/tmp/solution-draft-log-pr-1768003849690.txt" --public --verbose` + +**Original Buggy Code Pattern:** + +```javascript +const commandArgs = [`${logFile}`, publicFlag]; +if (verbose) commandArgs.push('--verbose'); +await $`gh-upload-log ${commandArgs.join(' ')}`; // BUG: Single string argument +``` + +**Fixed Code Pattern:** + +```javascript +await $`gh-upload-log ${logFile} ${publicFlag} --verbose`; // Each value is a separate argument +``` + +## Technical Analysis + +### Why This Happens + +The `buildShellCommand` function in `$.quote.mjs` handles arrays specially: + +```javascript +if (Array.isArray(value)) { + return value.map(quote).join(' '); // Each element quoted separately +} +``` + +When an array is passed directly: + +1. Each element is individually quoted +2. They are joined with spaces +3. The shell receives multiple arguments + +But when you call `.join(' ')` before passing to the template: + +1. The array becomes a string: `"file.txt --public --verbose"` +2. Template receives a **string**, not an array +3. The **entire string** gets quoted as one shell argument +4. The command sees one argument containing spaces, not multiple arguments + +### Demonstration + +```javascript +// Direct array interpolation (CORRECT) +const args = ['file.txt', '--public', '--verbose']; +await $`command ${args}`; +// Executed: command file.txt --public --verbose +// Shell receives: ['command', 'file.txt', '--public', '--verbose'] + +// Pre-joined array (INCORRECT) +const args = ['file.txt', '--public', '--verbose']; +await $`command ${args.join(' ')}`; +// Executed: command 'file.txt --public --verbose' +// Shell receives: ['command', 'file.txt --public --verbose'] +``` + +## Solutions + +### Correct Usage Patterns + +#### 1. Pass Array Directly (Recommended) + +```javascript +const args = ['file.txt', '--public', '--verbose']; +await $`command ${args}`; +``` + +#### 2. Use Separate Interpolations + +```javascript +const file = 'file.txt'; +const flags = ['--public', '--verbose']; +await $`command ${file} ${flags}`; +``` + +#### 3. Build Array Dynamically + +```javascript +const baseArgs = ['file.txt']; +const conditionalArgs = isVerbose ? ['--verbose'] : []; +const allArgs = [...baseArgs, ...conditionalArgs]; +await $`command ${allArgs}`; +``` + +### Incorrect Usage (Anti-Patterns) + +```javascript +// DON'T DO THIS - array becomes single argument +await $`command ${args.join(' ')}`; + +// DON'T DO THIS - template string becomes single argument +await $`command ${`${file} ${flag}`}`; + +// DON'T DO THIS - manual string concatenation +await $`command ${file + ' ' + flag}`; +``` + +## Error Recognition + +When you see errors like these, suspect the Array.join() pitfall: + +1. **File not found with flags in the path:** + + ``` + Error: File does not exist: "/path/to/file.txt --flag --option" + ``` + +2. **Command received unexpected argument count:** + + ``` + Error: Expected 3 arguments, got 1 + ``` + +3. **Flags not recognized:** + ``` + Error: Unknown option: "value --flag" + ``` + +## Prevention Strategies + +1. **Never use `.join()` before template interpolation** - Pass arrays directly +2. **Review string concatenation** - Ensure separate values stay separate +3. **Test with special characters** - Include spaces and flags in test cases +4. **Add debug logging** - Log the actual arguments being passed + +## Related Documentation + +- [BEST-PRACTICES.md](../../../BEST-PRACTICES.md) - Best practices for command-stream usage +- [README.md](../../../README.md) - Common Pitfalls section +- [$.quote.mjs](../../../js/src/$.quote.mjs) - Quote function implementation + +## References + +- Issue #153: https://github.com/link-foundation/command-stream/issues/153 +- Production Bug: https://github.com/link-assistant/hive-mind/issues/1096 +- Full Log: https://gist.github.com/konard/70a7c02ac0d1eee232dae2fbe5eeca7b diff --git a/js/tests/array-interpolation.test.mjs b/js/tests/array-interpolation.test.mjs new file mode 100644 index 0000000..feec505 --- /dev/null +++ b/js/tests/array-interpolation.test.mjs @@ -0,0 +1,329 @@ +/** + * Tests for array interpolation in command-stream + * + * This test file covers the Array.join() pitfall documented in issue #153. + * The key insight: arrays passed directly to template interpolation are handled + * correctly (each element becomes a separate argument), but if you call .join(' ') + * before passing, the entire string becomes a single argument. + * + * @see https://github.com/link-foundation/command-stream/issues/153 + * @see docs/case-studies/issue-153/README.md + * @see BEST-PRACTICES.md + */ + +import { $, quote } from '../src/$.mjs'; +import { describe, test, expect } from 'bun:test'; +import './test-helper.mjs'; // Automatically sets up beforeEach/afterEach cleanup + +describe('Array Interpolation', () => { + describe('Direct Array Passing (Correct Usage)', () => { + test('should treat each array element as separate argument', () => { + const args = ['arg1', 'arg2', 'arg3']; + const cmd = $({ mirror: false })`echo ${args}`; + + expect(cmd.spec.command).toBe('echo arg1 arg2 arg3'); + }); + + test('should quote elements with spaces individually', () => { + const args = ['file with spaces.txt', '--verbose']; + const cmd = $({ mirror: false })`command ${args}`; + + expect(cmd.spec.command).toBe("command 'file with spaces.txt' --verbose"); + }); + + test('should handle empty array', () => { + const args = []; + const cmd = $({ mirror: false })`echo ${args}`; + + expect(cmd.spec.command).toBe('echo '); + }); + + test('should handle single-element array', () => { + const args = ['single']; + const cmd = $({ mirror: false })`echo ${args}`; + + expect(cmd.spec.command).toBe('echo single'); + }); + + test('should handle array with special characters', () => { + const args = ['$var', '`command`', '$(sub)']; + const cmd = $({ mirror: false })`echo ${args}`; + + // Each special character should be quoted + expect(cmd.spec.command).toBe("echo '$var' '`command`' '$(sub)'"); + }); + + test('should handle array with flags correctly', () => { + const args = ['input.txt', '--public', '--verbose']; + const cmd = $({ mirror: false })`upload ${args}`; + + expect(cmd.spec.command).toBe('upload input.txt --public --verbose'); + }); + }); + + describe('Pre-joined Array (Anti-pattern)', () => { + test('joined array becomes single argument with spaces', () => { + const args = ['file.txt', '--flag']; + // This is the anti-pattern - joining before interpolation + const joined = args.join(' '); + const cmd = $({ mirror: false })`command ${joined}`; + + // The joined string gets quoted as ONE argument + expect(cmd.spec.command).toBe("command 'file.txt --flag'"); + }); + + test('demonstrates the bug: flags become part of filename', () => { + // This reproduces the exact bug from hive-mind#1096 + const args = ['/tmp/logfile.txt', '--public', '--verbose']; + const joined = args.join(' '); + const cmd = $({ mirror: false })`gh-upload-log ${joined}`; + + // WRONG: The shell sees one argument containing spaces + expect(cmd.spec.command).toBe( + "gh-upload-log '/tmp/logfile.txt --public --verbose'" + ); + // This would cause: Error: File does not exist: "/tmp/logfile.txt --public --verbose" + }); + + test('correct usage vs incorrect usage comparison', () => { + const args = ['file.txt', '--flag1', '--flag2']; + + // CORRECT: Direct array interpolation + const correctCmd = $({ mirror: false })`cmd ${args}`; + expect(correctCmd.spec.command).toBe('cmd file.txt --flag1 --flag2'); + + // INCORRECT: Pre-joined array + const incorrectCmd = $({ mirror: false })`cmd ${args.join(' ')}`; + expect(incorrectCmd.spec.command).toBe("cmd 'file.txt --flag1 --flag2'"); + }); + }); + + describe('Mixed Interpolation Patterns', () => { + test('should handle multiple separate interpolations', () => { + const file = 'data.txt'; + const flags = ['--verbose', '--force']; + const cmd = $({ mirror: false })`process ${file} ${flags}`; + + expect(cmd.spec.command).toBe('process data.txt --verbose --force'); + }); + + test('should handle array with conditional elements', () => { + const baseArgs = ['input.txt']; + const verbose = true; + const force = false; + + if (verbose) { + baseArgs.push('--verbose'); + } + if (force) { + baseArgs.push('--force'); + } + + const cmd = $({ mirror: false })`command ${baseArgs}`; + expect(cmd.spec.command).toBe('command input.txt --verbose'); + }); + + test('should handle spread operator pattern', () => { + const files = ['file1.txt', 'file2.txt']; + const flags = ['--recursive']; + const allArgs = [...files, ...flags]; + + const cmd = $({ mirror: false })`copy ${allArgs}`; + expect(cmd.spec.command).toBe('copy file1.txt file2.txt --recursive'); + }); + }); + + describe('Real-World Use Cases', () => { + test('git command with multiple flags', () => { + const flags = ['--oneline', '--graph', '--all']; + const cmd = $({ mirror: false })`git log ${flags}`; + + expect(cmd.spec.command).toBe('git log --oneline --graph --all'); + }); + + test('npm install with packages', () => { + const packages = ['lodash', 'express', 'typescript']; + const cmd = $({ mirror: false })`npm install ${packages}`; + + expect(cmd.spec.command).toBe('npm install lodash express typescript'); + }); + + test('file operations with paths containing spaces', () => { + const files = ['My Documents/file1.txt', 'Other Folder/file2.txt']; + const cmd = $({ mirror: false })`cat ${files}`; + + expect(cmd.spec.command).toBe( + "cat 'My Documents/file1.txt' 'Other Folder/file2.txt'" + ); + }); + + test('docker command with environment variables', () => { + const envVars = ['-e', 'NODE_ENV=production', '-e', 'DEBUG=false']; + const cmd = $({ mirror: false })`docker run ${envVars} myimage`; + + expect(cmd.spec.command).toBe( + 'docker run -e NODE_ENV=production -e DEBUG=false myimage' + ); + }); + + test('rsync with exclude patterns', () => { + const excludes = ['--exclude', 'node_modules', '--exclude', '.git']; + const cmd = $({ mirror: false })`rsync -av ${excludes} src/ dest/`; + + expect(cmd.spec.command).toBe( + 'rsync -av --exclude node_modules --exclude .git src/ dest/' + ); + }); + }); + + describe('Edge Cases', () => { + test('array with empty strings', () => { + const args = ['', 'arg', '']; + const cmd = $({ mirror: false })`echo ${args}`; + + expect(cmd.spec.command).toBe("echo '' arg ''"); + }); + + test('array with null-ish values coerced to strings', () => { + const args = [null, undefined, 'valid']; + const cmd = $({ mirror: false })`echo ${args}`; + + // null and undefined become empty strings + expect(cmd.spec.command).toBe("echo '' '' valid"); + }); + + test('nested arrays are flattened by the user (not automatic)', () => { + // Note: nested arrays are not automatically flattened + // Users should flatten them before passing + const nested = [['a', 'b'], 'c']; + const flattened = nested.flat(); + const cmd = $({ mirror: false })`echo ${flattened}`; + + expect(cmd.spec.command).toBe('echo a b c'); + }); + + test('array with numbers', () => { + const args = [1, 2, 3]; + const cmd = $({ mirror: false })`seq ${args}`; + + expect(cmd.spec.command).toBe('seq 1 2 3'); + }); + + test('array with boolean coercion', () => { + const args = [true, false]; + const cmd = $({ mirror: false })`echo ${args}`; + + expect(cmd.spec.command).toBe('echo true false'); + }); + }); +}); + +describe('quote() Function Direct Tests', () => { + test('quote function handles arrays correctly', () => { + const args = ['file.txt', '--flag']; + const result = quote(args); + + expect(result).toBe('file.txt --flag'); + }); + + test('quote function handles nested arrays', () => { + const args = ['safe', 'has space']; + const result = quote(args); + + expect(result).toBe("safe 'has space'"); + }); + + test('quote function handles mixed safe and unsafe elements', () => { + const args = ['safe', '$dangerous', 'also-safe']; + const result = quote(args); + + expect(result).toBe("safe '$dangerous' also-safe"); + }); +}); + +describe('Functional Tests (Command Execution)', () => { + test('array arguments work correctly with real command', async () => { + const args = ['hello', 'world']; + const result = await $({ mirror: false, capture: true })`echo ${args}`; + + expect(result.code).toBe(0); + expect(result.stdout.trim()).toBe('hello world'); + }); + + test('pre-joined array creates single argument (demonstrates bug)', async () => { + // This test shows that pre-joined arrays cause the bug + const args = ['hello', 'world']; + const joined = args.join(' '); + const result = await $({ mirror: false, capture: true })`echo ${joined}`; + + expect(result.code).toBe(0); + // Output is the same in this case because echo just prints + // But the shell received it as a single quoted argument + expect(result.stdout.trim()).toBe('hello world'); + }); + + test('array with spaces handled correctly', async () => { + // Create test files to demonstrate proper argument handling + const result = await $({ mirror: false, capture: true })`echo ${'one two'}`; + + // 'one two' is passed as single argument (quoted) + expect(result.stdout.trim()).toBe('one two'); + }); + + test('array elements become separate arguments for wc', async () => { + // wc -w counts words - this shows that arguments are properly separated + const args = ['a', 'b', 'c']; + + // Create a test that shows echo receives separate args + const result = await $({ + mirror: false, + capture: true, + })`/bin/sh -c 'echo $#'`; + expect(result.code).toBe(0); + // Shell received 0 extra args (just the -c and script) + }); +}); + +describe('Documentation Examples Verification', () => { + test('README Common Pitfalls example - incorrect usage', () => { + const args = ['file.txt', '--public', '--verbose']; + const cmd = $({ mirror: false })`command ${args.join(' ')}`; + + // This demonstrates the bug: one argument instead of three + expect(cmd.spec.command).toBe("command 'file.txt --public --verbose'"); + }); + + test('README Common Pitfalls example - correct usage', () => { + const args = ['file.txt', '--public', '--verbose']; + const cmd = $({ mirror: false })`command ${args}`; + + // Correct: three separate arguments + expect(cmd.spec.command).toBe('command file.txt --public --verbose'); + }); + + test('BEST-PRACTICES.md Pattern 1: Direct array passing', () => { + const args = ['file.txt', '--verbose']; + const cmd = $({ mirror: false })`command ${args}`; + + expect(cmd.spec.command).toBe('command file.txt --verbose'); + }); + + test('BEST-PRACTICES.md Pattern 2: Separate interpolations', () => { + const file = 'file.txt'; + const flags = ['--verbose', '--force']; + const cmd = $({ mirror: false })`command ${file} ${flags}`; + + expect(cmd.spec.command).toBe('command file.txt --verbose --force'); + }); + + test('BEST-PRACTICES.md Pattern 3: Build array dynamically', () => { + const verbose = true; + const allArgs = ['input.txt']; + if (verbose) { + allArgs.push('--verbose'); + } + const cmd = $({ mirror: false })`command ${allArgs}`; + + expect(cmd.spec.command).toBe('command input.txt --verbose'); + }); +}); diff --git a/package.json b/package.json index 694dc76..59e3bb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "command-stream", - "version": "0.9.2", + "version": "0.9.3", "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime", "type": "module", "main": "js/src/$.mjs", diff --git a/rust/BEST-PRACTICES.md b/rust/BEST-PRACTICES.md new file mode 100644 index 0000000..fa86c7b --- /dev/null +++ b/rust/BEST-PRACTICES.md @@ -0,0 +1,382 @@ +# Best Practices for command-stream (Rust) + +This document covers best practices, common patterns, and pitfalls to avoid when using the command-stream Rust library. + +## Table of Contents + +- [Argument Handling with Macros](#argument-handling-with-macros) +- [String Interpolation](#string-interpolation) +- [Security Best Practices](#security-best-practices) +- [Error Handling](#error-handling) +- [Async Patterns](#async-patterns) +- [Common Pitfalls](#common-pitfalls) + +--- + +## Argument Handling with Macros + +### Using the cmd!/s!/sh! Macros + +The command-stream macros (`cmd!`, `s!`, `sh!`, `cs!`) provide safe interpolation similar to JavaScript's `$` template literal: + +```rust +use command_stream::s; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Simple command + let result = s!("echo hello world").await?; + + // With interpolation (automatically quoted) + let name = "world"; + let result = s!("echo hello {}", name).await?; + + // Multiple arguments + let file = "test.txt"; + let flag = "--verbose"; + let result = s!("cat {} {}", file, flag).await?; + + Ok(()) +} +``` + +### Handling Multiple Arguments + +When you have a collection of arguments, handle them correctly: + +```rust +use command_stream::{run, quote}; + +// CORRECT: Use quote::quote_all for multiple arguments +let args = vec!["file.txt", "--public", "--verbose"]; +let quoted_args = quote::quote_all(&args); +let result = run(format!("command {}", quoted_args)).await?; + +// CORRECT: Build command with individual quotes +let args = vec!["file.txt", "--public", "--verbose"]; +let cmd = format!("command {}", + args.iter() + .map(|a| quote::quote(a)) + .collect::>() + .join(" ") +); +let result = run(cmd).await?; +``` + +### Vec/Slice Handling Patterns + +Unlike JavaScript where arrays are handled automatically in template literals, Rust requires explicit handling: + +```rust +use command_stream::quote; + +// Pattern 1: quote_all function +let args = vec!["file.txt", "--verbose"]; +let args_str = quote::quote_all(&args); +// Result: "file.txt --verbose" (each arg properly quoted) + +// Pattern 2: Manual iteration +let args = vec!["file with spaces.txt", "--verbose"]; +let args_str = args.iter() + .map(|a| quote::quote(a)) + .collect::>() + .join(" "); +// Result: "'file with spaces.txt' --verbose" + +// Pattern 3: Format with multiple placeholders +let file = "data.txt"; +let flag1 = "--verbose"; +let flag2 = "--force"; +let result = s!("cmd {} {} {}", file, flag1, flag2).await?; +``` + +--- + +## String Interpolation + +### Safe Interpolation (Default) + +The `quote` function automatically escapes dangerous characters: + +```rust +use command_stream::quote::quote; + +let dangerous = "'; rm -rf /; echo '"; +let safe = quote(dangerous); +// Result: "''\\'' rm -rf /; echo '\\'''" +// Shell will treat this as a literal string +``` + +### When Quoting is Applied + +```rust +use command_stream::quote::{quote, needs_quoting}; + +// Safe strings pass through unchanged +assert_eq!(quote("hello"), "hello"); +assert_eq!(quote("/path/to/file"), "/path/to/file"); + +// Dangerous strings are quoted +assert_eq!(quote("hello world"), "'hello world'"); +assert_eq!(quote("$var"), "'$var'"); + +// Check if quoting is needed +assert!(!needs_quoting("hello")); +assert!(needs_quoting("hello world")); +``` + +--- + +## Security Best Practices + +### Never Trust User Input + +```rust +use command_stream::{run, quote}; + +async fn process_file(user_filename: &str) -> Result<(), Box> { + // CORRECT: Quote user input + let safe_filename = quote::quote(user_filename); + let result = run(format!("cat {}", safe_filename)).await?; + + // ALSO CORRECT: Use macro interpolation + let result = s!("cat {}", user_filename).await?; + + Ok(()) +} +``` + +### Validate Before Execution + +```rust +use command_stream::run; +use std::path::Path; + +async fn delete_file(filename: &str) -> Result<(), Box> { + // Validate: no path traversal + if filename.contains("..") || filename.starts_with('/') { + return Err("Invalid filename".into()); + } + + // Validate: file exists and is a file (not directory) + let path = Path::new(filename); + if !path.is_file() { + return Err("Not a file".into()); + } + + run(format!("rm {}", quote::quote(filename))).await?; + Ok(()) +} +``` + +--- + +## Error Handling + +### Check Results + +```rust +use command_stream::run; + +async fn example() -> Result<(), Box> { + let result = run("ls nonexistent").await?; + + match result.code { + 0 => println!("Success: {}", result.stdout), + 2 => eprintln!("File not found"), + 127 => eprintln!("Command not found"), + code => eprintln!("Unknown error (code {})", code), + } + + Ok(()) +} +``` + +### Using the ? Operator + +```rust +use command_stream::{run, Result}; + +async fn critical_operation() -> Result { + let result = run("important-command").await?; + + if result.code != 0 { + return Err(command_stream::Error::CommandFailed { + code: result.code, + message: result.stderr, + }); + } + + Ok(result.stdout) +} +``` + +--- + +## Async Patterns + +### Basic Async Usage + +```rust +use command_stream::run; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let result = run("echo hello").await?; + println!("{}", result.stdout); + Ok(()) +} +``` + +### Parallel Execution + +```rust +use command_stream::run; +use tokio; + +async fn parallel_tasks() -> Result<(), Box> { + // Run multiple commands in parallel + let (r1, r2, r3) = tokio::join!( + run("task1"), + run("task2"), + run("task3") + ); + + println!("Task 1: {}", r1?.stdout); + println!("Task 2: {}", r2?.stdout); + println!("Task 3: {}", r3?.stdout); + + Ok(()) +} +``` + +### Using ProcessRunner for Control + +```rust +use command_stream::{ProcessRunner, RunOptions}; + +async fn controlled_execution() -> Result<(), Box> { + let options = RunOptions { + capture: true, + mirror: false, // Don't print to terminal + ..Default::default() + }; + + let mut runner = ProcessRunner::new("long-command", options); + runner.start().await?; + let result = runner.run().await?; + + println!("Captured: {}", result.stdout); + Ok(()) +} +``` + +--- + +## Common Pitfalls + +### 1. String Formatting Without Quoting + +**Problem:** Using `format!` without quoting can cause issues with special characters. + +```rust +// WRONG: Spaces break the command +let filename = "my file.txt"; +let cmd = format!("cat {}", filename); +// Result: "cat my file.txt" - interpreted as two args! + +// CORRECT: Quote the value +let cmd = format!("cat {}", quote::quote(filename)); +// Result: "cat 'my file.txt'" - single argument +``` + +### 2. Vec Join Without Proper Quoting + +**Problem:** Joining a Vec without quoting each element. + +```rust +// WRONG: join doesn't quote elements +let args = vec!["file with spaces.txt", "--flag"]; +let cmd = format!("command {}", args.join(" ")); +// Result: "command file with spaces.txt --flag" - BROKEN! + +// CORRECT: Use quote_all +let cmd = format!("command {}", quote::quote_all(&args)); +// Result: "command 'file with spaces.txt' --flag" +``` + +### 3. Forgetting .await + +**Problem:** Async functions return futures that must be awaited. + +```rust +// WRONG: Command never executes +let result = run("echo hello"); // Returns Future, not Result! + +// CORRECT: Await the future +let result = run("echo hello").await?; +``` + +### 4. Not Handling Non-Zero Exit Codes + +**Problem:** Assuming success without checking. + +```rust +// RISKY: May fail silently +let result = run("risky-command").await?; +use_output(&result.stdout); + +// BETTER: Check exit code +let result = run("risky-command").await?; +if result.code == 0 { + use_output(&result.stdout); +} else { + handle_error(&result.stderr); +} +``` + +### 5. Blocking in Async Context + +**Problem:** Using `run_sync` in async context blocks the runtime. + +```rust +// WRONG in async context +async fn bad_example() { + // This blocks the entire runtime thread! + let result = run_sync("slow-command"); +} + +// CORRECT: Use async version +async fn good_example() { + let result = run("slow-command").await; +} +``` + +--- + +## Quick Reference + +### Do's + +- Use `quote::quote()` for individual values +- Use `quote::quote_all()` for Vec/slice of arguments +- Use macro interpolation (`s!`, `cmd!`) for safe templating +- Always `.await` async operations +- Check exit codes for critical operations +- Validate user input before execution + +### Don'ts + +- Never format user input without quoting +- Never use `args.join(" ")` without quoting each element +- Don't forget `.await` on futures +- Don't assume commands succeed +- Don't block async contexts with `run_sync` + +--- + +## See Also + +- [../BEST-PRACTICES.md](../BEST-PRACTICES.md) - JavaScript best practices +- [src/quote.rs](src/quote.rs) - Quote function implementation +- [src/macros.rs](src/macros.rs) - Macro implementations From 326593936f61733d3f5cd021b72c905e0c5678d9 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 23:02:17 +0100 Subject: [PATCH 3/5] chore: Add changeset for documentation update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/array-join-pitfall-documentation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/array-join-pitfall-documentation.md diff --git a/.changeset/array-join-pitfall-documentation.md b/.changeset/array-join-pitfall-documentation.md new file mode 100644 index 0000000..3409eeb --- /dev/null +++ b/.changeset/array-join-pitfall-documentation.md @@ -0,0 +1,11 @@ +--- +'command-stream': patch +--- + +Document Array.join() pitfall and add best practices (fixes #153) + +- Add BEST-PRACTICES.md with detailed usage patterns for arrays, security, and error handling +- Add Common Pitfalls section to README.md explaining the Array.join() issue +- Add docs/case-studies/issue-153/ with real-world bug investigation from hive-mind#1096 +- Add rust/BEST-PRACTICES.md for Rust-specific patterns +- Add 34 tests for array interpolation covering correct usage and anti-patterns From f740d5b590425ae3265356d36be9494eb8822e31 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 23:05:13 +0100 Subject: [PATCH 4/5] Revert "Initial commit with task details" This reverts commit 5f78ead68f1be6628dba4d2b1bb64656dbf255b5. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 57390a3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/command-stream/issues/153 -Your prepared branch: issue-153-58c6ab4753ab -Your prepared working directory: /tmp/gh-issue-solver-1768081753441 - -Proceed. From 64c4fea40c3c6388c71a97b2c4b3b3b728304f3c Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 23:10:59 +0100 Subject: [PATCH 5/5] refactor: Reorganize file structure - move JS docs to js/ folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move BEST-PRACTICES.md to js/BEST-PRACTICES.md - Move docs/case-studies/ to js/docs/case-studies/ - Move docs/IMPLEMENTATION_NOTES.md to js/docs/ - Move docs/SHELL_OPERATORS_IMPLEMENTATION.md to js/docs/ - Update all cross-references in README.md, test files, and docs - Update changeset description to reflect new file locations This separates JavaScript documentation from the root level, keeping all JS-related files in the js/ folder and Rust-related files in rust/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/array-join-pitfall-documentation.md | 5 +++-- README.md | 2 +- BEST-PRACTICES.md => js/BEST-PRACTICES.md | 8 ++++---- {docs => js/docs}/IMPLEMENTATION_NOTES.md | 0 {docs => js/docs}/SHELL_OPERATORS_IMPLEMENTATION.md | 0 {docs => js/docs}/case-studies/issue-144/README.md | 0 .../docs}/case-studies/issue-144/failures-summary.md | 0 {docs => js/docs}/case-studies/issue-146/README.md | 0 {docs => js/docs}/case-studies/issue-153/README.md | 6 +++--- {docs => js/docs}/shell-operators-implementation.md | 0 js/tests/array-interpolation.test.mjs | 4 ++-- rust/BEST-PRACTICES.md | 2 +- 12 files changed, 14 insertions(+), 13 deletions(-) rename BEST-PRACTICES.md => js/BEST-PRACTICES.md (96%) rename {docs => js/docs}/IMPLEMENTATION_NOTES.md (100%) rename {docs => js/docs}/SHELL_OPERATORS_IMPLEMENTATION.md (100%) rename {docs => js/docs}/case-studies/issue-144/README.md (100%) rename {docs => js/docs}/case-studies/issue-144/failures-summary.md (100%) rename {docs => js/docs}/case-studies/issue-146/README.md (100%) rename {docs => js/docs}/case-studies/issue-153/README.md (95%) rename {docs => js/docs}/shell-operators-implementation.md (100%) diff --git a/.changeset/array-join-pitfall-documentation.md b/.changeset/array-join-pitfall-documentation.md index 3409eeb..d1ca09b 100644 --- a/.changeset/array-join-pitfall-documentation.md +++ b/.changeset/array-join-pitfall-documentation.md @@ -4,8 +4,9 @@ Document Array.join() pitfall and add best practices (fixes #153) -- Add BEST-PRACTICES.md with detailed usage patterns for arrays, security, and error handling +- Add js/BEST-PRACTICES.md with detailed usage patterns for arrays, security, and error handling - Add Common Pitfalls section to README.md explaining the Array.join() issue -- Add docs/case-studies/issue-153/ with real-world bug investigation from hive-mind#1096 +- Add js/docs/case-studies/issue-153/ with real-world bug investigation from hive-mind#1096 - Add rust/BEST-PRACTICES.md for Rust-specific patterns - Add 34 tests for array interpolation covering correct usage and anti-patterns +- Reorganize file structure: move JS-related docs to js/ folder, case studies to js/docs/case-studies/ diff --git a/README.md b/README.md index 4e21a86..b7d5b8d 100644 --- a/README.md +++ b/README.md @@ -1395,7 +1395,7 @@ if (verbose) allArgs.push('--verbose'); await $`command ${allArgs}`; ``` -See [BEST-PRACTICES.md](BEST-PRACTICES.md) for more detailed guidance. +See [js/BEST-PRACTICES.md](js/BEST-PRACTICES.md) for more detailed guidance. ## Testing diff --git a/BEST-PRACTICES.md b/js/BEST-PRACTICES.md similarity index 96% rename from BEST-PRACTICES.md rename to js/BEST-PRACTICES.md index d792324..92cdf28 100644 --- a/BEST-PRACTICES.md +++ b/js/BEST-PRACTICES.md @@ -266,7 +266,7 @@ await $`cmd ${args.join(' ')}`; // 1 argument: "file.txt --flag" await $`cmd ${args}`; // 2 arguments: "file.txt", "--flag" ``` -See [Case Study: Issue #153](docs/case-studies/issue-153/README.md) for detailed analysis. +See [Case Study: Issue #153](./docs/case-studies/issue-153/README.md) for detailed analysis. ### 2. Template String Concatenation @@ -371,6 +371,6 @@ if (result.code === 0) { ## See Also -- [README.md](README.md) - Main documentation -- [docs/case-studies/issue-153/README.md](docs/case-studies/issue-153/README.md) - Array.join() pitfall case study -- [js/src/$.quote.mjs](js/src/$.quote.mjs) - Quote function implementation +- [README.md](../README.md) - Main documentation +- [docs/case-studies/issue-153/README.md](./docs/case-studies/issue-153/README.md) - Array.join() pitfall case study +- [src/$.quote.mjs](./src/$.quote.mjs) - Quote function implementation diff --git a/docs/IMPLEMENTATION_NOTES.md b/js/docs/IMPLEMENTATION_NOTES.md similarity index 100% rename from docs/IMPLEMENTATION_NOTES.md rename to js/docs/IMPLEMENTATION_NOTES.md diff --git a/docs/SHELL_OPERATORS_IMPLEMENTATION.md b/js/docs/SHELL_OPERATORS_IMPLEMENTATION.md similarity index 100% rename from docs/SHELL_OPERATORS_IMPLEMENTATION.md rename to js/docs/SHELL_OPERATORS_IMPLEMENTATION.md diff --git a/docs/case-studies/issue-144/README.md b/js/docs/case-studies/issue-144/README.md similarity index 100% rename from docs/case-studies/issue-144/README.md rename to js/docs/case-studies/issue-144/README.md diff --git a/docs/case-studies/issue-144/failures-summary.md b/js/docs/case-studies/issue-144/failures-summary.md similarity index 100% rename from docs/case-studies/issue-144/failures-summary.md rename to js/docs/case-studies/issue-144/failures-summary.md diff --git a/docs/case-studies/issue-146/README.md b/js/docs/case-studies/issue-146/README.md similarity index 100% rename from docs/case-studies/issue-146/README.md rename to js/docs/case-studies/issue-146/README.md diff --git a/docs/case-studies/issue-153/README.md b/js/docs/case-studies/issue-153/README.md similarity index 95% rename from docs/case-studies/issue-153/README.md rename to js/docs/case-studies/issue-153/README.md index 0c862bd..8121916 100644 --- a/docs/case-studies/issue-153/README.md +++ b/js/docs/case-studies/issue-153/README.md @@ -156,9 +156,9 @@ When you see errors like these, suspect the Array.join() pitfall: ## Related Documentation -- [BEST-PRACTICES.md](../../../BEST-PRACTICES.md) - Best practices for command-stream usage -- [README.md](../../../README.md) - Common Pitfalls section -- [$.quote.mjs](../../../js/src/$.quote.mjs) - Quote function implementation +- [BEST-PRACTICES.md](../../BEST-PRACTICES.md) - Best practices for command-stream usage +- [README.md](../../../../README.md) - Common Pitfalls section +- [$.quote.mjs](../../../src/$.quote.mjs) - Quote function implementation ## References diff --git a/docs/shell-operators-implementation.md b/js/docs/shell-operators-implementation.md similarity index 100% rename from docs/shell-operators-implementation.md rename to js/docs/shell-operators-implementation.md diff --git a/js/tests/array-interpolation.test.mjs b/js/tests/array-interpolation.test.mjs index feec505..9da3591 100644 --- a/js/tests/array-interpolation.test.mjs +++ b/js/tests/array-interpolation.test.mjs @@ -7,8 +7,8 @@ * before passing, the entire string becomes a single argument. * * @see https://github.com/link-foundation/command-stream/issues/153 - * @see docs/case-studies/issue-153/README.md - * @see BEST-PRACTICES.md + * @see js/docs/case-studies/issue-153/README.md + * @see js/BEST-PRACTICES.md */ import { $, quote } from '../src/$.mjs'; diff --git a/rust/BEST-PRACTICES.md b/rust/BEST-PRACTICES.md index fa86c7b..22532fb 100644 --- a/rust/BEST-PRACTICES.md +++ b/rust/BEST-PRACTICES.md @@ -377,6 +377,6 @@ async fn good_example() { ## See Also -- [../BEST-PRACTICES.md](../BEST-PRACTICES.md) - JavaScript best practices +- [../js/BEST-PRACTICES.md](../js/BEST-PRACTICES.md) - JavaScript best practices - [src/quote.rs](src/quote.rs) - Quote function implementation - [src/macros.rs](src/macros.rs) - Macro implementations