bellpull
Every export of bellpull, with its signature and doc comment: ambientRuntime, DEFAULT_GRACE, DEFAULT_TIMEOUT, escapeArgument, escapeCommand, extensionCandidates and 20 more, plus 9 types.
bellpull — run another program and get back a result you can read.
A bellpull is the cord you pull in one room to ring a bell in another. Request work at a distance; the work happens elsewhere; someone comes back to you. That last clause is the package: {@link run} returns a {@link Result} for every outcome a process can have, including the ones the incumbents raise as exceptions.
Four things, and the boundaries between them are the design:
- {@link run} — spawn, collect, and come back with a record. A non-zero exit is data.
- {@link whichSync} / {@link searchPath} — resolution that reports which
PATHentry the executable came from, which is the open position in this layer. - {@link format} / {@link toJson} / {@link toEvent} — one result, three renderings.
- the
resolversplugin host, atbellpull/plugin.
The drop-in path for cross-spawn is bellpull/cross-spawn and is not re-exported here:
it reads the ambient process because its callers expect it to, and this entry does not,
so a program importing bellpull never picks that up by accident.
Honest scope. execa's surface is not reproduced — its streaming API and its
template-literal form are a different product and are recorded as out of scope in
spec.md. What is here is the result shape and the resolution, which is what the
intent's kill gate says to build on.
import { ambientRuntime, DEFAULT_GRACE, DEFAULT_TIMEOUT, … } from 'bellpull';Functions
ambientRuntime
The runtime of the process this is running in, or a runtime that admits it knows nothing.
A missing process yields platform: '', an empty environment and cwd: '.'. Every one
of those is a truthful answer that makes resolution fail rather than guess: an empty
PATH finds nothing, and finding nothing is the correct result when there is no
environment to look in.
uid/gid are only read where the platform has them. On Windows process.getuid is
absent, and undefined is what tells which.ts to skip the ownership half of the
executable-bit check rather than compare against a number nobody supplied.
function ambientRuntime(): Runtime;Returns Runtime
escapeArgument
Escape one argument so cmd.exe and then the callee's argv parser both give it back
unchanged.
doubleEscape is for a node_modules/.bin/*.cmd shim, which re-enters cmd.exe.
function escapeArgument(argument: unknown, doubleEscape?: boolean): string;| Parameter | Type |
|---|---|
argument | unknown |
doubleEscape (optional) | boolean |
Returns string
escapeCommand
Escape a command name for cmd.exe.
Only pass (2): the command is not quoted, because cmd.exe resolves it before the C
runtime parses anything, and a quoted command with a caret in it is not found.
function escapeCommand(command: string): string;| Parameter | Type |
|---|---|
command | string |
Returns string
format
How a result reads to a person: one line, and the failure's output when there is one.
function format(result: Result): string;| Parameter | Type |
|---|---|
result | Result |
Returns string
isWindows
Whether this runtime plays by Windows' rules. Cygwin and msys announce themselves in OSTYPE.
function isWindows(runtime: Runtime): boolean;| Parameter | Type |
|---|---|
runtime | Runtime |
Returns boolean
pathDelimiter
The separator between PATH entries: ; on Windows, : everywhere else.
function pathDelimiter(runtime: Runtime): string;| Parameter | Type |
|---|---|
runtime | Runtime |
Returns string
pathKey
The key PATH is spelled under in this environment.
path-key, 244.9 M downloads a week, is this function. Windows environment variables are
case-insensitive but a plain JavaScript object's keys are not, so an environment copied
out of process.env on Windows can carry Path, and a lookup for PATH misses it. The
last matching key wins, which is what path-key does and what the shell does.
function pathKey(runtime: Runtime): string;| Parameter | Type |
|---|---|
runtime | Runtime |
Returns string
pathOf
The PATH value for this runtime, under whichever key it is spelled.
function pathOf(runtime: Runtime): string;| Parameter | Type |
|---|---|
runtime | Runtime |
Returns string
readShebang
The interpreter the file at file declares, or undefined — for a file that is not there,
is not readable, is a directory, or simply has no shebang.
Every one of those is the same answer to the caller ("no interpreter to use"), so they are one return rather than four. The distinction that matters — whether the file exists — belongs to resolution, which has already been done by the time this is asked.
function readShebang(file: string): string | undefined;| Parameter | Type |
|---|---|
file | string |
Returns string \| undefined
run
Run a program and come back with a {@link Result}.
Rejects with {@link NotFoundError } when the executable does not resolve and {@link SpawnError} when the spawn itself fails. Everything else — any exit code, any signal, a breached deadline — resolves.
function run(command: string, args: readonly unknown[] | undefined, options: RunOptions): Promise<Result>;| Parameter | Type |
|---|---|
command | string |
args | readonly unknown[] | undefined |
options | RunOptions |
Returns Promise<Result>
shebangCommand
The interpreter a #! line names, or undefined.
#!/usr/bin/env node is node: env is a launcher, so its argument is the interpreter
and the path in front of it is discarded. #!/bin/sh -e is sh -e — the flag is kept,
because dropping it changes what the script does.
An empty argument is no argument, as it is to shebang-command, which tests it for truth:
#!/bin/sh followed by a trailing space is sh, not sh plus a space — a name nothing on
PATH answers to — and #!/usr/bin/env followed by two spaces and node names no
interpreter rather than an empty one.
function shebangCommand(source: string): string | undefined;| Parameter | Type |
|---|---|
source | string |
Returns string \| undefined
toEvent
The agent form: {@link outcomeOf}'s one word, plus the record's own fields.
function toEvent(result: Result): RunEvent;| Parameter | Type |
|---|---|
result | Result |
Returns RunEvent
toJson
The --json envelope: the record, flat, with nothing computed that is not in it.
function toJson(result: Result): Record<string, unknown>;| Parameter | Type |
|---|---|
result | Result |
Returns Record<string, unknown>
Classes
SpawnError
Spawning failed for a reason that is not a missing executable.
ERR_, not E_. The E_… codes are the plugin vocabulary — E_PLUGIN_SCHEMA and the
rest — which plugin-contract R8 requires every layer to share and
scripts/plugin-error-vocabulary-lock.test.ts enforces by refusing any 'E_…' literal a
host ships that is not in its declared union. This is a runtime failure rather than a
refused plugin, so it takes Node's own convention for a runtime error code and stays out
of a vocabulary it is not part of.
class SpawnError extends Error {
readonly command: string;
readonly cause: unknown;
readonly code = "ERR_SPAWN_FAILED";
constructor(command: string, cause: unknown);
}Constants
DEFAULT_GRACE
Milliseconds between SIGTERM and SIGKILL, for a child that declines to leave.
const DEFAULT_GRACE = 5000;DEFAULT_TIMEOUT
Milliseconds a child may take before it is killed. Finite by default — Y10.
const DEFAULT_TIMEOUT = 30000;name
The package's own name, kept from the reserved-name release so nothing that read it breaks.
const name: "bellpull";Interfaces
ExitHost
A host that can register work to run when the process is shutting down.
interface ExitHost {
/** Register; the returned function unregisters. `closeout`'s `Registry.add` fits as-is. */
add(handler: () => unknown, spec?: unknown): () => void;
}Result
What a run produced. One value, three renderings (see project.ts).
interface Result {
/** The exit was 0 and the run was not killed. The only field most callers branch on. */
ok: boolean;
/** The exit code, or `null` when the child died of a signal — the shape Node reports. */
code: number | null;
/** The signal that killed it, or `null`. */
signal: NodeJS.Signals | null;
/** Everything the child wrote to stdout. `''` when stdio was not captured. */
stdout: string;
/** Everything the child wrote to stderr. */
stderr: string;
/**
* Wall-clock milliseconds from spawn to close — **including Node's own scheduling**, not
* the child's CPU time (design R11). Stated here rather than in a README because the
* caveat is what makes the number usable: it is the right figure for "how long did the
* user wait" and the wrong one for "how expensive is this program".
*/
duration: number;
/** The command as the caller wrote it. */
command: string;
/** The arguments as the caller wrote them. */
args: readonly string[];
/**
* The file that actually ran, and the `PATH` entry it came from — the field that answers
* "which binary was this" without a second investigation. `undefined` when resolution was
* skipped (`shell: true`, where the shell resolves).
*/
executable: {
path: string;
from: string;
} | undefined;
/** The deadline fired and the child was killed. `ok` is `false`. */
timedOut: boolean;
}RunEvent
One event on an agent stream. type is what a consumer switches on.
interface RunEvent {
type: 'run';
outcome: Outcome;
command: string;
args: readonly string[];
code: number | null;
signal: string | null;
durationMs: number;
executable: string | null;
/** Where the executable came from, so an agent can report a `PATH` surprise itself. */
from: string | null;
}RunOptions
interface RunOptions extends SpawnOptions {
/** The ambient state. Required: nothing here reads `process` (Y9). */
runtime: Runtime;
/** Milliseconds before the child is killed. `0` disables the deadline, deliberately. */
timeout?: number | undefined;
/** Milliseconds between `SIGTERM` and `SIGKILL` on a breach. */
grace?: number | undefined;
/**
* Where to register the child's kill so it is not orphaned if the parent is shut down.
*
* **This is the `bellpull → closeout` seam, and it is a parameter rather than an import
* on purpose.** `closeout` owns bounded exit paths; a parent that is killed while a child
* is running must not leave that child behind. But `package-shape-lock.test.ts` asserts
* that a foundation package depends on *nothing* — bellpull and closeout are both in that
* tier, so a package edge between them is forbidden by a lock on main. The shape is
* declared structurally instead (the family's R3 idiom), so
* `run(cmd, args, { exitHost: closeoutRegistry })` composes with nothing imported.
*
* Given none, **no signal handler is registered at all**. Registering one silently would
* be this package deciding to own the process's signals, which is the layer above's job.
*/
exitHost?: ExitHost | undefined;
/** Passed to `child_process`. `'inherit'` is what a pass-through subcommand wants. */
stdio?: 'pipe' | 'inherit' | 'ignore' | readonly unknown[] | undefined;
}Runtime
The ambient state this package reads. Structural on purpose: a caller may pass Node's own
process, a fake, or an object assembled from a container's environment.
interface Runtime {
/** `process.platform`. `win32` selects `PATHEXT`, backslashes and the cmd.exe rules. */
readonly platform: Platform;
/** The environment. `PATH` (or whichever case Windows used) is read out of it. */
readonly env: Readonly<Record<string, string | undefined>>;
/** The working directory a relative command or `cwd` option is resolved against. */
readonly cwd: string;
/** Effective uid, where the platform has one. Absent means "do not check ownership". */
readonly uid?: number | undefined;
/** Effective gid, where the platform has one. */
readonly gid?: number | undefined;
}Types
Platform
A platform string, as process.platform spells it. Only win32 is ever special-cased.
type Platform = string;Re-exported
Documented on the page of the entry point that declares them.
| Export | Kind | Documented in |
|---|---|---|
extensionCandidates | function | bellpull/which |
pathExtensions | function | bellpull/which |
resolveExecutable | function | bellpull/which |
runPath | function | bellpull/which |
searchPath | function | bellpull/which |
whichAllSync | function | bellpull/which |
whichOrThrowSync | function | bellpull/which |
whichSync | function | bellpull/which |
NotFoundError | class | bellpull/which |
Resolution | interface | bellpull/which |
SearchEntry | interface | bellpull/which |
WhichOptions | interface | bellpull/which |
Incremental migration from cross-spawn
Swap cross-spawn for bellpull/cross-spawn in one import, then move call sites to run() one at a time, and know what changes at each step.
bellpull/which
Every export of bellpull/which, with its signature and doc comment: pathExtensions, extensionCandidates, searchPath, whichAllSync, whichSync, resolveExecutable and 5 more, plus 3 types.