Skip to content

Node Shell

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.

  • Tagged template API — Run commands with $`echo hello` syntax, just like Bun’s shell or Google’s zx
  • Real-time line streaming.lines() returns an AsyncIterable<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-parser under 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 like cat or grep
  • 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 compatibilityShellOutput implements Bun’s ShellOutput interface, making it a drop-in for Node.js environments

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 output
const files = await $`ls -la`.cwd("/tmp").text();
// Stream lines in real time
for await (const line of $`tail -f /var/log/syslog`.lines()) {
console.log(`[live] ${line}`);
}
// Parse JSON from a command
const pkg = await $`cat package.json`.quiet().json();
console.log(pkg.name); // "@jadujoel/node-shell"
// Write to stdin
const 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.

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 grep filtering
  • 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 both ShellPromise and resolved ShellOutput
LanguageTypeScript (strict mode)
PlatformNode.js
Runtimechild_process.spawn
Dependencyjsonc-parser for lenient JSON parsing
Testingnode:test + node:assert, c8 for coverage
LintingBiome
CI/CDGitHub Actions — test + auto-publish to npm
Package@jadujoel/node-shell on npm

The source code is available on the project’s GitHub repository.