# bellpull/plugin

> Every export of bellpull/plugin, with its signature and doc comment: validate, register, reset, registered, contributions, substitute and 3 more, plus 6 types.

Source: https://bellpull.interlace.tools/docs/api/plugin

<!-- Generated by scripts/api-reference.ts from the built dist/*.d.ts. Do not edit; run `npx tsx scripts/api-reference.ts`. -->

The plugin host for bellpull's half of the contract — `plugin-contract` R5a, R6, R7, R8,
and PLAN step 1.5.

A plugin is one plain object shared by the whole family. This file keeps the key bellpull
understands — **`resolvers`**, how an executable is found — and **ignores every other key
without complaining**, which is what makes the same object work on any subset of the
family that is installed. A plugin written for flagstaff registers here and contributes
nothing; its `spinners` and `components` are not bellpull's business and are not an error.

**Nothing here imports another layer, and the plugin shape is declared rather than
imported** (R3).

## Why `resolvers` is the key this layer owns

R5a's sentence is *"how an executable is found (`PATH`, a version manager, a container),
since `which` is the part every environment does differently"*. That is the whole
argument: `PATH` is a convention from a world with one filesystem and one user, and the
environments people actually run in have since bolted a search order on top of it — `nvm`
and `asdf` put shims in a directory computed from an environment variable, `pnpm` and
`yarn` place binaries somewhere `npm` does not, a devcontainer mounts tools outside every
`PATH` the host had. Each of those is *the same operation with a different list of
directories*, which is exactly the kind of difference that should be data rather than a
fork of the resolver.

## R7 is met with no exception asked for, and that is deliberate

`closeout`'s `handlers` had to take a function, because an exit handler *is* behaviour and
there is no data encoding of "close this socket". A resolver has one: a search order is a
list of directories. So every field here is inspectable without running anything —
`burgee plugin check` can print the search order a plugin contributes, and an agent can
*write* one, which is PRINCIPLES' "plugins as data that agents write" rather than a slogan.

The one thing a directory list cannot express on its own is a path computed from the
environment (`$ASDF_DATA_DIR/shims`). That is solved the way `paratext` solved the same
problem for OSC payloads: `{VAR}` in a path is substituted from the runtime's environment,
and an entry naming a variable that is not set is **dropped**, not guessed at. A template
is still data; a function would not be.

## Security: a resolver may only add absolute directories

A resolver contributes directories that are searched **before** `PATH`, which is real
privilege: whoever writes the plugin decides what `run('node')` means. Two refusals bound
it, both at `register()` rather than at search time, because a refusal at search time is a
refusal nobody sees:

- **a path must be absolute after substitution.** A relative entry resolves against
  whatever the working directory happens to be when a child is spawned, so a plugin that
  contributed `node_modules/.bin` would mean something different in every directory the
  program was run from — and in a directory an attacker can write to, it means their
  binary. `which.ts` refuses relative `PATH` entries for the same reason; this is the
  same rule one layer out, where it can be enforced before anything runs.
- **a substitution may not smuggle a separator.** `{HOME}` is a value from the
  environment, and an environment variable containing `:` or `;` would otherwise split
  one contributed directory into two — the entry is dropped rather than split.

```ts
import { validate, register, reset, … } from 'bellpull/plugin';
```

## Functions

### contributions

Every resolver every registered plugin contributed, in the order they will be searched.

This is the static projection of the search order, and the reason `rank` is worth having:
a caller — or `burgee plugin check` — reads what `run('node')` would resolve against
without resolving anything.

```ts
function contributions(): Contribution[];
```

**Returns** `Contribution[]`

### directories

The directories registered plugins contribute for this runtime, in search order.

A negative rank means "before `PATH`", so a caller splices these around the entries
`which.ts`'s own `searchPath()` returns. Nothing here searches: this is the list, as data,
which is the half a plugin is allowed to decide.

```ts
function directories(runtime: Runtime): ContributedDirectory[];
```

| Parameter | Type |
| :-- | :-- |
| `runtime` | `Runtime` |

**Returns** `ContributedDirectory[]`

### register

Register a plugin. Later wins, like ESLint flat config — though "wins" is narrower here
than for a colour token: two resolvers under the same name shadow, and two resolvers under
different names both apply, in rank order.

```ts
function register(plugin: unknown): void;
```

| Parameter | Type |
| :-- | :-- |
| `plugin` | `unknown` |

**Returns** `void`

### registered

The plugins registered, in registration order.

```ts
function registered(): readonly Plugin[];
```

**Returns** `readonly Plugin[]`

### reset

Forget every registered plugin. For tests, and for a program that re-plugs at runtime.

```ts
function reset(): void;
```

**Returns** `void`

### substitute

Substitute `{VAR}` from the environment.

`undefined` for an unset variable, for a value that is not absolute, and for a value
carrying a `PATH` separator — see the security note at the top of this file. Every one of
those is "this entry does not apply here", which is a normal answer in an environment
where the tool is simply not installed.

```ts
function substitute(template: string, runtime: Runtime): string | undefined;
```

| Parameter | Type |
| :-- | :-- |
| `template` | `string` |
| `runtime` | `Runtime` |

**Returns** `string \| undefined`

### validate

Refuse a plugin that cannot contribute a resolver, at the door.

Every refusal here is a refusal rather than a silent drop, for the reason roundel's host
gives about a misspelt token: a contribution that is quietly ignored looks like it worked,
and its author debugs the wrong thing — except that here the thing they are debugging is
"why did CI run a different `node` from my laptop", which is the question this layer
exists to make answerable.

```ts
function validate(plugin: unknown): asserts plugin is Plugin;
```

| Parameter | Type |
| :-- | :-- |
| `plugin` | `unknown` |

**Returns** `asserts plugin is Plugin`

## Classes

### PluginError

A refused plugin says what is wrong and what to do about it — the family's one vocabulary.

```ts
class PluginError extends Error {
    readonly code: PluginErrorCode;
    readonly fix: string;
    constructor(code: PluginErrorCode, message: string, fix: string);
}
```

## Constants

### CONTRACT

The plugin contract version. One number for the family — the same `1` flagstaff, roundel,
caique and closeout declare, written out rather than imported for the reason above.

```ts
const CONTRACT = 1;
```

## Interfaces

### ContributedDirectory

A contributed directory, and which resolver put it there.

```ts
interface ContributedDirectory {
    dir: string;
    /** The resolver's name, so a report can say why this directory is being searched. */
    resolver: string;
    rank: number;
}
```

### Contribution

One contributed resolver, with the plugin it came from and what it shadowed.

```ts
interface Contribution {
    name: string;
    from: string;
    resolver: Resolver;
    /** Plugins that contributed this name earlier and were overridden, in order. */
    shadowed: string[];
}
```

### Plugin

The keys bellpull reads. Declared structurally: any object with these fields is a plugin
here, whatever else it carries.

```ts
interface Plugin {
    name: string;
    contract?: number;
    resolvers?: Record<string, Resolver>;
}
```

### Resolver

One contributed way of finding an executable.

No functions, so it travels through JSON and a `plugin check` can print it.

```ts
interface Resolver {
    /**
     * Where this sits relative to `PATH`, which is rank `0`. A negative rank searches before
     * `PATH` — which is what a version manager's shims want, because being *after* `PATH` is
     * the same as not being installed. Ties keep registration order.
     */
    rank: number;
    /**
     * Directories to search, in order. Absolute after substitution, or dropped.
     * `{VAR}` is replaced by `runtime.env.VAR`; an unset variable drops the entry.
     */
    paths: readonly string[];
    /** Extra extensions to try, in order — `PATHEXT` for one resolver rather than the machine. */
    extensions?: readonly string[];
    /** When it applies. Absent means always. */
    when?: ResolverWhen;
}
```

### ResolverWhen

When a resolver applies. Every clause must hold; each array is an OR within itself.

```ts
interface ResolverWhen {
    /** Platforms, as `process.platform` spells them. Absent means every platform. */
    platform?: readonly string[];
    /** Any one of these environment variables merely being set — how a version manager announces itself. */
    envAny?: readonly string[];
}
```

## Types

### PluginErrorCode

```ts
type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
```
