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.
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?
There is no graded execa row, and bellpull does not claim execa compatibility. The
Compatibility page, generated by npm run compat:page from the
oracle's last run, grades the cross-spawn path only:
| passing | rate | |
|---|---|---|
bellpull/cross-spawn | 68 / 68 | 100.0% |
| cross-spawn itself (control) | 68 / 68 | 100.0% |
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.