bellpull
Coming from…

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%:

passingrate
bellpull against execa 10.2.1's suite0 / 13600.0%
execa itself (control)1360 / 1360100.0%
bellpull/cross-spawn against cross-spawn's suite68 / 68100.0%
cross-spawn itself (control)68 / 68100.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 with result.ok === false and the code; it rejects only when no process ran — the executable did not resolve, or the spawn failed.
  • It says which binary ran. result.executable is the path and the PATH entry it came from — the first question of every "works on my machine" investigation.
  • Bounded by default. timeout is finite, the kill is a ladder (SIGTERM, then SIGKILL), 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 --json flag cannot report something the human output did not.
  • No shell, ever, to solve a Windows problem. .cmd files run through a quoted cmd.exe invocation, 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/catch around 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.

On this page