burgee
API reference

burgee

Every export of burgee, with its signature and doc comment: defineError, ExitCode, isExitCode, ExitCodeValue, defineCommand, defineProgram and 14 more, plus 45 types.

burgee — a command declares itself once; every surface is that declaration read by a different reader.

The public entry, and only that. The execution core lives in execute.ts so a façade can import it without pulling this barrel.

What is not here, and why (D-093, reversed on a measurement)

This barrel used to re-export the value half of help.ts, mcp.ts, schema.ts, plugin.ts, manifest.ts and seniority/precedence as a convenience. A re-export is not free: it makes those modules live for every consumer of burgee, whether or not anything reads them. execute.ts loads each one behind an await import(), so the only thing keeping them on the startup path was this file.

Measured 2026-09-21 with --splitting --outdir over the transitive closure of import statements — the initial load a consumer actually pays:

  • import { run } from 'burgee' — 44,663 bytes
  • the same program against execute.ts directly — 31,047 bytes
  • cold start, burgee ÷ cac — 2.113 → 1.717

D-093 declined this split on a cold-start argument it did not have the number for. The number says 13,616 bytes and 19% of startup, so the split lands: every value moved here has a subpath of its own (burgee/help, burgee/mcp, burgee/schema, burgee/plugin, burgee/config), which is where a program that wants it should say so.

Every type stays. A type re-export is erased and costs a consumer nothing, so the whole type surface is still importable from burgee and no typed program has to move.

import { defineError, ExitCode, isExitCode, … } from 'burgee';

Functions

camel

--dry-run on the command line reaches the handler as dryRun.

function camel(flag: string): string;
ParameterType
flagstring

Returns string

checkCommand

The whole door: {@link checkDefinition} — which carries V5's reserved names — and {@link checkEffects }.

This exists as one function because it was two. defineCommand ran both; Manifest.use() ran neither, so a plugin's command was admitted unread — and a plugin option named json did not clash with the envelope flag, it replaced it in the parse config. The fix is not a second copy of the guard beside use(); it is that there is one guard and both callers reach it, which is the only arrangement a reader can check by looking.

It takes the declaration whole, for exactly that reason. It took effects and runs as required arguments so a caller could not decline a check by writing nothing; D1 would have made that five, and a sixth field would make it six. Reading the object means a field the door checks is one no caller has to remember to forward — including whether it runs.

function checkCommand(name: string, declared: Declared): void;
ParameterType
namestring
declaredDeclared

Returns void

checkDefinition

