command-$tream
$treamable commands executor
A modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime.
Features
- π Shell-like by Default: Commands behave exactly like running in terminal (stdoutβstdout, stderrβstderr, stdinβstdin)
- ποΈ Fully Controllable: Override default behavior with options (
mirror,capture,stdin) - π Multiple Usage Patterns: Classic await, async iteration, EventEmitter, .pipe() method, and mixed patterns
- π‘ Real-time Streaming: Process command output as it arrives, not after completion
- π Bun Optimized: Designed for Bun runtime with Node.js compatibility
- β‘ Performance: Memory-efficient streaming prevents large buffer accumulation
- π― Backward Compatible: Existing
await $syntax continues to work + Bun.$.text()method - π‘οΈ Type Safe: Full TypeScript support (coming soon)
- π§ Built-in Commands: 18 essential commands work identically across platforms
Comparison with Other Libraries
| Feature | command-stream | execa | cross-spawn | Bun.$ | ShellJS | zx |
|---|---|---|---|---|---|---|
| π¦ NPM Package | N/A (Built-in) | |||||
| β GitHub Stars | β 2 (Please β us!) | β 7,264 | β 1,149 | β 80,169 (Full Runtime) | β 14,375 | β 44,569 |
| π Monthly Downloads | 893 (New project!) | 381M | 409M | N/A (Built-in) | 35M | 4.2M |
| π Total Downloads | Growing | 6B+ | 5.4B | N/A (Built-in) | 596M | 37M |
| Runtime Support | β Bun + Node.js | β Node.js | β Node.js | π‘ Bun only | β Node.js | β Node.js |
| Template Literals | β
$`cmd` |
β
$`cmd` |
β Function calls | β
$`cmd` |
β Function calls | β
$`cmd` |
| Real-time Streaming | β Live output | π‘ Limited | β Buffer only | β Buffer only | β Buffer only | β Buffer only |
| Synchronous Execution | β
.sync() with events |
β
execaSync |
β
spawnSync |
β No | β Sync by default | β No |
| Async Iteration | β
for await (chunk of $.stream()) |
β No | β No | β No | β No | β No |
| EventEmitter Pattern | β
.on('data', ...) |
π‘ Limited events | π‘ Child process events | β No | β No | β No |
| Mixed Patterns | β Events + await/sync | β No | β No | β No | β No | β No |
| Bun.$ Compatibility | β
.text() method support |
β No | β No | β Native API | β No | β No |
| Shell Injection Protection | β Smart auto-quoting | β Safe by default | β Safe by default | β Built-in | π‘ Manual escaping | β Safe by default |
| Cross-platform | β macOS/Linux/Windows | β Yes | β Specialized cross-platform | β Yes | β Yes | β Yes |
| Performance | β‘ Fast (Bun optimized) | π Moderate | β‘ Fast | β‘ Very fast | π Moderate | π Slow |
| Memory Efficiency | β Streaming prevents buildup | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory | π‘ Buffers in memory |
| Error Handling | β
Configurable (set -e/set +e, non-zero OK by default) |
β Throws on error | β Basic (exit codes) | β Throws on error | β Configurable | β Throws on error |
| Shell Settings | β
set -e/set +e equivalent |
β No | β No | β No | π‘ Limited (set()) |
β No |
| Stdout Support | β Real-time streaming + events | β Node.js streams + interleaved | β Inherited/buffered | β Shell redirection + buffered | β Direct output | β
Readable streams + .pipe.stdout |
| Stderr Support | β Real-time streaming + events | β Streams + interleaved output | β Inherited/buffered | β
Redirection + .quiet() access |
β Error output | β
Readable streams + .pipe.stderr |
| Stdin Support | β string/Buffer/inherit/ignore | β Input/output streams | β Full stdio support | β Pipe operations | π‘ Basic | β Basic stdin |
| Built-in Commands | β 18 commands: cat, ls, mkdir, rm, mv, cp, touch, basename, dirname, seq, yes + all Bun.$ commands | β Uses system | β Uses system | β echo, cd, etc. | β 20+ commands: cat, ls, mkdir, rm, mv, cp, etc. | β Uses system |
| Virtual Commands Engine | β Revolutionary: Register JavaScript functions as shell commands with full pipeline support | β No custom commands | β No custom commands | β No extensibility | β No custom commands | β No custom commands |
| Pipeline/Piping Support | β
Advanced: System + Built-ins + Virtual + Mixed + .pipe() method |
β
Programmatic .pipe() + multi-destination |
β No piping | β Standard shell piping | β
Shell piping + .to() method |
β
Shell piping + .pipe() method |
| Bundle Size | π¦ ~20KB gzipped | π¦ ~400KB+ (packagephobia) | π¦ ~2KB gzipped | π― 0KB (built-in) | π¦ ~15KB gzipped | π¦ ~50KB+ (estimated) |
| Signal Handling | β Advanced SIGINT/SIGTERM forwarding with cleanup | π‘ Basic | β Excellent cross-platform | π‘ Basic | π‘ Basic | π‘ Basic |
| Process Management | β Robust child process lifecycle with proper termination | β Good | β Excellent spawn wrapper | β Basic | π‘ Limited | π‘ Limited |
| Debug Tracing | β Comprehensive VERBOSE logging for CI/debugging | π‘ Limited | β No | β No | π‘ Basic | β No |
| Test Coverage | β 518+ tests, 1165+ assertions | β Excellent | β Good | π‘ Good coverage | β Good | π‘ Good |
| CI Reliability | β Platform-specific handling (macOS/Ubuntu) | β Good | β Excellent | π‘ Basic | β Good | π‘ Basic |
| Documentation | β Comprehensive examples + guides | β Excellent | π‘ Basic | β Good | β Good | π‘ Limited |
| TypeScript | π Coming soon | β Full support | β Built-in | β Built-in | π‘ Community types | β Full support |
| License | β Unlicense (Public Domain) | π‘ MIT | π‘ MIT | π‘ MIT (+ LGPL dependencies) | π‘ BSD-3-Clause | π‘ Apache 2.0 |
π Popularity & Adoption:
- β GitHub Stars: Bun: 80,169 β’ zx: 44,569 β’ ShellJS: 14,375 β’ execa: 7,264 β’ cross-spawn: 1,149 β’ command-stream: 2 β us!
- π Total Downloads: execa: 6B+ β’ cross-spawn: 5.4B β’ ShellJS: 596M β’ zx: 37M β’ command-stream: Growing
- π Monthly Downloads: cross-spawn: 409M β’ execa: 381M β’ ShellJS: 35M β’ zx: 4.2M β’ command-stream: 893 (growing!)
β Help Us Grow! If command-stream's revolutionary virtual commands and advanced streaming capabilities help your project, please star us on GitHub to help the project grow!
Why Choose command-stream?
- π Truly Free: Unlicense (Public Domain) - No restrictions, no attribution required, use however you want
- π Revolutionary Virtual Commands: World's first fully customizable virtual commands engine - register JavaScript functions as shell commands!
- π Advanced Pipeline System: Only library where virtual commands work seamlessly in pipelines with built-ins and system commands
- π§ Built-in Commands: 18 essential commands work identically across all platforms - no system dependencies!
- π‘ Real-time Processing: Only library with true streaming and async iteration
- π Flexible Patterns: Multiple usage patterns (await, events, iteration, mixed)
- π Shell Replacement: Dynamic error handling with
set -e/set +eequivalents for .sh file replacement - β‘ Bun Optimized: Designed for Bun with Node.js fallback compatibility
- πΎ Memory Efficient: Streaming prevents large buffer accumulation
- π‘οΈ Production Ready: 518+ tests, 1165+ assertions with comprehensive coverage including CI reliability
- π― Advanced Signal Handling: Robust SIGINT/SIGTERM forwarding with proper child process cleanup
- π Debug-Friendly: Comprehensive VERBOSE tracing for CI debugging and troubleshooting
Built-in Commands (π NEW!)
command-stream now includes 18 built-in commands that work identically to their bash/sh counterparts, providing true cross-platform shell scripting without system dependencies:
π File System Commands
cat- Read and display file contentsls- List directory contents (supports-l,-a,-A)mkdir- Create directories (supports-precursive)rm- Remove files/directories (supports-r,-f)mv- Move/rename files and directoriescp- Copy files/directories (supports-rrecursive)touch- Create files or update timestamps
π§ Utility Commands
basename- Extract filename from pathdirname- Extract directory from pathseq- Generate number sequencesyes- Output string repeatedly (streaming)
β‘ System Commands
cd- Change directorypwd- Print working directoryecho- Print arguments (supports-n)sleep- Wait for specified timetrue/false- Success/failure commandswhich- Locate commandsexit- Exit with codeenv- Print environment variablestest- File condition testing
β¨ Key Advantages
- π Cross-Platform: Works identically on Windows, macOS, and Linux
- π Performance: No system calls - pure JavaScript execution
- π Pipeline Support: All commands work in pipelines and virtual command chains
- βοΈ Option Aware: Commands respect
cwd,env, and other options - π‘οΈ Safe by Default: Proper error handling and safety checks (e.g.,
rmrequires-rfor directories) - π Bash Compatible: Error messages and behavior match bash/sh exactly
import from 'command-stream';
// All these work without any system dependencies!
await `mkdir -p project/src`;
await `touch project/src/index.js`;
await `echo "console.log('Hello!');" > project/src/index.js`;
await `ls -la project/src`;
await `cat project/src/index.js`;
await `cp -r project project-backup`;
await `rm -r project-backup`;
// Mix built-ins with pipelines and virtual commands
await `seq 1 5 | cat > numbers.txt`;
await `basename /path/to/file.txt .txt`; // β "file"
Installation
# Using npm
# Using bun
Smart Quoting & Security
Command-stream provides intelligent auto-quoting to protect against shell injection while avoiding unnecessary quotes for safe strings:
Smart Quoting Behavior
import from 'command-stream';
// Safe strings are NOT quoted (performance optimization)
await `echo ${name}`; // name = "hello" β echo hello
await `${cmd} --version`; // cmd = "/usr/bin/node" β /usr/bin/node --version
// Dangerous strings are automatically quoted for safety
await `echo ${userInput}`; // userInput = "test; rm -rf /" β echo 'test; rm -rf /'
await `echo ${pathWithSpaces}`; // pathWithSpaces = "/my path/file" β echo '/my path/file'
// Special characters that trigger auto-quoting:
// Spaces, $, ;, |, &, >, <, `, *, ?, [, ], {, }, (, ), !, #, and others
// User-provided quotes are preserved
const quotedPath = "'/path with spaces/file'";
await `cat ${quotedPath}`; // β cat '/path with spaces/file' (no double-quoting!)
const doubleQuoted = '"/path with spaces/file"';
await `cat ${doubleQuoted}`; // β cat '"/path with spaces/file"' (preserves intent)
Shell Injection Protection
All interpolated values are automatically secured:
// β
SAFE - All these injection attempts are neutralized
const dangerous = "'; rm -rf /; echo '";
await `echo ${dangerous}`; // β echo ''\'' rm -rf /; echo '\'''
const cmdSubstitution = '$(whoami)';
await `echo ${cmdSubstitution}`; // β echo '$(whoami)' (literal text, not executed)
const varExpansion = '$HOME';
await `echo ${varExpansion}`; // β echo '$HOME' (literal text, not expanded)
// β
SAFE - Even complex injection attempts
const complex = '`cat /etc/passwd`';
await `echo ${complex}`; // β echo '`cat /etc/passwd`' (literal text)
Disabling Auto-Escape (Advanced)
β οΈ WARNING: Use with extreme caution! Only use raw() with trusted input to prevent shell injection attacks.
For advanced use cases where you need to use command strings directly without auto-escaping, use the raw() function:
import from 'command-stream';
// β οΈ DANGEROUS - Bypasses all safety checks
const userCommand = 'echo "hello" && ls -la';
await `${}`; // β Executes: echo "hello" && ls -la
// β
Safe use case: Trusted command templates
const trustedCommand = 'git log --oneline --graph --all';
await `${}`;
// β
Combining raw with safe interpolation
const branch = 'main'; // User input - will be auto-quoted
await `${} ${branch}`;
// β git log --oneline 'main' (raw part unescaped, branch safely quoted)
// π― Use case: Pre-built command strings from configuration
const config = ;
await `${}`;
// β οΈ NEVER use raw() with user input
const userInput = req..; // β DANGEROUS!
await `${}`; // β Shell injection vulnerability!
// β
Instead, use normal interpolation for user input
await `echo ${userInput}`; // β
Safe - auto-escaped
When to use raw():
- β Trusted command templates from your codebase
- β Configuration files you control
- β Hardcoded command strings
- β Complex shell operators that need to be preserved
When NOT to use raw():
- β User input (form fields, API parameters, CLI arguments)
- β External data (database, API responses, files)
- β Any untrusted source
- β When you're unsure - use normal interpolation instead
Usage Patterns
Classic Await (Backward Compatible)
import from 'command-stream';
const result = await `ls -la`;
console.log;
console.log; // exit code
Custom Options with $({ options }) Syntax (NEW!)
import from 'command-stream';
// Create a $ with custom options
const $silent = ;
const result = await `echo "quiet operation"`;
// Options for stdin handling
const $withInput = ;
await `cat`; // Pipes the input to cat
// Custom environment variables
const $withEnv = ;
await `printenv MY_VAR`; // Prints: value
// Custom working directory
const $inTmp = ;
await `pwd`; // Prints: /tmp
// Interactive mode for TTY commands (requires TTY environment)
const $interactive = ;
await `vim myfile.txt`; // Full TTY access for editor
await `less README.md`; // Proper pager interaction
// Combine multiple options
const $custom = ;
await `cat > output.txt`; // Writes to /tmp/output.txt silently
// Reusable configurations
const $prod = ;
await `npm start`;
await `npm test`;
Execution Control (NEW!)
import from 'command-stream';
// Commands don't auto-start when created
const cmd = `echo "hello"`;
// Three ways to start execution:
// 1. Explicit start with options
cmd.; // Default async mode
cmd.; // Explicitly async
cmd.; // Synchronous execution
// 2. Convenience methods
cmd.; // Same as start({ mode: 'async' })
cmd.; // Same as start({ mode: 'sync' })
// 3. Auto-start by awaiting (always async)
await cmd; // Auto-starts in async mode
// Event handlers can be attached before starting
const process = `long-command`
.
.;
// Start whenever you're ready
process.;
Synchronous Execution
import from 'command-stream';
// Use .sync() for blocking execution
const result = `echo "hello"`.;
console.log; // "hello\n"
// Events still work but are batched after completion
`echo "world"`..;
Async Iteration (Real-time Streaming)
import from 'command-stream';
EventEmitter Pattern (Event-driven)
import from 'command-stream';
// Attach event handlers then start execution
`command`
.
.
.
.
.; // Explicitly start the command
// Or auto-start by awaiting
const cmd = `another-command`.;
await cmd; // Auto-starts in async mode
Mixed Pattern (Best of Both Worlds)
import from 'command-stream';
// Async mode - events fire in real-time
const process = `streaming-command`;
process.;
const result = await process;
console.log;
// Sync mode - events fire after completion (batched)
const syncCmd = `another-command`;
syncCmd.;
const syncResult = syncCmd.;
Streaming Interfaces
Advanced streaming interfaces for fine-grained process control:
import from 'command-stream';
// π― STDIN CONTROL: Send data to interactive commands (real-time)
const grepCmd = `grep "important"`;
const stdin = await grepCmd..; // Available immediately
stdin.;
stdin.;
stdin.;
stdin.;
const result = await grepCmd;
console.log; // "important message\n"
// π§ BINARY DATA: Access raw buffers (after command finishes)
const cmd = `echo "Hello World"`;
const buffer = await cmd..; // Complete snapshot
console.log; // 12
// π TEXT DATA: Access as strings (after command finishes)
const textCmd = `echo "Hello World"`;
const text = await textCmd..; // Complete snapshot
console.log; // "Hello World"
// β‘ PROCESS CONTROL: Kill commands that ignore stdin
const pingCmd = `ping google.com`;
// Some commands ignore stdin input
const pingStdin = await pingCmd..;
if
// Use kill() for forceful termination
setTimeout;
const pingResult = await pingCmd;
console.log; // 143 (SIGTERM)
// π MIXED STDOUT/STDERR: Handle both streams (complete snapshots)
const mixedCmd = `sh -c 'echo "out" && echo "err" >&2'`;
const = await Promise.;
console.log; // "out"
console.log; // "err"
// πββοΈ AUTO-START: Streams auto-start processes when accessed
const cmd = `echo "test"`;
console.log; // false
const output = await cmd..; // Auto-starts, immediate access
console.log; // true
// π BACKWARD COMPATIBLE: Traditional await still works
const traditional = await `echo "still works"`;
console.log; // "still works\n"
Key Features:
command.streams.stdin/stdout/stderr- Direct access to Node.js streamscommand.buffers.stdin/stdout/stderr- Binary data as Buffer objectscommand.strings.stdin/stdout/stderr- Text data as stringscommand.kill()- Forceful process termination- Auto-start behavior: Process starts only when accessing stream properties
- Perfect for: Interactive commands (grep, sort, bc), data processing, real-time control
- Network commands (ping, wget) ignore stdin β Use
kill()method instead
π Streams vs Buffers/Strings:
streams.*- Available immediately when command starts, for real-time interactionbuffers.*&strings.*- Complete snapshots available only after command finishes
Shell Replacement (.sh β .mjs)
Replace bash scripts with JavaScript while keeping shell semantics:
import from 'command-stream';
// set -e equivalent: exit on any error
shell.;
await `mkdir -p build`;
await `npm run build`;
// set +e equivalent: allow errors (like bash)
shell.;
const cleanup = await `rm -rf temp`; // Won't throw if fails
// set -e again for critical operations
shell.;
await `cp -r build/* deploy/`;
// Other bash-like settings
shell.; // set -v: print commands
shell.; // set -x: trace execution
// Or use the bash-style API
; // set -e
; // set +e
; // set -x
; // Long form also supported
Cross-Platform File Operations (Built-in Commands)
Replace system-dependent operations with built-in commands that work identically everywhere:
import from 'command-stream';
// File system operations work on Windows, macOS, and Linux identically
await `mkdir -p project/src project/tests`;
await `touch project/src/index.js project/tests/test.js`;
// List files with details
const files = await `ls -la project/src`;
console.log;
// Copy and move operations
await `cp project/src/index.js project/src/backup.js`;
await `mv project/src/backup.js project/backup.js`;
// File content operations
await `echo "export default 'Hello World';" > project/src/index.js`;
const content = await `cat project/src/index.js`;
console.log;
// Path operations
const filename = await `basename project/src/index.js .js`; // β "index"
const directory = await `dirname project/src/index.js`; // β "project/src"
// Generate sequences and process them
await `seq 1 10 | cat > numbers.txt`;
const numbers = await `cat numbers.txt`;
// Cleanup
await `rm -r project numbers.txt`;
Virtual Commands (Extensible Shell)
Create custom commands that work seamlessly alongside built-ins:
import from 'command-stream';
// Register a custom command
;
// Use it like any other command
await `greet Alice`; // β "Hello, Alice!"
await `echo "Bob" | greet`; // β "Hello, Bob!"
// Streaming virtual commands with async generators
;
// Use in pipelines with built-ins
await `countdown 3 | cat > countdown.txt`;
// Virtual commands work in all patterns
// Management functions
console.log; // List all registered commands
; // Remove custom commands
π₯ Why Virtual Commands Are Revolutionary
No other shell library offers this level of extensibility:
- π« Bun.$: Fixed set of built-in commands, no extensibility API
- π« execa: Transform/pipeline system, but no custom commands
- π« zx: JavaScript functions only, no shell command integration
command-stream breaks the barrier between JavaScript functions and shell commands:
// β Other libraries: Choose JavaScript OR shell
await ; // execa: separate processes
await `node script.js`; // zx: shell commands only
// β
command-stream: JavaScript functions AS shell commands
;
await `deploy production`; // JavaScript function as shell command!
await `deploy staging | tee log.txt`; // Works in pipelines!
Unique capabilities:
- Seamless Integration: Virtual commands work exactly like built-ins
- Pipeline Support: Full stdin/stdout passing between virtual and system commands
- Streaming: Async generators for real-time output
- Dynamic Registration: Add/remove commands at runtime
- Option Awareness: Virtual commands respect
cwd,env, etc.
π Advanced Pipeline Support
command-stream offers the most advanced piping system in the JavaScript ecosystem:
Shell-Style Piping (Traditional)
import from 'command-stream';
// β
Standard shell piping (like all libraries)
await `echo "hello world" | wc -w`; // β "2"
// β
Built-in to built-in piping
await `seq 1 5 | cat > numbers.txt`;
// β
System to built-in piping
await `git log --oneline | head -n 5`;
// π UNIQUE: Virtual command piping
;
;
// β
Built-in to virtual piping
await `echo "hello" | uppercase`; // β "HELLO"
// β
Virtual to virtual piping
await `echo "hello" | uppercase | reverse`; // β "OLLEH"
// β
Mixed pipelines (system + built-in + virtual)
await `git log --oneline | head -n 3 | uppercase | cat > LOG.txt`;
// β
Complex multi-stage pipelines
await `find . -name "*.js" | head -n 10 | basename | sort | uniq`;
π Programmatic .pipe() Method (NEW!)
World's first shell library with full .pipe() method support for virtual commands:
import from 'command-stream';
// β
Basic programmatic piping
const result = await `echo "hello"`.;
// π Virtual command chaining
;
;
// β
Chain virtual commands with .pipe()
const result = await `echo "Hello"`
.
.;
// β "[PROCESSED] Hello !!!"
// β
Mix with built-in commands
const fileData = await `cat large-file.txt`
.
.;
// β
Error handling in pipelines
try catch
// β
Complex data processing
;
;
// Real-world API processing pipeline
const userName = await `curl -s https://api.github.com/users/octocat`
.
.;
// β "The Octocat"
// Cleanup
;
;
;
;
π How We Compare
| Library | Pipeline Types | Custom Commands in Pipes | .pipe() Method |
Real-time Streaming |
|---|---|---|---|---|
| command-stream | β System + Built-ins + Virtual + Mixed | β Full support | β Full virtual command support | β Yes |
| Bun.$ | β System + Built-ins | β No custom commands | β No .pipe() method |
β No |
| execa | β
Programmatic .pipe() |
β No shell integration | β Basic process piping | π‘ Limited |
| zx | β
Shell piping + .pipe() |
β No custom commands | β Stream piping only | β No |
π― Unique Advantages:
- Virtual commands work seamlessly in both shell pipes AND
.pipe()method - no other library can do this - Mixed pipeline types - combine system, built-in, and virtual commands freely in both syntaxes
- Real-time streaming through virtual command pipelines
- Full stdin/stdout passing between all command types
- Dual piping syntax - use shell
|OR programmatic.pipe()interchangeably
Default Behavior: Shell-like with Programmatic Control
command-stream behaves exactly like running commands in your shell by default:
import from 'command-stream';
// This command will:
// 1. Print "Hello" to your terminal (stdoutβstdout)
// 2. Print "Error!" to your terminal (stderrβstderr)
// 3. Capture both outputs for programmatic access
const result = await `sh -c "echo 'Hello'; echo 'Error!' >&2"`;
console.log; // "Hello\n"
console.log; // "Error!\n"
console.log; // 0
Key Default Options:
mirror: true- Live output to terminal (like shell)capture: true- Capture output for later use (unlike shell)stdin: 'inherit'- Inherit stdin from parent process
Fully Controllable:
import from 'command-stream';
// Disable terminal output but still capture
const result = await ;
// Custom stdin input
const custom = await ;
// Create custom $ with different defaults
const quiet$ = ;
await `echo "silent"`; // Won't print to terminal
// Disable both mirroring and capturing for performance
await ;
This gives you the best of both worlds: shell-like behavior by default, but with full programmatic control and real-time streaming capabilities.
Real-world Examples
Log File Streaming with Session ID Extraction
import from 'command-stream';
import from 'fs';
let sessionId = null;
let logFile = null;
Progress Monitoring
import from 'command-stream';
let progress = 0;
`download-large-file`
.
.;
API Reference
ProcessRunner Class
The enhanced $ function returns a ProcessRunner instance that extends EventEmitter.
Events
data: Emitted for each chunk with{type: 'stdout'|'stderr', data: Buffer}stdout: Emitted for stdout chunks (Buffer)stderr: Emitted for stderr chunks (Buffer)end: Emitted when process completes with final result objectexit: Emitted with exit code
Methods
start(options): Explicitly start command executionoptions.mode:'async'(default) or'sync'- execution mode
async(): Shortcut forstart({ mode: 'async' })- start async executionsync(): Shortcut forstart({ mode: 'sync' })- execute synchronously (blocks until completion)stream(): Returns an async iterator for real-time chunk processingpipe(destination): Programmatically pipe output to another command (returns new ProcessRunner)then(),catch(),finally(): Promise interface for await support (auto-starts in async mode)
Properties
stdout: Direct access to child process stdout streamstderr: Direct access to child process stderr streamstdin: Direct access to child process stdin stream
Default Options
By default, command-stream behaves like running commands in the shell:
Option Details:
mirror: boolean- Whether to pipe output to terminal in real-timecapture: boolean- Whether to capture output in result objectstdin: 'inherit' | 'ignore' | string | Buffer- How to handle stdininteractive: boolean- Enable TTY forwarding for interactive commands (requiresstdin: 'inherit'and TTY environment)cwd: string- Working directory for commandenv: object- Environment variables
Override defaults:
- Use
$({ options })syntax for one-off configurations with template literals - Use
sh(command, options)for one-off overrides with string commands - Use
create(defaultOptions)to create custom$with different defaults
Shell Settings API
Control shell behavior like bash set/unset commands:
Functions
shell.errexit(boolean): Enable/disable exit-on-error (likeset Β±e)shell.verbose(boolean): Enable/disable command printing (likeset Β±v)shell.xtrace(boolean): Enable/disable execution tracing (likeset Β±x)set(option): Enable shell option ('e','v','x', or long names)unset(option): Disable shell optionshell.settings(): Get current settings object
Error Handling Modes
import from 'command-stream';
// β
Default behavior: Commands don't throw on non-zero exit
const result = await `ls nonexistent-file`; // Won't throw
console.log; // β 2 (non-zero, but no exception)
// β
Enable errexit: Commands throw on non-zero exit
shell.;
try catch
// β
Disable errexit: Back to non-throwing behavior
shell.;
await `ls nonexistent-file`; // Won't throw, returns result with code 2
// β
One-time override without changing global settings
try catch
Virtual Commands API
Control and extend the command system with custom JavaScript functions:
Functions
register(name, handler): Register a virtual commandname: Command name (string)handler: Function or async generator(args, stdin, options) => result
unregister(name): Remove a virtual commandlistCommands(): Get array of all registered command namesenableVirtualCommands(): Enable virtual command processingdisableVirtualCommands(): Disable virtual commands (use system commands only)
Advanced Virtual Command Features
import from 'command-stream';
// β
Cancellation support with AbortController
;
// β
Access to all process options
// All original options (built-in + custom) are available in the 'options' object
// Common options like cwd, env are also available at top level for convenience
// Runtime additions: isCancelled function, abortSignal
;
// β
Error handling and non-zero exit codes
;
// β
Example: User options flow through to virtual commands
;
// Usage example showing options passed to virtual command:
const result = await `show-options`;
console.log; // Output: Custom: hello world, CWD: /tmp
Handler Function Signature
// Regular async function
// Async generator for streaming
Utility Functions API
Control how values are interpolated into commands:
Functions
quote(value): Manually quote a value using the same smart quoting logic as auto-interpolationraw(value): β οΈ Dangerous! Bypass auto-escaping to use command strings directly (see Disabling Auto-Escape)
quote() - Manual Quoting
Apply the same smart quoting logic manually:
import from 'command-stream';
const path = '/path with spaces/file.txt';
const quoted = ;
console.log; // β '/path with spaces/file.txt'
// Use case: Pre-process values before interpolation
const args = .;
// β ["'hello world'", 'test']
raw() - Disable Auto-Escape
β οΈ WARNING: Only use with trusted input! See Disabling Auto-Escape section for detailed documentation and security considerations.
import from 'command-stream';
// Bypass auto-escaping for trusted command strings
const trustedCommand = 'echo "hello" && ls -la';
await `${}`;
// β Executes: echo "hello" && ls -la (without escaping)
// β οΈ NEVER use with untrusted input - shell injection risk!
Built-in Commands
18 cross-platform commands that work identically everywhere:
File System: cat, ls, mkdir, rm, mv, cp, touch
Utilities: basename, dirname, seq, yes
System: cd, pwd, echo, sleep, true, false, which, exit, env, test
All built-in commands support:
- Standard flags (e.g.,
ls -la,mkdir -p,rm -rf) - Pipeline operations
- Option awareness (
cwd,env, etc.) - Bash-compatible error messages and exit codes
Supported Options
'e'/'errexit': Exit on command failure'v'/'verbose': Print commands before execution'x'/'xtrace': Trace command execution with+prefix'u'/'nounset': Error on undefined variables (planned)'pipefail': Pipe failure detection (planned)
Result Object
.text() Method (Bun.$ Compatibility)
For compatibility with Bun.$, all result objects include an async .text() method:
import from 'command-stream';
// Both sync and async execution support .text()
const result1 = await `echo "hello world"`;
const text1 = await result1.; // "hello world\n"
const result2 = `echo "sync example"`.;
const text2 = await result2.; // "sync example\n"
// .text() is equivalent to accessing .stdout
.;
// Works with built-in commands
const result3 = await `seq 1 3`;
const text3 = await result3.; // "1\n2\n3\n"
// Works with .pipe() method
const result4 = await `echo "pipe test"`.;
const text4 = await result4.; // "pipe test\n"
Signal Handling (CTRL+C Support)
The library provides advanced CTRL+C handling that properly manages signals across different scenarios:
How It Works
- Smart Signal Forwarding: CTRL+C is forwarded only when child processes are active
- User Handler Preservation: When no children are running, your custom SIGINT handlers work normally
- Process Groups: Child processes use detached spawning for proper signal isolation
- TTY Mode Support: Raw TTY mode is properly managed and restored on interruption
- Graceful Termination: Uses SIGTERM β SIGKILL escalation for robust process cleanup
- Exit Code Standards: Proper signal exit codes (130 for SIGINT, 143 for SIGTERM)
Advanced Signal Behavior
// β
Smart signal handling - only interferes when necessary
import from 'command-stream';
// Case 1: No children active - your handlers work normally
process.;
// Press CTRL+C β Your handler runs, exits with code 42
// Case 2: Children active - automatic forwarding
await `ping 8.8.8.8`; // Press CTRL+C β Forwards to ping, exits with code 130
// Case 3: Multiple processes - all interrupted
await Promise.; // Press CTRL+C β All processes terminated, exits with code 130
Examples
// Long-running command that can be interrupted with CTRL+C
try catch
// Multiple concurrent processes - CTRL+C stops all
try catch
// Works with streaming patterns too
try catch
Signal Handling Behavior
- π― Smart Detection: Only forwards CTRL+C when child processes are active
- π‘οΈ Non-Interference: Preserves user SIGINT handlers when no children running
- β‘ Interactive Commands: Use
interactive: trueoption for commands likevim,less,topto enable proper TTY forwarding and signal handling - π Process Groups: Detached spawning ensures proper signal isolation
- π§Ή TTY Cleanup: Raw terminal mode properly restored on interruption
- π Standard Exit Codes:
130- SIGINT interruption (CTRL+C)143- SIGTERM termination (programmatic kill)137- SIGKILL force termination
Command Resolution Priority
// Understanding how commands are resolved:
// 1. Virtual Commands (highest priority)
;
await `echo test`; // β "virtual!"
// 2. Built-in Commands (if no virtual match)
;
await `echo test`; // β Uses built-in echo
// 3. System Commands (if no built-in/virtual match)
await `unknown-command`; // β Uses system PATH lookup
// 4. Virtual Bypass (special case)
await `sleep 1`; // Bypasses virtual sleep, uses system sleep
Execution Patterns Deep Dive
When to Use Different Patterns
import from 'command-stream';
// β
Use await for simple command execution
const result = await `ls -la`;
// β
Use .sync() when you need blocking execution with events
const syncCmd = `build-script`
.
.; // Events fire after completion
// β
Use .start() for non-blocking execution with real-time events
const asyncCmd = `long-running-server`
.
.; // Events fire in real-time
// β
Use .stream() for processing large outputs efficiently
// Memory efficient - processes chunks as they arrive
// β
Use EventEmitter pattern for complex workflows
`deployment-script`
.
.
.
.;
Performance Considerations
// π Memory Efficient: For large outputs, use streaming
// π Memory Inefficient: Buffers entire output in memory
const result = await `cat huge-file.log`;
; // Loads everything into memory
// β‘ Fastest: Sync execution for small, quick commands
const quickResult = `pwd`.;
// π Best for UX: Async with events for long-running commands
`npm install`..;
Common Pitfalls
Array Argument Handling
When passing multiple arguments, pass the array directly - never use .join(' ') before interpolation:
import from 'command-stream';
// WRONG - entire string becomes ONE argument
const args = ;
await `command ${args.}`;
// 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:
if
But when you call .join(' ') first:
- The array becomes a string:
"file.txt --public --verbose" - Template receives a string, not an array
- The entire string gets quoted as one argument
- Command receives one argument containing spaces
Recommended Patterns
// Pattern 1: Direct array passing
const args = ;
await `command ${args}`;
// Pattern 2: Separate interpolations
const file = 'file.txt';
const flags = ;
await `command ${file} ${flags}`;
// Pattern 3: Build array dynamically
const allArgs = ;
if allArgs.;
await `command ${allArgs}`;
See js/BEST-PRACTICES.md for more detailed guidance.
Testing
# Run comprehensive test suite (518+ tests)
# Run tests with coverage report
# Run specific test categories
Requirements
- Bun: >= 1.0.0 (primary runtime)
- Node.js: >= 20.0.0 (compatibility support)
Roadmap
π Coming Soon
- TypeScript Support: Full .d.ts definitions and type safety
- Enhanced Shell Options:
set -u(nounset) andset -o pipefailsupport - More Built-in Commands: Additional cross-platform utilities
π‘ Planned Features
- Performance Optimizations: Further Bun runtime optimizations
- Advanced Error Handling: Enhanced error context and debugging
- Plugin System: Extensible architecture for custom integrations
Contributing
We welcome contributions! Since command-stream is public domain software, your contributions will also be released into the public domain.
π Getting Started
π Development Guidelines
- All features must have comprehensive tests
- Built-in commands should match bash/sh behavior exactly
- Maintain cross-platform compatibility (Windows, macOS, Linux)
- Follow the existing code style and patterns
π§ͺ Running Tests
License - Our Biggest Advantage
The Unlicense (Public Domain)
Unlike other shell utilities that require attribution (MIT, Apache 2.0), command-stream is released into the public domain. This means:
- β No attribution required - Use it without crediting anyone
- β No license files to include - Simplify your distribution
- β No restrictions - Modify, sell, embed, whatever you want
- β No legal concerns - It's as free as code can be
- β Corporate friendly - No license compliance overhead
This makes command-stream ideal for:
- Commercial products where license attribution is inconvenient
- Embedded systems where every byte counts
- Educational materials that can be freely shared
- Internal tools without legal review requirements
"This is free and unencumbered software released into the public domain."