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: execa's own suite grades it 0 / 1179, and that zero is the design.
bellpull is an execa alternative for running subprocesses: executable resolution, a bounded run and a structured result every caller can read. It is not an execa drop-in — read this page before you plan a one-line swap.
Migrate from execa: not one import
There is no bellpull/execa. execa's streaming API and its $`cmd` template form are a
different product and are out of scope, so moving is a rewrite of each call site, not of the
import line:
- import { execa } from 'execa';
- const { stdout } = await execa('git', ['rev-parse', 'HEAD']);
+ import { run } from 'bellpull';
+ const runtime = { platform: process.platform, env: process.env, cwd: process.cwd() };
+ const result = await run('git', ['rev-parse', 'HEAD'], { runtime });
+ if (result.ok) console.log(result.stdout);The one-import path bellpull does ship is for cross-spawn, the other spawner in this layer:
- import spawn from 'cross-spawn';
+ import spawn from 'bellpull/cross-spawn';Same callable default, same .sync.
Is bellpull compatible with execa?
No — and that is measured, not just stated. The
Compatibility page, generated by npm run compat:page from the
oracle's last run, grades execa's own suite against bellpull and publishes the result as a
ceiling, not as a drop-in on its way to 100%:
| passing | rate | |
|---|---|---|
bellpull against execa 10.2.1's suite | 0 / 1360 | 0.0% |
| execa itself (control) | 1360 / 1360 | 100.0% |
bellpull/cross-spawn against cross-spawn's suite | 68 / 68 | 100.0% |
| cross-spawn itself (control) | 68 / 68 | 100.0% |
The 1360 are execa's arguments/, methods/ and return/ directories — how a command is
built, the entry points, and the result and error a caller reads — less one case,
write to fast-exit subprocess, which runs a compiled executable execa ships beside its tests.
The oracle never vendors a binary, so that case is excluded for execa and bellpull alike, and
the count is the same on every machine. Every file imports execa,
which bellpull does not export, so every one fails before a case runs; the other nine
directories are vendored too and read the same zero, and are left out of the recurring grade
only because the whole suite takes eleven minutes to run.
bellpull's README records what that 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
behaviour is covered by bellpull's own tests.
What you gain over execa
- A non-zero exit is a value. execa throws when a child exits non-zero, so every caller
wraps every call.
run()resolves withresult.ok === falseand the code; it rejects only when no process ran — the executable did not resolve, or the spawn failed. - 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, so a--jsonflag cannot report something the human output did not. - No shell, ever, to solve a Windows problem.
.cmdfiles run through a quotedcmd.exeinvocation, not{ shell: true }, which would re-open command injection.
When to switch from execa
- Your callers, human or agent, need the same result as text and as JSON.
- You are tired of
try/catcharound every subprocess to read its exit code.
If you depend on execa's streaming or its template syntax, stay on execa. The entry points
and the resolvers plugin host are on bellpull.
Compatibility
How bellpull's drop-ins are graded — cross-spawn 68 / 68 and node-which 5 / 5 by their own suites — and why execa's suite grades bellpull 0 / 1360 and is declared a ceiling, not a drop-in.
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.