# 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.

Source: https://bellpull.interlace.tools/docs/coming-from/execa

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

```diff
- 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:

```diff
- 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](https://burgee.interlace.tools/docs/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 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](/docs).
