Coming from cross-spawn
A cross-spawn alternative with a drop-in path: import spawn from bellpull/cross-spawn, graded 68 / 68 by cross-spawn's own test suite — then zero dependencies, and a run() whose result says which binary ran and treats a non-zero exit as a value.
bellpull is a cross-spawn alternative you adopt by changing one import.
bellpull/cross-spawn is cross-spawn's API — the callable default and .sync — and
cross-spawn's own test suite is the grade.
Migrate from cross-spawn in one import
- import spawn from 'cross-spawn';
+ import spawn from 'bellpull/cross-spawn';Everything else stays: spawn(command, args?, options?) returns a ChildProcess,
spawn.sync(command, args?, options?) returns what spawnSync returns, and _parse and
_enoent hang off the default as they do on cross-spawn. A missing command is reported the
way cross-spawn reports it — an ENOENT error event, or result.error from .sync. The
CommonJS form works too, on the Node versions bellpull supports:
- const spawn = require('cross-spawn');
+ const spawn = require('bellpull/cross-spawn');Is bellpull compatible with cross-spawn?
Graded, not claimed. cross-spawn's own suite, vendored at 7.0.6 and unmodified apart from the
import specifier, runs against bellpull/cross-spawn beside a control that runs it against
real cross-spawn:
| passing | rate | |
|---|---|---|
bellpull/cross-spawn | 68 / 68 | 100.0% |
| cross-spawn itself (control) | 68 / 68 | 100.0% |
From Compatibility, which npm run compat:page generates from the
oracle's last run; that page is the authority. The suite runs each case four ways — spawn,
spawn with a forced shell, sync, and sync with a forced shell — so a divergence in one
path cannot hide behind the other three.
What 68 / 68 does not cover: on POSIX cross-spawn is a pass-through, so its suite never
reaches the Windows escaping or the cmd.exe branch. That half is covered by bellpull's own
tests, which round-trip a 37-vector injection corpus through simulations of both Windows
parsers.
What you gain over cross-spawn
bellpull/cross-spawn is deliberately cross-spawn's behaviour — on POSIX, being identical
is the product. The façade changes the dependency tree; the gain is the rest of the package,
which you adopt a call site at a time:
- Zero dependencies. cross-spawn 7.0.6 pulls
path-key,shebang-commandandwhich. - A non-zero exit is a value.
run()frombellpullresolves withresult.ok === falseand the code; it rejects only when no process ran. - It says which binary ran.
result.executableis the path and thePATHentry it came from — the first question of every "works on my machine" investigation. - Bounded by default.
timeoutis finite, the kill is a ladder (SIGTERM, thenSIGKILL), and the output written before the kill is kept. - One result, three readers.
format()for a person,toJson()for--json,toEvent()for an agent stream, all derived from one record.
import { run } from 'bellpull';
const runtime = { platform: process.platform, env: process.env, cwd: process.cwd() };
const result = await run('npm', ['test'], { runtime });
// result.ok, result.code, result.executable — no try/catchWhen to switch from cross-spawn
- You want cross-spawn's Windows handling without its dependency tree.
- Your callers, human or agent, need a subprocess's outcome as text and as JSON.
If you need a raw ChildProcess and nothing more, the one-import swap is the whole
migration. The entry points and the resolvers plugin host are on bellpull.
Coming from execa
An execa alternative with zero dependencies: bellpull's run() treats a non-zero exit as a value, not an exception, says which binary ran, and renders one result as text, --json or an agent event. Not an execa drop-in, and no execa compatibility row exists.
Coming from which
A which alternative with a drop-in path: import which from bellpull/node-which, graded 5 / 5 by node-which's own test suite — then zero dependencies, and a resolver that returns the PATH entry the binary came from.