burgee 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,10 @@
19
19
  <img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
20
20
  </p>
21
21
 
22
+ <p align="center">
23
+ Docs: <a href="https://burgee.interlace.tools/docs/packages/burgee">https://burgee.interlace.tools/docs/packages/burgee</a>
24
+ </p>
25
+
22
26
  A **burgee** is the small swallowtail flag a boat flies to say which club or fleet it
23
27
  belongs to — a flag of identity, not of instruction. That is what this framework does for
24
28
  a command-line program: a command declares itself once, and every surface is that
@@ -73,8 +77,10 @@ Nothing here needs keeping in sync, because nothing is written twice.
73
77
 
74
78
  ## Already on commander?
75
79
 
76
- Drop-in compatible with both incumbents, graded by **their own test suites** — 1,215
77
- commander tests and 1,185 yargs tests — with the pass rate published and ratcheting:
80
+ Drop-in compatible with both incumbents, graded by **their own test suites** — 1,360 / 1,360
81
+ of commander's tests and 804 / 804 of yargs' on the
82
+ [compatibility page](https://burgee.interlace.tools/docs/compatibility) — with the pass rate
83
+ published and ratcheting:
78
84
 
79
85
  ```diff
80
86
  - import { Command } from 'commander';
@@ -119,9 +125,46 @@ help from the manifest, plugins with hook filters, and a `burgee/commander` faç
119
125
  runs a real commander program. The dev loop, prompts, lazy commands and groups are not
120
126
  here yet.
121
127
 
122
- Roadmap, architecture and the 101-requirement floor:
128
+ Roadmap, architecture and the 114-requirement floor:
123
129
  <https://github.com/ofri-peretz/burgee>
124
130
 
131
+ ## FAQ
132
+
133
+ ### Is burgee a commander alternative?
134
+
135
+ Yes — and a yargs alternative. It is drop-in compatible with both: change
136
+ `import { Command } from 'commander'` to `import { Command } from 'burgee/commander'` (or
137
+ `yargs` to `burgee/yargs`) and your code and tests are unchanged. Compatibility is graded by
138
+ each host's own test suite in CI, not asserted. Side by side:
139
+ [burgee vs commander](https://burgee.interlace.tools/docs/vs/commander) and
140
+ [burgee vs yargs](https://burgee.interlace.tools/docs/vs/yargs).
141
+
142
+ ### How do I make my CLI usable by an AI agent?
143
+
144
+ Declare it with `defineCommand()` — or keep it on commander or yargs syntax through the
145
+ drop-in front ends. Every command then answers `--json` with one stable envelope,
146
+ `--schema` with the whole command tree as data, and exits `2` when the *command* was wrong
147
+ so an agent knows to rewrite it rather than retry. None of it is written by hand; it is
148
+ the declaration read by a different reader.
149
+ [Your CLI is an agent tool](https://burgee.interlace.tools/docs/agent-surfaces) has each surface.
150
+
151
+ ### How do I expose a CLI over MCP?
152
+
153
+ Run it with `--mcp`: the same manifest is served as MCP tools over stdio. A command becomes
154
+ a tool only when it declares its `effects` (`read_only`, `idempotent` or `non_idempotent`),
155
+ so nothing reaches an agent by accident. Register it with any stdio client:
156
+
157
+ ```json
158
+ { "mcpServers": { "mytool": { "command": "npx", "args": ["mytool", "--mcp"] } } }
159
+ ```
160
+
161
+ ### Does it have dependencies?
162
+
163
+ None outside this repository. `burgee` installs five packages from its own family —
164
+ `bellpull`, `closeout`, `linegauge`, `roundel` and `seniority` — and each of those takes
165
+ nothing from outside it either: one repository, one release pipeline, one supply chain to
166
+ audit.
167
+
125
168
  ---
126
169
 
127
170
  Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on burgee declares
@@ -141,6 +184,7 @@ Graded by the incumbent's own test suite:
141
184
  | suite | passing |
142
185
  | :-- | --: |
143
186
  | `commander` | 1360 / 1360 |
187
+ | `meow` | 132 / 148 |
144
188
  | `yargs` | 804 / 804 |
145
189
  ## Where it sits
146
190
 
@@ -0,0 +1,22 @@
1
+ import { ExitCode } from './exit-code.js';
2
+ export interface CheckReport {
3
+ name: string;
4
+ commands: {
5
+ path: string;
6
+ description: string;
7
+ effects: string;
8
+ }[];
9
+ hooks: {
10
+ stage: 'preRun' | 'postRun' | 'onError';
11
+ applies: string;
12
+ }[];
13
+ }
14
+ export interface CheckRefusal {
15
+ refused: {
16
+ code: string;
17
+ message: string;
18
+ fix: string;
19
+ };
20
+ exitCode: ExitCode;
21
+ }
22
+ export declare function checkPlugin(file: string): Promise<CheckReport | CheckRefusal>;
package/dist/check.js ADDED
@@ -0,0 +1,35 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { ExitCode } from './exit-code.js';
4
+ import { Manifest } from './manifest.js';
5
+ import { PluginError, validate } from './plugin.js';
6
+ const STAGES = ['preRun', 'postRun', 'onError'];
7
+ const refuse = (code, message, fix) => ({ refused: { code, message, fix }, exitCode: ExitCode.RUNTIME });
8
+ export async function checkPlugin(file) {
9
+ const loaded = (await import(pathToFileURL(resolve(file)).href));
10
+ const plugin = loaded.default ?? loaded;
11
+ try {
12
+ validate(plugin, []);
13
+ new Manifest().use(plugin);
14
+ }
15
+ catch (error) {
16
+ if (error instanceof PluginError)
17
+ return refuse(error.code, error.message, error.fix);
18
+ if (error instanceof Error)
19
+ return refuse('E_PLUGIN_SCHEMA', error.message, 'fix the contributed command the message names; it is checked exactly as one of the program’s own');
20
+ throw error;
21
+ }
22
+ const { name, commands = [], hooks = {} } = plugin;
23
+ const report = {
24
+ name,
25
+ commands: commands.map((c) => ({ path: c.path.join(' '), description: c.description ?? '', effects: String(c.effects ?? 'undeclared') })),
26
+ hooks: STAGES.filter((stage) => hooks[stage] !== undefined).map((stage) => ({
27
+ stage,
28
+ applies: hooks[stage]?.filter?.command === undefined ? 'every command' : `commands matching ${String(hooks[stage]?.filter?.command)}`,
29
+ })),
30
+ };
31
+ if (report.commands.length === 0 && report.hooks.length === 0) {
32
+ return refuse('E_NO_CONTRIBUTION', `${name} registers, but contributes nothing burgee reads`, 'add `commands` or `hooks` — a key another package in the family reads is allowed in the same object, but `burgee check` cannot show it');
33
+ }
34
+ return report;
35
+ }
package/dist/cli.d.ts CHANGED
@@ -81,4 +81,10 @@ export declare const migrateCommand: import("./execute.js").Command<{
81
81
  readonly description: "migrate even though the git tree has uncommitted changes";
82
82
  };
83
83
  }>;
84
+ /**
85
+ * `burgee check <plugin-file>` — the feedback loop PRINCIPLES 7 asks every extension surface
86
+ * for, and the one burgee did not have. See `check.ts`: this one returns its report as data, so
87
+ * `--json` is the form an agent that just wrote a plugin reads.
88
+ */
89
+ export declare const pluginCheckCommand: import("./execute.js").Command<import("./execute.js").OptionSpecs>;
84
90
  export declare const program: import("./manifest.js").Manifest;
package/dist/cli.js CHANGED
@@ -133,9 +133,20 @@ export const migrateCommand = defineCommand({
133
133
  return await migrate({ dir: positionals[0] ?? host.cwd(), dryRun: options.dryRun === true, force: options.force === true });
134
134
  },
135
135
  });
136
+ export const pluginCheckCommand = defineCommand({
137
+ name: 'check',
138
+ description: 'Validate a burgee plugin, register it into a throwaway program, and report what it contributes',
139
+ arguments: [{ name: 'file', description: 'the plugin module to check' }],
140
+ effects: 'read_only',
141
+ examples: [
142
+ { command: 'burgee check ./my-plugin.mjs', description: 'what the plugin contributes, or why it was refused' },
143
+ { command: 'burgee check ./my-plugin.mjs --json', description: 'the same, as data; exit 1 on a refusal' },
144
+ ],
145
+ run: async ({ positionals }) => (await import('./check.js')).checkPlugin(positionals[0] ?? ''),
146
+ });
136
147
  export const program = defineProgram({
137
148
  name: 'burgee',
138
149
  description: 'The agent-native CLI framework, and the tools that come with it',
139
- commands: [brandCommand, devCommand, migrateCommand],
150
+ commands: [brandCommand, devCommand, migrateCommand, pluginCheckCommand],
140
151
  });
141
152
  run(program);
@@ -5,7 +5,6 @@ import { stripVTControlCharacters } from 'node:util';
5
5
  import * as crossSpawn from 'bellpull/cross-spawn';
6
6
  import { ExitCode } from '../exit-code.js';
7
7
  import { Manifest } from '../manifest.js';
8
- import { serveMcp } from '../mcp.js';
9
8
  import { host } from '../runtime.js';
10
9
  import { machineJson, schemaOf } from '../schema.js';
11
10
  import { suggestSimilar } from '../suggest.js';
@@ -1473,7 +1472,7 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1473
1472
  await root.parseAsync(args, { from: 'user', stdout: { write: (s) => out.push(s) }, stderr: { write: (s) => err.push(s) }, exit: (c) => void (code = c) });
1474
1473
  return { stdout: out.join(''), stderr: err.join(''), code };
1475
1474
  };
1476
- return serveMcp(this.manifest, { input: host.stdin, output: { write: writeOut }, invoke }).then(() => true);
1475
+ return import('../mcp.js').then(async ({ serveMcp }) => serveMcp(this.manifest, { input: host.stdin, output: { write: writeOut }, invoke })).then(() => true);
1477
1476
  }
1478
1477
  return false;
1479
1478
  }
@@ -1565,6 +1564,10 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1565
1564
  .then(async (value) => {
1566
1565
  await manifest.fire('postRun', name, options);
1567
1566
  settle(value);
1567
+ })
1568
+ .catch(async (cause) => {
1569
+ await manifest.fire('onError', name, options);
1570
+ throw cause;
1568
1571
  });
1569
1572
  }
1570
1573
  _warnDeprecated() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `burgee/config` — the configuration layer, by itself.
3
+ *
4
+ * Precedence, provenance and `--explain` come from `seniority`, the package whose job that
5
+ * is. They were re-exported from the root barrel until the barrel's cost was measured
6
+ * (see `index.ts`): 3,135 bundled bytes on the startup path of every program, for a
7
+ * surface a program only touches when it wants to read or explain its own configuration.
8
+ */
9
+ export { ConfigError, envName, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority/precedence';
10
+ export { explain } from 'seniority/explain';
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ export { ConfigError, envName, resolve, screaming } from 'seniority/precedence';
2
+ export { explain } from 'seniority/explain';
@@ -47,8 +47,17 @@ export declare function checkDefinition(name: string, options: Record<string, Op
47
47
  * second copy of the guard beside `use()`; it is that there is one guard and both callers
48
48
  * reach it, which is the only arrangement a reader can check by looking.
49
49
  *
50
- * `effects` and `runs` are required rather than optional for exactly that reason. An optional
51
- * third argument would be a check a caller can decline by writing nothing, which is the shape
52
- * of the defect `checkEffects` exists to remove, one level up.
50
+ * It takes the declaration whole, for exactly that reason. It took `effects` and `runs` as
51
+ * required arguments so a caller could not decline a check by writing nothing; D1 would have
52
+ * made that five, and a sixth field would make it six. Reading the object means a field the
53
+ * door checks is one no caller has to remember to forward — including whether it runs.
53
54
  */
54
- export declare function checkCommand(name: string, options: Record<string, OptionSpec>, effects: unknown, runs: boolean): void;
55
+ export declare function checkCommand(name: string, declared: Declared): void;
56
+ /** What the door reads of a command: a first-party declaration and a plugin's have the same fields. */
57
+ export interface Declared {
58
+ options?: Record<string, OptionSpec>;
59
+ effects?: unknown;
60
+ deprecated?: boolean | string;
61
+ run?: unknown;
62
+ load?: unknown;
63
+ }
@@ -5,6 +5,7 @@ export const WITHHELD = 'withheld';
5
5
  const DECLARED = [...EFFECTS, WITHHELD];
6
6
  const RESERVED = new Set(['json', 'help', 'schema', 'mcp', 'version', 'explain']);
7
7
  function checkSpec(name, key, spec, options) {
8
+ checkDeprecated(`option "${key}" of "${name}"`, spec.deprecated);
8
9
  if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
9
10
  throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
10
11
  }
@@ -49,7 +50,12 @@ function checkEffects(name, effects, runs) {
49
50
  throw new Error(`burgee: command "${name}" declares effects ${JSON.stringify(effects)}, which is not an effects; use ${DECLARED.join(', ')}`);
50
51
  }
51
52
  }
52
- export function checkCommand(name, options, effects, runs) {
53
- checkDefinition(name, options);
54
- checkEffects(name, effects, runs);
53
+ function checkDeprecated(what, deprecated) {
54
+ if (deprecated === true || deprecated === '')
55
+ throw new Error(`burgee: ${what} is deprecated with no replacement; name it, e.g. deprecated: '--force'`);
56
+ }
57
+ export function checkCommand(name, declared) {
58
+ checkDeprecated(`command "${name}"`, declared.deprecated);
59
+ checkDefinition(name, declared.options ?? {});
60
+ checkEffects(name, declared.effects, declared.run !== undefined || declared.load !== undefined);
55
61
  }
package/dist/execute.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface Command<S extends OptionSpecs = OptionSpecs> {
32
32
  group?: string;
33
33
  epilogue?: string;
34
34
  hidden?: boolean;
35
+ /** What replaces this command, e.g. `'deploy'`: shown in help, `--schema` and the warning. `true` alone is refused (D1). */
35
36
  deprecated?: boolean | string;
36
37
  /**
37
38
  * What running it does to the world (N6). Declaring one of the three is what exposes the
package/dist/execute.js CHANGED
@@ -1,18 +1,15 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
- import { ConfigError, explain, resolve as resolveLayers } from 'seniority/precedence';
3
+ import { ConfigError, resolve as resolveLayers } from 'seniority/precedence';
4
4
  import { detectAgent } from './agent.js';
5
5
  import { checkCommand } from './definition.js';
6
6
  import { ExitCode, isExitCode } from './exit-code.js';
7
- import { renderHelp } from './help.js';
8
7
  import { Manifest, relationsOf } from './manifest.js';
9
- import { serveMcp } from './mcp.js';
10
8
  import { camel, kebab } from './names.js';
11
9
  import { nearestPackage } from './pkg.js';
12
10
  import { host } from './runtime.js';
13
- import { commandSchemaOf, machineJson, schemaOf, summaryOf, typedName } from './schema.js';
14
11
  import { detachedTeardown, processTeardown } from './shutdown.js';
15
- import { checkRelations, coerce, UsageError } from './validate.js';
12
+ import { AuthError, checkRelations, coerce, UsageError } from './validate.js';
16
13
  function helpFields(c) {
17
14
  const node = {};
18
15
  if (c.description !== undefined)
@@ -38,7 +35,7 @@ function helpFields(c) {
38
35
  return node;
39
36
  }
40
37
  export function defineCommand(command) {
41
- checkCommand(command.name, command.options ?? {}, command.effects, command.run !== undefined || command.load !== undefined);
38
+ checkCommand(command.name, command);
42
39
  return command;
43
40
  }
44
41
  function addTree(manifest, parent, commands) {
@@ -182,7 +179,7 @@ async function resolveValues(manifest, specs, values, io) {
182
179
  const out = { values: resolution.values, provenance: resolution.provenance };
183
180
  const asked = values['explain'];
184
181
  if (typeof asked === 'string')
185
- out.explainText = explain(asked, resolution);
182
+ out.explainText = (await import('seniority/explain')).explain(asked, resolution);
186
183
  for (const [name, spec] of Object.entries(specs)) {
187
184
  if (out.values[name] === undefined && spec.required === true && out.explainText === undefined) {
188
185
  throw new UsageError(`missing required option --${kebab(name)}`, `pass --${kebab(name)} <value>`);
@@ -214,6 +211,18 @@ function exitSignal(cause) {
214
211
  const code = cause?.code;
215
212
  return typeof code === 'number' && isExitCode(code) ? code : undefined;
216
213
  }
214
+ const CLASSIFIED = [
215
+ [UsageError, ExitCode.USAGE],
216
+ [AuthError, ExitCode.AUTH],
217
+ [ConfigError, ExitCode.CONFIG],
218
+ ];
219
+ function carried(cause) {
220
+ const { hint, fix } = (cause ?? {});
221
+ return {
222
+ ...(typeof hint === 'string' ? { hint } : {}),
223
+ ...(typeof fix === 'string' ? { fix } : {}),
224
+ };
225
+ }
217
226
  async function describeFailure(cause, argv, node) {
218
227
  const signal = exitSignal(cause);
219
228
  if (signal !== undefined)
@@ -221,12 +230,9 @@ async function describeFailure(cause, argv, node) {
221
230
  const message = cause instanceof Error ? cause.message : String(cause);
222
231
  if (cause instanceof ActionRequired)
223
232
  return { code: ExitCode.CANCELLED, message, action: cause.spec, ...(cause.spec.hint === undefined ? {} : { hint: cause.spec.hint }) };
224
- if (cause instanceof UsageError) {
225
- return { code: ExitCode.USAGE, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
226
- }
227
- if (cause instanceof ConfigError) {
228
- return { code: ExitCode.CONFIG, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
229
- }
233
+ const named = CLASSIFIED.find(([Class]) => cause instanceof Class);
234
+ if (named !== undefined)
235
+ return { code: named[1], message, ...carried(cause) };
230
236
  if (isParseArgsFailure(cause)) {
231
237
  const explain = await import('./unknown-option.js');
232
238
  const dash = explain.singleDashHint(argv);
@@ -251,7 +257,8 @@ function runnableNext(manifest, spec, json) {
251
257
  return (spec.next ?? []).map((n) => ({ command: `${program} ${n.command}${json && !n.command.includes('--json') ? ' --json' : ''}`, when: n.when }));
252
258
  }
253
259
  const HELP_FLAGS = new Set(['--help', '-h']);
254
- function helpDocumentOf(manifest, node) {
260
+ async function helpDocumentOf(manifest, node) {
261
+ const { commandSchemaOf, typedName } = await import('./schema.js');
255
262
  const root = manifest.rootPath;
256
263
  const children = manifest.commands
257
264
  .filter((c) => c.path.length === node.path.length + 1 && c.path.slice(0, node.path.length).join(' ') === node.path.join(' '))
@@ -275,19 +282,24 @@ export function beforeTerminator(argv) {
275
282
  function rootNode(manifest, root) {
276
283
  return manifest.find(root) ?? { path: root, options: {} };
277
284
  }
278
- function unresolved({ manifest, root, io }, argv, at) {
285
+ const renderHelp = async (manifest, node, io) => {
286
+ const help = await import('./help.js');
287
+ return help.renderHelp(manifest, node, { width: io.width, color: help.colorFor(io.env, detectAgent(io.env, io.tty).interactive) });
288
+ };
289
+ const machineJson = async (...args) => (await import('./schema.js')).machineJson(...args);
290
+ async function unresolved({ manifest, root, io }, argv, at) {
279
291
  const node = at ?? rootNode(manifest, root);
280
292
  const typed = argv.slice(node.path.length - root.length);
281
293
  const first = typed[0] ?? '';
282
294
  if (typed.length > 0 && HELP_FLAGS.has(first)) {
283
295
  if (beforeTerminator(typed).includes('--json'))
284
- return { text: `${machineJson(helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
285
- return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.OK };
296
+ return { text: `${await machineJson(await helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
297
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.OK };
286
298
  }
287
299
  if (first === '--version' || first === '-V')
288
300
  return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
289
301
  if (typed.length === 0)
290
- return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.USAGE };
302
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.USAGE };
291
303
  throw new UsageError(`unknown command "${typed[0] ?? ''}"`, 'run --help to see the available commands');
292
304
  }
293
305
  async function completion(manifest, argv, io) {
@@ -310,11 +322,11 @@ async function surface(manifest, argv, io) {
310
322
  if (await completion(manifest, argv, io))
311
323
  return true;
312
324
  if (argv[0] === 'help') {
313
- io.out.write(helpCommand(manifest, argv.slice(1), manifest.rootPath, io.width));
325
+ io.out.write(await helpCommand(manifest, argv.slice(1), manifest.rootPath, io));
314
326
  return true;
315
327
  }
316
328
  if (head.includes('--schema')) {
317
- io.out.write(`${machineJson(schemaSurface(manifest, argv), head)}\n`);
329
+ io.out.write(`${await machineJson(await schemaSurface(manifest, argv), head)}\n`);
318
330
  return true;
319
331
  }
320
332
  if (head[0] === '--mcp') {
@@ -333,13 +345,15 @@ async function surface(manifest, argv, io) {
333
345
  });
334
346
  return { stdout: out.join(''), stderr: err.join(''), code };
335
347
  };
348
+ const { serveMcp } = await import('./mcp.js');
336
349
  await serveMcp(manifest, { input: io.stdin, output: io.out, invoke });
337
350
  return true;
338
351
  }
339
352
  return false;
340
353
  }
341
354
  const SCHEMA_BUDGET = 48_000;
342
- function schemaSurface(manifest, argv) {
355
+ async function schemaSurface(manifest, argv) {
356
+ const { commandSchemaOf, schemaOf, summaryOf } = await import('./schema.js');
343
357
  const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema' && !a.startsWith('--format=')), manifest.rootPath);
344
358
  if (node?.run !== undefined)
345
359
  return commandSchemaOf(node, manifest.rootPath);
@@ -347,9 +361,9 @@ function schemaSurface(manifest, argv) {
347
361
  const budget = manifest.schemaBudget ?? SCHEMA_BUDGET;
348
362
  return JSON.stringify(full).length <= budget ? full : summaryOf(manifest, budget);
349
363
  }
350
- function helpCommand(manifest, argv, root, width) {
364
+ async function helpCommand(manifest, argv, root, io) {
351
365
  const { node } = manifest.resolve(argv, root);
352
- return renderHelp(manifest, node ?? rootNode(manifest, root), { width });
366
+ return renderHelp(manifest, node ?? rootNode(manifest, root), io);
353
367
  }
354
368
  function changedOf(node, data) {
355
369
  const value = isPlainObject(data) ? data['changed'] : undefined;
@@ -375,9 +389,9 @@ async function dispatch(manifest, { node, rest, name }, io) {
375
389
  const flags = canonical(parsed.values, node.options, parsed.tokens);
376
390
  const json = flags.json === true;
377
391
  if (flags.help === true && json)
378
- return { json, text: `${machineJson(helpDocumentOf(manifest, node), json ? ['--json'] : [])}\n` };
392
+ return { json, text: `${await machineJson(await helpDocumentOf(manifest, node), json ? ['--json'] : [])}\n` };
379
393
  if (flags.help === true)
380
- return { json, text: renderHelp(manifest, node, { width: io.width }) };
394
+ return { json, text: await renderHelp(manifest, node, io) };
381
395
  if (flags.version === true)
382
396
  return { json, text: `${versionOf(manifest, io)}\n` };
383
397
  const resolved = await resolveValues(manifest, node.options, flags, io);
@@ -469,7 +483,7 @@ export async function execute(manifest, opts = {}) {
469
483
  return await leave(io, ExitCode.OK);
470
484
  const { node, rest } = manifest.resolve(argv, root);
471
485
  if (node?.run === undefined) {
472
- const { text, code } = unresolved({ manifest, root, io }, argv, node);
486
+ const { text, code } = await unresolved({ manifest, root, io }, argv, node);
473
487
  (code === ExitCode.OK ? io.out : io.err).write(text);
474
488
  return await leave(io, code);
475
489
  }
@@ -10,9 +10,25 @@ export declare const ExitCode: {
10
10
  readonly CONFIG: 3;
11
11
  /** The user or caller cancelled. */
12
12
  readonly CANCELLED: 4;
13
+ /**
14
+ * E6 — the far side said no: a credential is missing, expired, or refused.
15
+ *
16
+ * Its own code because it is the most actionable one in the survey. `RUNTIME` means *it
17
+ * failed, read the message*; `AUTH` means *log in and run it again*, and a script or an
18
+ * agent can branch on that without parsing prose. `USAGE` says fix the script, `CONFIG`
19
+ * says fix the runner, and this says fix the credential — three different responses that
20
+ * collapsed into one code before it existed.
21
+ *
22
+ * **5, where `gh` uses 4.** Four is `CANCELLED` here and has been since the contract was
23
+ * written, and moving a published code to match another tool's is a breaking change for
24
+ * every consumer that already branches on it. The survey's other citation, `aws` v2, uses
25
+ * 252/253/254 and agrees with nobody either; what matters is that the code is stable and
26
+ * documented, not that it matches a particular neighbour.
27
+ */
28
+ readonly AUTH: 5;
13
29
  /** SIGINT after the terminal was restored (E5). */
14
30
  readonly SIGINT: 130;
15
31
  };
16
32
  export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
17
- /** True for the six codes in the contract and nothing else. */
33
+ /** True for the seven codes in the contract and nothing else. */
18
34
  export declare function isExitCode(n: unknown): n is ExitCode;
package/dist/exit-code.js CHANGED
@@ -4,6 +4,7 @@ export const ExitCode = {
4
4
  USAGE: 2,
5
5
  CONFIG: 3,
6
6
  CANCELLED: 4,
7
+ AUTH: 5,
7
8
  SIGINT: 130,
8
9
  };
9
10
  const CODES = new Set(Object.values(ExitCode));
@@ -0,0 +1,2 @@
1
+ /** `burgee/help` — the help renderer, by itself. See `index.ts` for why it is not in the barrel. */
2
+ export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
@@ -0,0 +1 @@
1
+ export { renderHelp } from './help.js';
package/dist/help.d.ts CHANGED
@@ -24,6 +24,18 @@ export interface HelpOptions {
24
24
  */
25
25
  theme?: HelpTheme;
26
26
  }
27
+ /**
28
+ * Whether the engine colours help (O2). `FORCE_COLOR` decides when set — `0` and `false`
29
+ * off, anything else, the empty string included, on — so it overrides a pipe and
30
+ * `NO_COLOR` both, as Node's own `getColorDepth` does. Otherwise colour needs someone to
31
+ * see it: an interactive terminal (the caller's answer, in which a detected agent is not
32
+ * one, N12), no non-empty `NO_COLOR`, and a `TERM` other than `dumb`.
33
+ *
34
+ * It lives here, not in the engine, so the startup path pays for none of it (W4).
35
+ * Not `tty.WriteStream.prototype.hasColors(env)`, which gives the same answers: with
36
+ * both variables set it calls `process.emitWarning`, and this runs under an injected env.
37
+ */
38
+ export declare function colorFor(env: Record<string, string | undefined>, interactive: boolean): boolean;
27
39
  /**
28
40
  * Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120).
29
41
  *
package/dist/help.js CHANGED
@@ -3,6 +3,12 @@ import { width as displayWidth, widest } from 'linegauge';
3
3
  import { flagsOf, kebab } from './names.js';
4
4
  const identity = (s) => s;
5
5
  const PLAIN = { heading: identity, command: identity, flag: identity, value: identity };
6
+ export function colorFor(env, interactive) {
7
+ const force = env['FORCE_COLOR'];
8
+ if (force !== undefined)
9
+ return force !== '0' && force !== 'false';
10
+ return interactive && !env['NO_COLOR'] && env['TERM'] !== 'dumb';
11
+ }
6
12
  const DEFAULTS = {
7
13
  heading: (s) => styleText('bold', s, { validateStream: false }),
8
14
  command: (s) => styleText('bold', s, { validateStream: false }),
package/dist/index.d.ts CHANGED
@@ -4,15 +4,40 @@
4
4
  *
5
5
  * The public entry, and only that. The execution core lives in execute.ts so a
6
6
  * façade can import it without pulling this barrel.
7
+ *
8
+ * ## What is not here, and why (D-093, reversed on a measurement)
9
+ *
10
+ * This barrel used to re-export the value half of `help.ts`, `mcp.ts`, `schema.ts`,
11
+ * `plugin.ts`, `manifest.ts` and `seniority/precedence` as a convenience. A re-export is
12
+ * not free: it makes those modules live for every consumer of `burgee`, whether or not
13
+ * anything reads them. `execute.ts` loads each one behind an `await import()`, so the only
14
+ * thing keeping them on the startup path was this file.
15
+ *
16
+ * Measured 2026-09-21 with `--splitting --outdir` over the transitive closure of `import`
17
+ * statements — the initial load a consumer actually pays:
18
+ *
19
+ * - `import { run } from 'burgee'` — **44,663 bytes**
20
+ * - the same program against `execute.ts` directly — **31,047 bytes**
21
+ * - cold start, `burgee ÷ cac` — **2.113 → 1.717**
22
+ *
23
+ * D-093 declined this split on a cold-start argument it did not have the number for. The
24
+ * number says 13,616 bytes and 19% of startup, so the split lands: every value moved here
25
+ * has a subpath of its own (`burgee/help`, `burgee/mcp`, `burgee/schema`, `burgee/plugin`,
26
+ * `burgee/config`), which is where a program that wants it should say so.
27
+ *
28
+ * **Every `type` stays.** A type re-export is erased and costs a consumer nothing, so the
29
+ * whole type surface is still importable from `burgee` and no typed program has to move.
7
30
  */
8
31
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
9
32
  export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, type AnyCommand, type Command, type CommandContext, type InferOptions, type OptionSpecs, type Program, type RunOptions, type RunResult, } from './execute.js';
10
33
  export { checkCommand, checkDefinition } from './definition.js';
11
- export { camel, kebab, UsageError } from './validate.js';
34
+ export { AuthError, camel, kebab, UsageError } from './validate.js';
12
35
  export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
13
- export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
14
- export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
15
- export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority/precedence';
16
- export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
17
- export { CONTRACT, definePlugin, PluginError, type PluginErrorCode } from './plugin.js';
18
- export { Manifest, type ArgumentSpec, type CommandNode, type Effects, type Example, type Hook, type LazyModule, type OptionSpec, type ActionRequiredSpec, type Plugin, type Relation, type RunContext, type StandardResult, type StandardSchemaV1, } from './manifest.js';
36
+ export type { HelpOptions, HelpTheme, HelpToken } from './help.js';
37
+ export type { Invoke, ServeOptions, Tool, ToolAnnotations } from './mcp.js';
38
+ export type { Candidate, Layers, Provenance, Resolution, Source } from 'seniority/precedence';
39
+ export type { CommandSchema, JsonSchema, ProgramSchema, SchemaSummary } from './schema.js';
40
+ export type { PluginErrorCode } from './plugin.js';
41
+ export type {
42
+ /** The class itself lives at `burgee/schema`; the *type* stays here so `defineProgram`'s return type is nameable. */
43
+ Manifest, ArgumentSpec, CommandNode, Effects, Example, Hook, LazyModule, OptionSpec, ActionRequiredSpec, Plugin, Relation, RunContext, StandardResult, StandardSchemaV1, } from './manifest.js';
package/dist/index.js CHANGED
@@ -1,11 +1,5 @@
1
1
  export { ExitCode, isExitCode } from './exit-code.js';
2
2
  export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, } from './execute.js';
3
3
  export { checkCommand, checkDefinition } from './definition.js';
4
- export { camel, kebab, UsageError } from './validate.js';
4
+ export { AuthError, camel, kebab, UsageError } from './validate.js';
5
5
  export { AGENT_PROBES, detectAgent } from './agent.js';
6
- export { renderHelp } from './help.js';
7
- export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf } from './mcp.js';
8
- export { ConfigError, envName, explain, resolve, screaming } from 'seniority/precedence';
9
- export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf } from './schema.js';
10
- export { CONTRACT, definePlugin, PluginError } from './plugin.js';
11
- export { Manifest, } from './manifest.js';
@@ -0,0 +1,2 @@
1
+ /** `burgee/mcp` — the MCP server, by itself. See `index.ts` for why it is not in the barrel. */
2
+ export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
@@ -0,0 +1 @@
1
+ export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf } from './mcp.js';
@@ -0,0 +1,21 @@
1
+ import { type AnyFlag, type Options } from './types.js';
2
+ /** Where a command run's parent arguments end and the child's begin. */
3
+ export interface Split {
4
+ parent: string[];
5
+ input: string[];
6
+ command?: string;
7
+ unknownCommand?: string;
8
+ }
9
+ /** Every flag name a caller may write, and the canonical name each maps to. */
10
+ export declare function aliasMap(flags: Record<string, AnyFlag>): Record<string, string[]>;
11
+ export declare function typeofDefault(value: unknown): 'string' | 'boolean' | 'number' | undefined;
12
+ /**
13
+ * Where the parent's arguments end and the command's begin.
14
+ *
15
+ * The first token the parser reads as positional is the command word; everything after it in
16
+ * the *raw* argv is the child's, unparsed. `--` is not a fence here — the suite states that
17
+ * `-- --unknown run` reports `Unknown command: --unknown`, so a post-separator word is a
18
+ * candidate command like any other.
19
+ */
20
+ export declare function splitAtCommand(argv: string[], parserOptions: Record<string, unknown>, opts: Options): Split;
21
+ /** stderr and exit 2 — how meow ends a run the flags do not support. */