Node Shell
Overview
Section titled “Overview”Node Shell is a lightweight shell execution library for Node.js that brings the ergonomics of Bun’s built-in $ shell to any Node.js environment. It provides a tagged template function for running shell commands with a fluent, chainable API — parse output as text, JSON, binary, or stream it line-by-line in real time.
The library wraps child_process.spawn with a developer-friendly interface that handles the tedious parts: buffering stdout/stderr, managing exit codes, piping stdin, and converting output to the format you actually need. One dependency (jsonc-parser), strict TypeScript, and full Bun ShellOutput interface compatibility.
Features
Section titled “Features”- Tagged template API — Run commands with
$`echo hello`syntax, just like Bun’s shell or Google’s zx - Real-time line streaming —
.lines()returns anAsyncIterable<string>that yields stdout line-by-line as the process runs, using an internal buffer to handle partial chunks correctly - Multiple output formats —
.text(),.json(),.arrayBuffer(),.blob(),.bytes()— pick the format that fits your use case - Lenient JSON parsing — Uses
jsonc-parserunder the hood, so.json()handles trailing commas and comments without blowing up - Stdin access — Write to a running process via
proc.stdin, useful for piping data into commands likecatorgrep - Fluent error control — Throws on non-zero exit codes by default; chain
.nothrow()to get the result instead, or use.throws(false)for explicit control - Environment & execution control —
.env(),.cwd(),.timeout(),.shell()— inject variables, change directories, set time limits, or swap the shell binary - Bun ShellOutput compatibility —
ShellOutputimplements Bun’sShellOutputinterface, making it a drop-in for Node.js environments
How It Works
Section titled “How It Works”The core is two classes: ShellPromise (the chainable builder) and ShellOutput (the result). When you write $`ls -l`, the template tag constructs a ShellPromise. Configuration methods like .quiet() and .env() mutate internal state before the process spawns. The actual child_process.spawn call is deferred until you await or call an output method:
import { $ } from '@jadujoel/node-shell';
// Simple text outputconst files = await $`ls -la`.cwd("/tmp").text();
// Stream lines in real timefor await (const line of $`tail -f /var/log/syslog`.lines()) { console.log(`[live] ${line}`);}
// Parse JSON from a commandconst pkg = await $`cat package.json`.quiet().json();console.log(pkg.name); // "@jadujoel/node-shell"
// Write to stdinconst proc = $`grep hello`.nothrow().quiet();proc.stdin.write("hello world\n");proc.stdin.write("goodbye world\n");proc.stdin.end();const result = await proc;console.log(result.text()); // "hello world\n"The ShellPromise implements .then(), .catch(), and .finally() directly, so it works seamlessly with await and promise chains without needing an explicit .promise accessor.
Testing
Section titled “Testing”The test suite includes 25+ tests using Node’s built-in test runner (node:test + node:assert), with code coverage via c8. Tests cover:
- Basic command execution and environment variable injection
- Error handling paths — both throwing and non-throwing modes
- Stdin interaction — single writes, multiple writes, empty input, large payloads (1000+ lines), and
grepfiltering - Output format conversions — text, JSON, ArrayBuffer, Blob, Uint8Array
- Edge cases — timeouts, custom shells, non-existent shells (ENOENT), disconnect events
- Promise protocol —
.then(),.catch(),.finally()on bothShellPromiseand resolvedShellOutput
Tech Stack
Section titled “Tech Stack”| Language | TypeScript (strict mode) |
| Platform | Node.js |
| Runtime | child_process.spawn |
| Dependency | jsonc-parser for lenient JSON parsing |
| Testing | node:test + node:assert, c8 for coverage |
| Linting | Biome |
| CI/CD | GitHub Actions — test + auto-publish to npm |
| Package | @jadujoel/node-shell on npm |
Source Code
Section titled “Source Code”The source code is available on the project’s GitHub repository.