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.tsdirectly — 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;| Parameter | Type |
|---|---|
flag | string |
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;| Parameter | Type |
|---|---|
name | string |
declared | Declared |
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;| Parameter | Type |
|---|---|
name | string |
options | Record<string, OptionSpec> |
Returns void
defineCommand
function defineCommand<const S extends OptionSpecs = OptionSpecs>(command: Command<S>): Command<S>;| Parameter | Type |
|---|---|
command | Command<S> |
Returns Command<S>
defineError
function defineError(definition: ErrorDefinition): DefinedErrorClass;| Parameter | Type |
|---|---|
definition | ErrorDefinition |
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;| Parameter | Type |
|---|---|
program | Program |
Returns Manifest
detectAgent
function detectAgent(env: Record<string, string | undefined>, tty: boolean, probes?: readonly AgentProbe[]): Detection;| Parameter | Type |
|---|---|
env | Record<string, string | undefined> |
tty | boolean |
probes (optional) | readonly AgentProbe[] |
Returns Detection
execute
function execute(manifest: Manifest, opts?: RunOptions & {
root?: string[];
from?: 'node' | 'user';
}): Promise<void>;| Parameter | Type |
|---|---|
manifest | Manifest |
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;| Parameter | Type |
|---|---|
name | string |
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;| Parameter | Type |
|---|---|
manifest | Manifest |
argv | readonly 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>;| Parameter | Type |
|---|---|
target | Command<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>;| Parameter | Type |
|---|---|
manifest | Manifest |
argv | readonly 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;| Parameter | Type |
|---|---|
name | string |
specs | T |
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:
agentisctx.agent: which agent, if any, the environment names.interactiveis 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.
| Export | Kind | Documented in |
|---|---|---|
isExitCode | function | burgee/testing |
Manifest | class | burgee/schema |
ExitCode | const | burgee/testing |
ExitCodeValue | const | burgee/testing |
Candidate | interface | burgee/config |
CommandSchema | interface | burgee/schema |
HelpOptions | interface | burgee/help |
JsonSchema | interface | burgee/schema |
Layers | interface | burgee/config |
Plugin | interface | burgee/plugin |
ProgramSchema | interface | burgee/schema |
Provenance | interface | burgee/config |
Resolution | interface | burgee/config |
SchemaSummary | interface | burgee/schema |
ServeOptions | interface | burgee/mcp |
Tool | interface | burgee/mcp |
ToolAnnotations | interface | burgee/mcp |
HelpTheme | type | burgee/help |
HelpToken | type | burgee/help |
Invoke | type | burgee/mcp |
PluginErrorCode | type | burgee/plugin |
Source | type | burgee/config |
Plugins
Every layer takes plugins the same way — a plain object, validated against one published schema, checked by the package's own command before it ships. One object can extend all nine.
burgee/plugin
Every export of burgee/plugin, with its signature and doc comment: validate, definePlugin, CONTRACT, PluginError, plus 2 types.