What must be true of a declaration before anything runs (yargs #1198, #887, #1679): a known type, one short alias per command, no two keys that meet on the command line, and none of V5's reserved surfaces.

The reserved names used to be a second loop over the same keys in {@link checkCommand}, and the numeric bound used to be a fourth branch in this one. Both moved to pay for the canonical-key check below without raising a weight ceiling — ./plugin had 5 bytes of headroom — and both are better where they are now: flag is computed once here and kebab is the identity on every reserved name, and a numeric bound is a fact about a spec rather than about a name. The file is 51 bytes smaller than before the check existed.

function checkDefinition(name: string, options: Record<string, OptionSpec>): void;
ParameterType
namestring
optionsRecord<string, OptionSpec>

Returns void

defineCommand

function defineCommand<const S extends OptionSpecs = OptionSpecs>(command: Command<S>): Command<S>;
ParameterType
commandCommand<S>

Returns Command<S>

defineError

function defineError(definition: ErrorDefinition): DefinedErrorClass;
ParameterType
definitionErrorDefinition

Returns DefinedErrorClass

defineProgram

A native multi-command program. The manifest it builds is the same one the façades fill.

function defineProgram(program: Program): Manifest;
ParameterType
programProgram

Returns Manifest

detectAgent

function detectAgent(env: Record<string, string | undefined>, tty: boolean, probes?: readonly AgentProbe[]): Detection;
ParameterType
envRecord<string, string | undefined>
ttyboolean
probes (optional)readonly AgentProbe[]

Returns Detection

execute

function execute(manifest: Manifest, opts?: RunOptions & {
    root?: string[];
    from?: 'node' | 'user';
}): Promise<void>;
ParameterType
manifestManifest
opts (optional)RunOptions & { root?: string[]; from?: 'node' | 'user'; }

Returns Promise<void>

kebab

dryRun → dry-run; a kebab key stays as it is.

function kebab(name: string): string;
ParameterType
namestring

Returns string

resolveCommand

M6: which command argv names, or null — the same longest-prefix match execute uses.

function resolveCommand(manifest: Manifest, argv: readonly string[]): CommandNode | null;
ParameterType
manifestManifest
argvreadonly string[]

Returns CommandNode \| null

run

The one-file entry: a single command, or a program from defineProgram. Both go through execute, so there is exactly one code path from argv to exit.

function run<S extends OptionSpecs>(target: Command<S> | Manifest, opts?: RunOptions): Promise<void>;
ParameterType
targetCommand<S> | Manifest
opts (optional)RunOptions

Returns Promise<void>

runCommand

M6: run a command programmatically — the harness's own entry, public. The streams are captured, the exit recorded; env, cwd and the entry can be injected like execute's.

function runCommand(manifest: Manifest, argv: readonly string[], opts?: Pick<RunOptions, 'env' | 'cwd' | 'entry' | 'stdin'>): Promise<RunResult>;
ParameterType
manifestManifest
argvreadonly string[]
opts (optional)Pick<RunOptions, 'env' | 'cwd' | 'entry' | 'stdin'>

Returns Promise<RunResult>

sharedOptions

Options declared once and spread into each command that takes them (M4): never global, so --schema stays a tree and each command's help lists them as its own, and the handler's options type carries them like any other. Every copy is tagged sharedFrom with the set's name, so the schema says where it came from.

function sharedOptions<const T extends OptionSpecs>(name: string, specs: T): T;
ParameterType
namestring
specsT

Returns T

Classes

AuthError

E6 — the far side said no. Throw this and the run leaves with ExitCode.AUTH.

The one error class whose response is unambiguous: not "read the message and decide" but "get a credential and run it again". A handler that throws a bare Error for a 401 gets RUNTIME, which is the code for everything, and a caller retrying on it retries forever.

fix is the exact command that gets the credential, where the program knows it — hint is prose a person reads and fix is a line a caller runs, which is the turn the field saves.

class AuthError extends Error {
    readonly hint?: string | undefined;
    readonly fix?: string | undefined;
    constructor(message: string, hint?: string | undefined, fix?: string | undefined);
}

UsageError

A usage problem the caller can fix, carrying the flag that fixes it (E3).

class UsageError extends Error {
    readonly hint?: string | undefined;
    constructor(message: string, hint?: string | undefined);
}

Constants

AGENT_PROBES

const AGENT_PROBES: readonly AgentProbe[];

Interfaces

ActionRequiredSpec

What a caller must do before the command can continue (N11): synthesised into the envelope.

interface ActionRequiredSpec {
    /** A short machine-readable reason: `login`, `confirm`, `missing-config` … */
    reason: string;
    message: string;
    /** Runnable commands, each with when to run it; the engine prefixes the program and carries the caller's flags. */
    next?: readonly {
        command: string;
        when: string;
    }[];
    hint?: string;
}

AgentProbe

Agent detection, not just isTTY (N12). An agent may well have a terminal; what it does not have is a person. FORCE_TTY=1 overrides. AI_AGENT is the generic escape hatch any agent can set.

The list is the five variables roundel/terminal's AGENTS holds, in the same order, and it grows by one only for a variable that uniquely identifies an agent: set by the agent, and never in a terminal a person types in. CURSOR_TRACE_ID fails that test — Cursor sets it in every integrated terminal — so it is not here and must not be (D-20260930-one-interactive-rule).

Two answers come from here, and they are about different streams:

  • agent is ctx.agent: which agent, if any, the environment names.
  • interactive is the output side: stdout is a terminal a person is reading, not an agent's capture. Help's colour reads it (O2, colorFor).

Whether a person may be asked is not this function's answer. ctx.interactive is roundel's interactive() — a terminal on stdin, no CI, no agent — computed in ctx.ts.

interface AgentProbe {
    /** Environment variable whose presence names the agent. */
    variable: string;
    agent: string;
}

ArgumentSpec

A positional, as help documents it (yargs #2012).

interface ArgumentSpec {
    name: string;
    description?: string;
    required?: boolean;
    variadic?: boolean;
    default?: string;
    /** `'file'`: a path, where `-` means standard input — handed to the handler as `ctx.stdin` (S4). */
    type?: 'file';
}

Command

interface Command<S extends OptionSpecs = OptionSpecs> {
    name: string;
    description?: string;
    /** Shown in command lists instead of the description. */
    summary?: string;
    /** Declared once (S1); the handler's `options` type is derived from it. */
    options?: S;
    arguments?: ArgumentSpec[];
    examples?: Example[];
    /** Heading this command is listed under in its parent's help. */
    group?: string;
    epilogue?: string;
    hidden?: boolean;
    /** What replaces this command, e.g. `'deploy'`: shown in help, `--schema` and the warning. `true` alone is refused (D1). */
    deprecated?: boolean | string;
    /**
     * What running it does to the world (N6). Declaring one of the three is what exposes the
     * command as an MCP tool (N2); `'withheld'` declares that it is not offered to agents.
     *
     * Optional on the type and **required at definition time** on a command that runs:
     * `defineCommand` refuses one that omits it. It stays optional here because a group that
     * only holds subcommands declares none, and TypeScript cannot make a field's presence
     * depend on a sibling's without splitting `Command` into a union that would cost the
     * option-spec inference every caller of this type relies on.
     */
    effects?: DeclaredEffects;
    /** Relationships between options, validated before choices and the handler (S2, S6). */
    relations?: readonly Relation[];
    /**
     * The top-level fields of this command's result (N14). With them, `--json=` lists them
     * without running the handler and `--json=a,b` refuses an unknown field before it runs.
     * Without them `--json=a,b` still selects, checked against the result's own keys.
     */
    fields?: readonly string[];
    /** Absent on a group that only holds subcommands. `NoInfer`: the spec fixes S, the handler only reads it. */
    run?: (ctx: CommandContext<InferOptions<NoInfer<S>>>) => unknown;
    /** The handler's module, imported on dispatch only (M2); everything else about the command is declared here. */
    load?: () => Promise<LazyModule>;
    commands?: AnyCommand[];
}

CommandContext

interface CommandContext<O> extends Omit<RunContext, 'options'> {
    options: O;
}

CommandNode

interface CommandNode {
    path: string[];
    description?: string;
    /** Shown in command lists instead of the description (yargs #1265). */
    summary?: string;
    options: Record<string, OptionSpec>;
    relations?: readonly Relation[];
    arguments?: ArgumentSpec[];
    examples?: Example[];
    /** Heading this command is listed under in its parent's help (yargs #684). */
    group?: string;
    epilogue?: string;
    hidden?: boolean;
    deprecated?: boolean | string;
    /**
     * What running it does to the world, or `'withheld'` (N2, N6). Required on a node that
     * runs — `checkCommand` refuses one that omits it — and optional on the type, because a
     * group carries no `effects` and the host front-ends build nodes that never reach that
     * door: commander and yargs have no notion of effects and their graded suites declare
     * none, so a façade's command is withheld in fact and cannot be made to say so.
     */
    effects?: DeclaredEffects;
    /** The result's top-level fields, as declared (N14); what `--json=` lists. */
    fields?: readonly string[];
    run?: (ctx: RunContext) => unknown;
    /**
     * The handler's module, imported on dispatch only (M2): the manifest — help, schema,
     * completions, MCP tool list — is complete from this node without loading it. A node
     * with `load` and no `run` gets a `run` that imports on first call.
     */
    load?: () => Promise<LazyModule>;
    /** Which plugin contributed this, if any. Declared, never diffed (M3). */
    plugin?: string;
}

DefinedErrorClass

An author-defined error class; exitCode is the code it leaves with.

interface DefinedErrorClass {
    new (message: string, options?: DefinedErrorOptions): Error & DefinedErrorOptions;
    readonly exitCode: number;
}

DefinedErrorOptions

What new takes: the message, and optionally the E3 hint and exact fix.

interface DefinedErrorOptions {
    hint?: string;
    fix?: string;
}

Detection

interface Detection {
    /** The agent named by the environment, if any; `AI_AGENT`'s own value when it names one. */
    agent?: string;
    /**
     * stdout is a terminal and no agent is named, or `FORCE_TTY=1`: the output side, which help's
     * colour reads. Whether to *ask* is `ctx.interactive`, roundel's rule over stdin and `CI`.
     */
    interactive: boolean;
}

ErrorDefinition

What an author declares.

interface ErrorDefinition {
    /** The class name, shown as the error's `name`. */
    name: string;
    /** The exit code, 7–125, owned by this class alone. */
    code: number;
}

Example

One example: a single copy-pasteable command line, the description below it (H2).

interface Example {
    command: string;
    description?: string;
}

Hook

interface Hook {
    filter?: HookFilter;
    /** `parse` may return the argv to use instead; every other stage's return is ignored. */
    handler: (ctx: HookContext) => unknown;
}

LazyModule

What a lazily loaded command module exports: the handler as run or as the default export (M2).

interface LazyModule {
    default?: (ctx: RunContext) => unknown;
    run?: (ctx: RunContext) => unknown;
}

OptionSpec

One option, declared once (S1). The key is the canonical camelCase name the handler reads; the CLI form is derived as kebab-case (S5): dryRun is typed --dry-run.

interface OptionSpec {
    /** `boolean` never consumes a value (S7); `number` rejects NaN and Infinity (S3). */
    type: 'string' | 'boolean' | 'number';
    /**
     * D3 / D-119 — values computed when a person presses TAB: the generated script calls the
     * program back (`<program> __complete <command> --<option> <partial>`) for this option and
     * no other. Declaring it is the opt-in; an option without one completes from `choices`, or
     * not at all, and never runs the program.
     */
    complete?: (partial: string) => Iterable<string> | Promise<Iterable<string>>;
    description?: string;
    required?: boolean;
    short?: string;
    default?: string | boolean | number | readonly string[] | readonly number[];
    /** Environment variable consulted when the flag is absent (V2). Read from the injected env, never process.env directly. */
    env?: string;
    /** Allowed values, enforced and shown as `(one of: a, b)` (yargs #1408, #1186). */
    choices?: readonly string[];
    /** Repeatable, and split on `separator` (`,` unless declared): `--tag a --tag b,c` → `['a', 'b', 'c']` (S8). */
    multiple?: boolean;
    separator?: string;
    /**
     * The other options this one requires: `--out --dependsOn force` is a usage error without
     * `--force` (S2). Sugar for a `{ implies: [this, other] }` relation per name, and nothing
     * else — one engine, one order, one error vocabulary.
     *
     * It exists as a second spelling because the first states the constraint away from the
     * option it constrains: a reader looking at `out` learns nothing from a `relations` entry
     * three keys down, and neither does the help line for `--out`. Both incumbents spell it on
     * the option (commander `.implies()`, yargs `.implies()`), and so does Fig, whose `Option`
     * declares this exact key.
     */
    dependsOn?: readonly string[];
    /**
     * The other options this one may not be given with (S2): `{ conflicts: [this, other] }` per
     * name. Commander's `.conflicts()`, yargs' `.conflicts()`, Fig's `exclusiveOn`.
     *
     * One-sided is enough — the relation it compiles to holds whichever of the two argv names
     * first — so declare it once, on whichever option the constraint belongs to.
     */
    exclusive?: readonly string[];
    /** `number` only. */
    minimum?: number;
    maximum?: number;
    integer?: boolean;
    /** Any Standard Schema, run on the parsed value; its issues become a usage error (S1). */
    schema?: StandardSchemaV1;
    /** The value's name in help: `--id <dataset-id>` (yargs #833). */
    placeholder?: string;
    /** `true` renders `(deprecated)`; a string names the replacement: `(deprecated: use --force)` (yargs #2248). */
    deprecated?: boolean | string;
    hidden?: boolean;
    /**
     * `boolean` only: whether `--no-<name>` is accepted. Every boolean is negatable unless this
     * says `false`. burgee's own parser negates every boolean (see `toParseConfig`); the
     * commander façade sets `false` where commander would refuse the negation, so completions
     * and `--mcp` never offer or send a flag the parser behind them rejects.
     */
    negatable?: boolean;
    /** The shared set this option was copied from (M4); `--schema` carries it, help lists the option like any other. */
    sharedFrom?: string;
}

Program

interface Program {
    name: string;
    version?: string;
    description?: string;
    /** Options read `PREFIX_OPTION_NAME` from the environment unless they name their own variable (V2). */
    envPrefix?: string;
    /** Characters of `--schema` output above which it is summarised (N13); 48,000 by default. */
    schemaBudget?: number;
    /**
     * Opt into config discovery (V6): `--config <path>` > `NAME_CONFIG` > `./name.config.{json,mjs,js,cjs}`
     * > `package.json#name` > the user config directory; `true` uses the program's name.
     */
    config?: boolean | {
        name: string;
    };
    commands: AnyCommand[];
}

RunContext

interface RunContext {
    options: Record<string, unknown>;
    positionals: string[];
    passthrough: string[];
    /**
     * Standard input, present only when a `type: 'file'` argument was given `-` (S4). The
     * positional still reads `-`, so a handler checks it the same way it checks a path.
     */
    stdin?: NodeJS.ReadableStream;
    env: Record<string, string | undefined>;
    /** Exit with an E1 code. Unwinds cleanly: the code is honoured and nothing is printed. */
    exit: (code: number) => never;
    /**
     * A person may be asked and be expected to answer (N12): roundel's `interactive()`, the
     * family's one rule — a terminal on stdin, no `CI`, no detected agent — or `FORCE_TTY=1`.
     */
    interactive: boolean;
    /** The agent the environment names, if any (N12). */
    agent?: string;
    /** Stop and tell the caller what to do instead of blocking on a prompt (N11). */
    actionRequired: (spec: ActionRequiredSpec) => never;
    /**
     * Cleanup that runs on **every** path out of the run (E5): a normal return, `ctx.exit`,
     * Ctrl-C, SIGTERM, a terminal closing, an uncaught throw. Returns the function that
     * unregisters it, for a command that cleaned up on its own.
     *
     * The handler runs after stdout has been drained (O5) and before the terminal is handed
     * back, and it runs exactly once however many of those arrive together. `label` is what a
     * breached shutdown deadline calls it; without one an arrow is reported as `(anonymous)`,
     * and the anonymous arrow is the shape that hangs.
     *
     * Typed here rather than re-exported from `closeout`, so a command's signature does not
     * change when that package's does.
     */
    onExit: (handler: () => void | Promise<void>, label?: string) => () => void;
}

RunOptions

interface RunOptions {
    argv?: string[];
    /** Read by `--mcp`, which serves JSON-RPC over it; its `isTTY` is whether a person may be asked (N12). */
    stdin?: NodeJS.ReadableStream;
    /** The environment env-bound options read from. Injected by the harness; the process's own otherwise. */
    env?: Record<string, string | undefined>;
    /** `columns` is read when present, so help wraps to the terminal (H3); `isTTY` decides whether help is coloured (O2). */
    stdout?: {
        write: (s: string) => unknown;
        columns?: number;
        isTTY?: boolean;
    };
    stderr?: {
        write: (s: string) => unknown;
    };
    /** Receives the E1 code. The default calls process.exit; an injected one may simply record it. */
    exit?: (code: number) => void;
    /** Where config discovery starts; the process's own otherwise. */
    cwd?: string;
    /** The entry file, whose nearest package.json owns the program's version (V4); `process.argv[1]` otherwise. */
    entry?: string;
}

RunResult

interface RunResult {
    code: number;
    stdout: string;
    stderr: string;
}

StandardSchemaV1

The Standard Schema interface (standardschema.dev), declared here so any implementation — zod, valibot, arktype — is accepted as an option's schema without a dependency (S1).

interface StandardSchemaV1<Output = unknown> {
    readonly '~standard': {
        readonly version: 1;
        readonly vendor: string;
        readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
    };
}

Usage

The envelope's error.usage, and what the prose renders.

interface Usage {
    /** The command line from the program name: `demo greet [options] <name>`. */
    command: string;
    /** A runnable command's options, as typed. */
    options?: UsageRow[];
    /** A group's commands, for a word it does not know. */
    commands?: UsageRow[];
    /** The command that lists every row, present only when some were left out. */
    more?: string;
}

UsageRow

interface UsageRow {
    /** `--code <value>`, `-n, --name <value>`, or a command word. */
    name: string;
    description: string;
}

Types

AnyCommand

type AnyCommand = Command<any>;

Effects

What running a command does to the world (N6). Declared, never inferred: it decides whether the command is exposed as an MCP tool at all (N2) and generates the tool's readOnlyHint / idempotentHint / destructiveHint — MCP defaults destructiveHint to true, so silence is the dangerous reading.

type Effects = 'read_only' | 'idempotent' | 'non_idempotent';

InferOptions

The handler's options, derived from the declaration (S1): choices become a union, multiple an array, number a number.

type InferOptions<S extends Record<string, OptionSpec>> = {
    [K in keyof S]: Present<S[K]> extends true ? Many<S[K], Scalar<S[K]>> : Many<S[K], Scalar<S[K]>> | undefined;
};

OptionSpecs

type OptionSpecs = Record<string, OptionSpec>;

Relation

A relationship between options, validated after parsing and before choices and the handler (S2, S6). implies takes a second option name, or a predicate over the values.

type Relation = {
    exactlyOneOf: readonly string[];
} | {
    atLeastOneOf: readonly string[];
} | {
    atMostOneOf: readonly string[];
} | {
    conflicts: readonly string[];
} | {
    implies: readonly [string, string | ((values: Record<string, unknown>) => boolean)];
};

StandardResult

type StandardResult<Output> = {
    readonly value: Output;
    readonly issues?: undefined;
} | {
    readonly issues: readonly {
        readonly message: string;
    }[];
};

Re-exported

Documented on the page of the entry point that declares them.

ExportKindDocumented in
isExitCodefunctionburgee/testing
Manifestclassburgee/schema
ExitCodeconstburgee/testing
ExitCodeValueconstburgee/testing
Candidateinterfaceburgee/config
CommandSchemainterfaceburgee/schema
HelpOptionsinterfaceburgee/help
JsonSchemainterfaceburgee/schema
Layersinterfaceburgee/config
Plugininterfaceburgee/plugin
ProgramSchemainterfaceburgee/schema
Provenanceinterfaceburgee/config
Resolutioninterfaceburgee/config
SchemaSummaryinterfaceburgee/schema
ServeOptionsinterfaceburgee/mcp
Toolinterfaceburgee/mcp
ToolAnnotationsinterfaceburgee/mcp
HelpThemetypeburgee/help
HelpTokentypeburgee/help
Invoketypeburgee/mcp
PluginErrorCodetypeburgee/plugin
Sourcetypeburgee/config

On this page