burgee 0.6.0 → 0.7.0

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/dist/execute.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
- import { ConfigError, explain, resolve as resolveLayers } from 'seniority';
3
+ import { ConfigError, explain, resolve as resolveLayers } from 'seniority/precedence';
4
4
  import { detectAgent } from './agent.js';
5
+ import { checkCommand } from './definition.js';
5
6
  import { ExitCode, isExitCode } from './exit-code.js';
6
7
  import { renderHelp } from './help.js';
7
- import { Manifest } from './manifest.js';
8
+ import { Manifest, relationsOf } from './manifest.js';
8
9
  import { serveMcp } from './mcp.js';
9
10
  import { camel, kebab } from './names.js';
10
11
  import { nearestPackage } from './pkg.js';
11
- import { commandSchemaOf, machineJson, schemaOf, summaryOf } from './schema.js';
12
- import { checkDefinition, checkRelations, coerce, UsageError } from './validate.js';
12
+ import { host } from './runtime.js';
13
+ import { commandSchemaOf, machineJson, schemaOf, summaryOf, typedName } from './schema.js';
14
+ import { detachedTeardown, processTeardown } from './shutdown.js';
15
+ import { checkRelations, coerce, UsageError } from './validate.js';
13
16
  function helpFields(c) {
14
17
  const node = {};
15
18
  if (c.description !== undefined)
@@ -34,13 +37,8 @@ function helpFields(c) {
34
37
  node.relations = c.relations;
35
38
  return node;
36
39
  }
37
- const RESERVED = new Set(['json', 'help', 'schema', 'mcp', 'version', 'explain']);
38
40
  export function defineCommand(command) {
39
- for (const name of Object.keys(command.options ?? {})) {
40
- if (RESERVED.has(name) || RESERVED.has(kebab(name)))
41
- throw new Error(`burgee: option "${name}" is reserved and cannot be redefined`);
42
- }
43
- checkDefinition(command.name, command.options ?? {});
41
+ checkCommand(command.name, command.options ?? {}, command.effects, command.run !== undefined || command.load !== undefined);
44
42
  return command;
45
43
  }
46
44
  function addTree(manifest, parent, commands) {
@@ -94,6 +92,9 @@ class ExitSignal extends Error {
94
92
  this.code = code;
95
93
  }
96
94
  }
95
+ const ctxExit = (code) => {
96
+ throw new ExitSignal(code);
97
+ };
97
98
  function isPlainObject(value) {
98
99
  return typeof value === 'object' && value !== null && !Array.isArray(value);
99
100
  }
@@ -159,7 +160,7 @@ function packageLayer(pkg, name) {
159
160
  return typeof field === 'object' && field !== null && !Array.isArray(field) ? { path: pkg.path, data: field } : undefined;
160
161
  }
161
162
  async function configLayers(name, values, io) {
162
- const { discover } = await import('seniority');
163
+ const { discover } = await import('seniority/config');
163
164
  const explicit = values['config'];
164
165
  const disabled = values['noConfig'] === true;
165
166
  const loaded = await discover({ name, cwd: io.cwd, env: io.env, ...(typeof explicit === 'string' ? { explicit } : {}), disabled });
@@ -238,19 +239,35 @@ async function describeFailure(cause, argv, node) {
238
239
  }
239
240
  function textFailure(failure) {
240
241
  const hint = failure.hint === undefined ? '' : `hint: ${failure.hint}\n`;
242
+ const fix = failure.fix === undefined ? '' : `fix: ${failure.fix}\n`;
241
243
  if (failure.action !== undefined) {
242
244
  const next = (failure.action.next ?? []).map((n) => ` ${n.command} ${n.when}\n`).join('');
243
245
  return `action required (${failure.action.reason}): ${failure.message}\n${next === '' ? '' : `next:\n${next}`}${hint}`;
244
246
  }
245
- return `error: ${failure.message}\n${hint}`;
247
+ return `error: ${failure.message}\n${hint}${fix}`;
246
248
  }
247
249
  function runnableNext(manifest, spec, json) {
248
250
  const program = manifest.rootPath.join(' ');
249
251
  return (spec.next ?? []).map((n) => ({ command: `${program} ${n.command}${json && !n.command.includes('--json') ? ' --json' : ''}`, when: n.when }));
250
252
  }
251
253
  const HELP_FLAGS = new Set(['--help', '-h']);
254
+ function helpDocumentOf(manifest, node) {
255
+ const root = manifest.rootPath;
256
+ const children = manifest.commands
257
+ .filter((c) => c.path.length === node.path.length + 1 && c.path.slice(0, node.path.length).join(' ') === node.path.join(' '))
258
+ .map((c) => typedName(c, root));
259
+ return {
260
+ schemaVersion: 1,
261
+ ...commandSchemaOf(node, root),
262
+ ...(children.length === 0 ? {} : { commands: children }),
263
+ };
264
+ }
252
265
  const HELP_WIDTH = 100;
253
- const processExit = (code) => process.exit(code);
266
+ const processExit = (code) => host.exit(code);
267
+ async function leave(io, code) {
268
+ await io.teardown.run(code);
269
+ io.exit(code);
270
+ }
254
271
  export function beforeTerminator(argv) {
255
272
  const at = argv.indexOf('--');
256
273
  return at === -1 ? argv : argv.slice(0, at);
@@ -262,8 +279,11 @@ function unresolved({ manifest, root, io }, argv, at) {
262
279
  const node = at ?? rootNode(manifest, root);
263
280
  const typed = argv.slice(node.path.length - root.length);
264
281
  const first = typed[0] ?? '';
265
- if (typed.length > 0 && HELP_FLAGS.has(first))
282
+ if (typed.length > 0 && HELP_FLAGS.has(first)) {
283
+ if (beforeTerminator(typed).includes('--json'))
284
+ return { text: `${machineJson(helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
266
285
  return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.OK };
286
+ }
267
287
  if (first === '--version' || first === '-V')
268
288
  return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
269
289
  if (typed.length === 0)
@@ -350,6 +370,8 @@ async function dispatch(manifest, { node, rest, name }, io) {
350
370
  const parsed = parseArgs({ args: rest, options: toParseConfig(node.options, manifest.config !== undefined), allowPositionals: true, strict: true, tokens: true });
351
371
  const flags = canonical(parsed.values, node.options, parsed.tokens);
352
372
  const json = flags.json === true;
373
+ if (flags.help === true && json)
374
+ return { json, text: `${machineJson(helpDocumentOf(manifest, node), json ? ['--json'] : [])}\n` };
353
375
  if (flags.help === true)
354
376
  return { json, text: renderHelp(manifest, node, { width: io.width }) };
355
377
  if (flags.version === true)
@@ -358,18 +380,15 @@ async function dispatch(manifest, { node, rest, name }, io) {
358
380
  if (resolved.explainText !== undefined)
359
381
  return { json, text: resolved.explainText };
360
382
  const { provenance } = resolved;
361
- checkRelations(node.relations, resolved.values, provenance);
383
+ checkRelations(relationsOf(node), resolved.values, provenance);
362
384
  const values = await coerce(node.options, resolved.values);
363
385
  const { positionals, passthrough } = splitPositionals(parsed.tokens);
364
386
  requirePositionals(node, positionals);
365
387
  warnDeprecated(node, io);
366
388
  await manifest.fire('preRun', name, values);
367
- const exit = (code) => {
368
- io.exit(code);
369
- throw new ExitSignal(code);
370
- };
371
389
  const detection = detectAgent(io.env, io.tty);
372
- const data = await node.run({ options: values, positionals, passthrough, env: io.env, exit, actionRequired, ...detection });
390
+ const onExit = (handler, label) => io.teardown.add(handler, label);
391
+ const data = await node.run({ options: values, positionals, passthrough, env: io.env, exit: ctxExit, onExit, actionRequired, ...detection });
373
392
  await manifest.fire('postRun', name, values);
374
393
  const changed = changedOf(node, data);
375
394
  return { json, data, provenance, ...(changed === undefined ? {} : { changed }) };
@@ -393,66 +412,67 @@ function warnDeprecated(node, io) {
393
412
  const use = typeof node.deprecated === 'string' ? `, use '${node.deprecated}'` : '';
394
413
  io.err.write(`warning: '${typed}' is deprecated${use}\n`);
395
414
  }
396
- function emit(io, outcome) {
415
+ async function emit(io, outcome) {
397
416
  if (outcome.text !== undefined) {
398
417
  io.out.write(outcome.text);
399
- return io.exit(ExitCode.OK);
418
+ return await leave(io, ExitCode.OK);
400
419
  }
401
420
  const meta = { provenance: outcome.provenance ?? {}, ...(outcome.changed === undefined ? {} : { changed: outcome.changed }) };
402
421
  const envelope = { ok: true, data: outcome.data, meta };
403
422
  io.out.write(outcome.json ? `${JSON.stringify(envelope)}\n` : `${render(outcome.data)}\n`);
404
- return io.exit(ExitCode.OK);
423
+ return await leave(io, ExitCode.OK);
405
424
  }
406
425
  async function report(cause, { manifest, io, argv, json, name }) {
407
426
  const failure = await describeFailure(cause, argv, resolveCommand(manifest, argv) ?? undefined);
408
427
  if (failure.silent === true)
409
- return io.exit(failure.code);
428
+ return await leave(io, failure.code);
410
429
  await manifest.fire('onError', name, {});
411
430
  if (failure.action !== undefined) {
412
431
  const next = runnableNext(manifest, failure.action, json);
413
432
  const rendered = { ...failure, action: { ...failure.action, next } };
414
433
  const body = { ok: false, status: 'action_required', reason: failure.action.reason, message: failure.message, next, hint: failure.hint, error: { code: failure.code, message: failure.message } };
415
434
  io.err.write(json ? `${JSON.stringify(body)}\n` : textFailure(rendered));
416
- return io.exit(failure.code);
435
+ return await leave(io, failure.code);
417
436
  }
418
- const body = { code: failure.code, message: failure.message, hint: failure.hint };
437
+ const body = { code: failure.code, message: failure.message, hint: failure.hint, ...(failure.fix === undefined ? {} : { fix: failure.fix }) };
419
438
  io.err.write(json ? `${JSON.stringify({ ok: false, error: body })}\n` : textFailure(failure));
420
- return io.exit(failure.code);
439
+ return await leave(io, failure.code);
421
440
  }
422
441
  function ioOf(opts) {
423
- const out = opts.stdout ?? process.stdout;
442
+ const out = opts.stdout ?? host.stdout;
424
443
  return {
425
444
  out,
426
- err: opts.stderr ?? process.stderr,
427
- env: opts.env ?? process.env,
445
+ err: opts.stderr ?? host.stderr,
446
+ env: opts.env ?? host.env,
428
447
  exit: opts.exit ?? processExit,
429
448
  width: out.columns ?? HELP_WIDTH,
430
- stdin: opts.stdin ?? process.stdin,
431
- cwd: opts.cwd ?? process.cwd(),
432
- pkg: nearestPackage(dirname(opts.entry ?? process.argv[1] ?? process.cwd())),
449
+ stdin: opts.stdin ?? host.stdin,
450
+ cwd: opts.cwd ?? host.cwd(),
451
+ pkg: nearestPackage(dirname(opts.entry ?? host.argv[1] ?? host.cwd())),
433
452
  tty: out.isTTY === true,
453
+ teardown: opts.exit === undefined ? processTeardown([host.stdout, host.stderr]) : detachedTeardown(),
434
454
  };
435
455
  }
436
456
  export async function execute(manifest, opts = {}) {
437
457
  const io = ioOf(opts);
438
- const raw = opts.argv ?? process.argv;
458
+ const raw = opts.argv ?? host.argv;
439
459
  const argv = opts.argv === undefined || opts.from === 'node' ? raw.slice(2) : raw;
440
460
  const root = opts.root ?? manifest.rootPath;
441
461
  let json = beforeTerminator(argv).includes('--json');
442
462
  let name = '';
443
463
  try {
444
464
  if (await surface(manifest, argv, io))
445
- return io.exit(ExitCode.OK);
465
+ return await leave(io, ExitCode.OK);
446
466
  const { node, rest } = manifest.resolve(argv, root);
447
467
  if (node?.run === undefined) {
448
468
  const { text, code } = unresolved({ manifest, root, io }, argv, node);
449
469
  (code === ExitCode.OK ? io.out : io.err).write(text);
450
- return io.exit(code);
470
+ return await leave(io, code);
451
471
  }
452
472
  name = node.path.slice(root.length).join(' ');
453
473
  const outcome = await dispatch(manifest, { node: node, rest, name }, io);
454
474
  json = outcome.json;
455
- return emit(io, outcome);
475
+ return await emit(io, outcome);
456
476
  }
457
477
  catch (cause) {
458
478
  return await report(cause, { manifest, io, argv, json, name });
package/dist/help.d.ts CHANGED
@@ -24,7 +24,12 @@ export interface HelpOptions {
24
24
  */
25
25
  theme?: HelpTheme;
26
26
  }
27
- /** Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120). */
27
+ /**
28
+ * Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120).
29
+ *
30
+ * The running `used` is the width of `current` in columns, carried rather than recomputed:
31
+ * measuring the accumulated line once per word would make a long paragraph quadratic.
32
+ */
28
33
  export declare function wrap(text: string, width: number): string[];
29
34
  /**
30
35
  * Render help for one node — a runnable command, a group, or both — as text.
package/dist/help.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { styleText } from 'node:util';
2
- import { kebab } from './names.js';
2
+ import { width as displayWidth, widest } from 'linegauge';
3
+ import { flagsOf, kebab } from './names.js';
3
4
  const identity = (s) => s;
4
5
  const PLAIN = { heading: identity, command: identity, flag: identity, value: identity };
5
6
  const DEFAULTS = {
@@ -47,6 +48,10 @@ function annotate(text, spec, verbose) {
47
48
  parts.push(`(one of: ${spec.choices.join(', ')})`);
48
49
  if (spec.multiple === true)
49
50
  parts.push('(repeatable)');
51
+ if (spec.dependsOn !== undefined && spec.dependsOn.length > 0)
52
+ parts.push(`(requires ${flagsOf(spec.dependsOn).join(', ')})`);
53
+ if (spec.exclusive !== undefined && spec.exclusive.length > 0)
54
+ parts.push(`(conflicts with ${flagsOf(spec.exclusive).join(', ')})`);
50
55
  if (spec.env !== undefined)
51
56
  parts.push(`[env: ${spec.env}]`);
52
57
  if (verbose)
@@ -108,26 +113,38 @@ function usageLine(node, root, hasChildren, paint) {
108
113
  export function wrap(text, width) {
109
114
  const out = [];
110
115
  for (const line of text.split('\n')) {
111
- if (/^\s/.test(line) || line.length <= width) {
116
+ if (/^\s/.test(line) || displayWidth(line) <= width)
112
117
  out.push(line);
113
- continue;
118
+ else
119
+ out.push(...wrapLine(line, width));
120
+ }
121
+ return out;
122
+ }
123
+ function wrapLine(line, width) {
124
+ const rows = [];
125
+ let current = '';
126
+ let used = 0;
127
+ for (const word of line.split(' ')) {
128
+ const w = displayWidth(word);
129
+ if (current === '') {
130
+ current = word;
131
+ used = w;
132
+ }
133
+ else if (used + 1 + w > width) {
134
+ rows.push(current);
135
+ current = word;
136
+ used = w;
114
137
  }
115
- let current = '';
116
- for (const word of line.split(' ')) {
117
- if (current !== '' && current.length + 1 + word.length > width) {
118
- out.push(current);
119
- current = word;
120
- }
121
- else
122
- current = current === '' ? word : `${current} ${word}`;
138
+ else {
139
+ current = `${current} ${word}`;
140
+ used += 1 + w;
123
141
  }
124
- out.push(current);
125
142
  }
126
- return out;
143
+ rows.push(current);
144
+ return rows;
127
145
  }
128
146
  function termColumn(rows, width) {
129
- const longest = Math.max(0, ...rows.map((r) => r.term.length));
130
- return Math.min(longest, Math.floor(width * TERM_SHARE));
147
+ return Math.min(widest(rows.map((r) => r.term)), Math.floor(width * TERM_SHARE));
131
148
  }
132
149
  function layout(rows, width, column, paint) {
133
150
  const textWidth = Math.max(1, width - INDENT.length - column - GUTTER);
@@ -140,11 +157,12 @@ function layout(rows, width, column, paint) {
140
157
  }
141
158
  const wrapped = wrap(text, textWidth);
142
159
  const continuation = INDENT + ' '.repeat(column + GUTTER);
143
- if (term.length > column) {
160
+ const termWidth = displayWidth(term);
161
+ if (termWidth > column) {
144
162
  lines.push(`${INDENT}${cell}`, ...wrapped.map((l) => `${continuation}${l}`));
145
163
  continue;
146
164
  }
147
- const pad = ' '.repeat(column + GUTTER - term.length);
165
+ const pad = ' '.repeat(column + GUTTER - termWidth);
148
166
  lines.push(`${INDENT}${cell}${pad}${wrapped[0] ?? ''}`, ...wrapped.slice(1).map((l) => `${continuation}${l}`));
149
167
  }
150
168
  return lines;
package/dist/index.d.ts CHANGED
@@ -7,10 +7,12 @@
7
7
  */
8
8
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
9
9
  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
- export { camel, checkDefinition, kebab, UsageError } from './validate.js';
10
+ export { checkCommand, checkDefinition } from './definition.js';
11
+ export { camel, kebab, UsageError } from './validate.js';
11
12
  export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
12
13
  export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
13
14
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
14
- export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority';
15
+ export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority/precedence';
15
16
  export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
16
- export { definePlugin, 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';
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';
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  export { ExitCode, isExitCode } from './exit-code.js';
2
2
  export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, } from './execute.js';
3
- export { camel, checkDefinition, kebab, UsageError } from './validate.js';
3
+ export { checkCommand, checkDefinition } from './definition.js';
4
+ export { camel, kebab, UsageError } from './validate.js';
4
5
  export { AGENT_PROBES, detectAgent } from './agent.js';
5
6
  export { renderHelp } from './help.js';
6
7
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf } from './mcp.js';
7
- export { ConfigError, envName, explain, resolve, screaming } from 'seniority';
8
+ export { ConfigError, envName, explain, resolve, screaming } from 'seniority/precedence';
8
9
  export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf } from './schema.js';
9
- export { definePlugin, Manifest, } from './manifest.js';
10
+ export { CONTRACT, definePlugin, PluginError } from './plugin.js';
11
+ export { Manifest, } from './manifest.js';
@@ -4,7 +4,12 @@
4
4
  * Commands reach it through a façade (`burgee/commander`, `burgee/yargs`) or
5
5
  * natively, and it does not record which. That is the whole reason a plugin
6
6
  * written once works on every rung of the adoption ladder (J7, J8).
7
+ *
8
+ * The plugin *shape* and its refusals live in `./plugin.js`, which is this package's host in
9
+ * the sense `plugin-contract` means: one file per package owning `Plugin`, `validate()` and
10
+ * the error vocabulary. `use()` below is burgee's `register()`.
7
11
  */
12
+ import { type Plugin } from './plugin.js';
8
13
  /**
9
14
  * The Standard Schema interface (standardschema.dev), declared here so any implementation
10
15
  * — zod, valibot, arktype — is accepted as an option's `schema` without a dependency (S1).
@@ -42,6 +47,26 @@ export interface OptionSpec {
42
47
  /** Repeatable, and split on `separator` (`,` unless declared): `--tag a --tag b,c` → `['a', 'b', 'c']` (S8). */
43
48
  multiple?: boolean;
44
49
  separator?: string;
50
+ /**
51
+ * The other options this one requires: `--out --dependsOn force` is a usage error without
52
+ * `--force` (S2). Sugar for a `{ implies: [this, other] }` relation per name, and nothing
53
+ * else — one engine, one order, one error vocabulary.
54
+ *
55
+ * It exists as a second spelling because the first states the constraint away from the
56
+ * option it constrains: a reader looking at `out` learns nothing from a `relations` entry
57
+ * three keys down, and neither does the help line for `--out`. Both incumbents spell it on
58
+ * the option (commander `.implies()`, yargs `.implies()`), and so does Fig, whose `Option`
59
+ * declares this exact key.
60
+ */
61
+ dependsOn?: readonly string[];
62
+ /**
63
+ * The other options this one may not be given with (S2): `{ conflicts: [this, other] }` per
64
+ * name. Commander's `.conflicts()`, yargs' `.conflicts()`, Fig's `exclusiveOn`.
65
+ *
66
+ * One-sided is enough — the relation it compiles to holds whichever of the two argv names
67
+ * first — so declare it once, on whichever option the constraint belongs to.
68
+ */
69
+ exclusive?: readonly string[];
45
70
  /** `number` only. */
46
71
  minimum?: number;
47
72
  maximum?: number;
@@ -68,6 +93,25 @@ export interface LazyModule {
68
93
  * to true, so silence is the dangerous reading.
69
94
  */
70
95
  export type Effects = 'read_only' | 'idempotent' | 'non_idempotent';
96
+ /**
97
+ * What a command may declare under `effects`: one of the three above, or `'withheld'`.
98
+ *
99
+ * `'withheld'` is not an effect and is deliberately not spelled like one. It answers a
100
+ * different question — *may an agent call this?* — and it exists because the two questions
101
+ * used to share one absence. `effects: undefined` meant both "I decided agents should not
102
+ * have this" and "I forgot", and the second is the one that ships: the tool an author built
103
+ * for an agent was simply not in `tools/list`, and nothing said so (N6).
104
+ *
105
+ * It is not `'none'`, which reads as *this command has no effects* — which is `read_only`,
106
+ * the one value it could be confused with and the one confusion that would matter. Nor is it
107
+ * a second field: a boolean beside a now-required `effects` would mean that declaring what a
108
+ * command does to the world silently opts it in, and the default for *that* field would be
109
+ * the silence this change exists to remove. One field, four answers, no default.
110
+ *
111
+ * {@link Effects} stays the three, because `annotationsOf` is total on them: a withheld
112
+ * command has no hints, because it has no tool.
113
+ */
114
+ export type DeclaredEffects = Effects | 'withheld';
71
115
  /**
72
116
  * A relationship between options, validated after parsing and before choices and the
73
117
  * handler (S2, S6). `implies` takes a second option name, or a predicate over the values.
@@ -83,6 +127,22 @@ export type Relation = {
83
127
  } | {
84
128
  implies: readonly [string, string | ((values: Record<string, unknown>) => boolean)];
85
129
  };
130
+ /**
131
+ * The option-level `dependsOn` / `exclusive` of {@link OptionSpec}, as the `Relation` union
132
+ * the engine already enforces. Declaration order, options then their names, so two runs of
133
+ * the same manifest publish the same list.
134
+ */
135
+ export declare function optionRelations(options: Record<string, OptionSpec>): Relation[];
136
+ /**
137
+ * Every constraint on a command, from wherever it was declared: the command's own
138
+ * `relations` first, then the ones its options spell on themselves.
139
+ *
140
+ * One function because there must be one answer. `validate.ts` enforces this list and
141
+ * `schema.ts` publishes it, and a surface that computed its own would be the defect
142
+ * `relations-schema.test.ts` was written for — an agent learning a constraint by being
143
+ * refused, one round trip at a time.
144
+ */
145
+ export declare function relationsOf(node: Pick<CommandNode, 'options' | 'relations'>): Relation[];
86
146
  /** A positional, as help documents it (yargs #2012). */
87
147
  export interface ArgumentSpec {
88
148
  name: string;
@@ -122,6 +182,20 @@ export interface RunContext {
122
182
  agent?: string;
123
183
  /** Stop and tell the caller what to do instead of blocking on a prompt (N11). */
124
184
  actionRequired: (spec: ActionRequiredSpec) => never;
185
+ /**
186
+ * Cleanup that runs on **every** path out of the run (E5): a normal return, `ctx.exit`,
187
+ * Ctrl-C, SIGTERM, a terminal closing, an uncaught throw. Returns the function that
188
+ * unregisters it, for a command that cleaned up on its own.
189
+ *
190
+ * The handler runs after stdout has been drained (O5) and before the terminal is handed
191
+ * back, and it runs exactly once however many of those arrive together. `label` is what a
192
+ * breached shutdown deadline calls it; without one an arrow is reported as `(anonymous)`,
193
+ * and the anonymous arrow is the shape that hangs.
194
+ *
195
+ * Typed here rather than re-exported from `closeout`, so a command's signature does not
196
+ * change when that package's does.
197
+ */
198
+ onExit: (handler: () => void | Promise<void>, label?: string) => () => void;
125
199
  }
126
200
  export interface CommandNode {
127
201
  path: string[];
@@ -137,8 +211,14 @@ export interface CommandNode {
137
211
  epilogue?: string;
138
212
  hidden?: boolean;
139
213
  deprecated?: boolean | string;
140
- /** Required for a command to be served as an MCP tool (N2, N6). */
141
- effects?: Effects;
214
+ /**
215
+ * What running it does to the world, or `'withheld'` (N2, N6). Required on a node that
216
+ * runs — `checkCommand` refuses one that omits it — and optional on the type, because a
217
+ * group carries no `effects` and the host front-ends build nodes that never reach that
218
+ * door: commander and yargs have no notion of effects and their graded suites declare
219
+ * none, so a façade's command is withheld in fact and cannot be made to say so.
220
+ */
221
+ effects?: DeclaredEffects;
142
222
  run?: (ctx: RunContext) => unknown;
143
223
  /**
144
224
  * The handler's module, imported on dispatch only (M2): the manifest — help, schema,
@@ -162,17 +242,6 @@ export interface Hook {
162
242
  options: Record<string, unknown>;
163
243
  }) => void | Promise<void>;
164
244
  }
165
- export interface Plugin {
166
- name: string;
167
- commands?: CommandNode[];
168
- hooks?: {
169
- preRun?: Hook;
170
- postRun?: Hook;
171
- onError?: Hook;
172
- };
173
- enforce?: 'pre' | 'post';
174
- }
175
- export declare function definePlugin(plugin: Plugin): Plugin;
176
245
  /** Rolldown's lesson: evaluate the filter before crossing the boundary. */
177
246
  export declare function hookApplies(hook: Hook | undefined, command: string): hook is Hook;
178
247
  export declare class Manifest {
@@ -191,6 +260,13 @@ export declare class Manifest {
191
260
  /** Characters of `--schema` output above which it is summarised (N13). */
192
261
  schemaBudget?: number;
193
262
  add(node: CommandNode): void;
263
+ /**
264
+ * Register a plugin, after the plugin host has read it (`plugin.ts`).
265
+ *
266
+ * Nothing is pushed until everything has been checked, so a refused plugin contributes no
267
+ * command and leaves no half-registration behind: `use()` used to push first and read the
268
+ * object afterwards, which is how `use(undefined)` became a `TypeError` one line later.
269
+ */
194
270
  use(plugin: Plugin): void;
195
271
  /** `enforce: 'pre'` first, then unordered, then `'post'` — the Vite/Rolldown convention. */
196
272
  private ordered;
@@ -205,3 +281,9 @@ export declare class Manifest {
205
281
  rest: string[];
206
282
  };
207
283
  }
284
+ /**
285
+ * Re-exported so a façade importing `Plugin` keeps importing it from the module it registers
286
+ * against. The declaration itself lives in `./plugin.js`, which is the host file the family's
287
+ * locks read.
288
+ */
289
+ export type { Plugin };
package/dist/manifest.js CHANGED
@@ -1,3 +1,17 @@
1
+ import { validate } from './plugin.js';
2
+ export function optionRelations(options) {
3
+ const out = [];
4
+ for (const [key, spec] of Object.entries(options)) {
5
+ for (const other of spec.dependsOn ?? [])
6
+ out.push({ implies: [key, other] });
7
+ for (const other of spec.exclusive ?? [])
8
+ out.push({ conflicts: [key, other] });
9
+ }
10
+ return out;
11
+ }
12
+ export function relationsOf(node) {
13
+ return [...(node.relations ?? []), ...optionRelations(node.options)];
14
+ }
1
15
  export function lazyRun(load) {
2
16
  let loaded;
3
17
  return async (ctx) => {
@@ -9,9 +23,6 @@ export function lazyRun(load) {
9
23
  return await handler(ctx);
10
24
  };
11
25
  }
12
- export function definePlugin(plugin) {
13
- return plugin;
14
- }
15
26
  export function hookApplies(hook, command) {
16
27
  if (hook === undefined)
17
28
  return false;
@@ -30,10 +41,10 @@ export class Manifest {
30
41
  this.commands.push(node.load !== undefined && node.run === undefined ? { ...node, run: lazyRun(node.load) } : node);
31
42
  }
32
43
  use(plugin) {
44
+ validate(plugin, this.commands.map((c) => c.path.join(' ')));
33
45
  this.plugins.push(plugin);
34
- for (const command of plugin.commands ?? []) {
46
+ for (const command of plugin.commands ?? [])
35
47
  this.add({ ...command, plugin: plugin.name });
36
- }
37
48
  }
38
49
  ordered() {
39
50
  return [...this.plugins].sort((a, b) => (a.enforce === undefined ? 1 : ORDER[a.enforce]) - (b.enforce === undefined ? 1 : ORDER[b.enforce]));
package/dist/mcp.d.ts CHANGED
@@ -22,7 +22,17 @@ export type Invoke = (argv: string[]) => Promise<{
22
22
  export declare function annotationsOf(effects: Effects): ToolAnnotations;
23
23
  /** `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`. */
24
24
  export declare const toolName: (node: CommandNode, root: string[]) => string;
25
- /** The tool list: every runnable, visible command that declared its effects. */
25
+ /**
26
+ * The tool list: every runnable, visible command that declared what running it does.
27
+ *
28
+ * Two commands are absent and for different reasons. One declared `'withheld'` — an author
29
+ * who thought about it and said no, which is what that word is for. The other has no
30
+ * `effects` at all, which `checkCommand` now refuses at declaration, so on burgee's own API
31
+ * it cannot reach here; a command built through the commander or yargs façade still can,
32
+ * because neither incumbent has a notion of effects and neither can be made to acquire one
33
+ * without breaking the suites that grade the façades. The filter treats both as *not a
34
+ * tool*, which is the same conservative reading it always had.
35
+ */
26
36
  export declare function toolsOf(manifest: Manifest): Tool[];
27
37
  /** A tool call's arguments back into argv: the command, its options, `--json`, then positionals in declared order. */
28
38
  export declare function argvOf(node: CommandNode, root: string[], args: Record<string, unknown>): string[];
package/dist/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import { WITHHELD } from './definition.js';
2
3
  import { inputSchemaOf, runnable, typedName } from './schema.js';
3
4
  export const MCP_PROTOCOL_VERSION = '2025-06-18';
4
5
  const JSON_RPC_INVALID_REQUEST = -32600;
@@ -20,7 +21,7 @@ function describe(node) {
20
21
  }
21
22
  export function toolsOf(manifest) {
22
23
  return runnable(manifest)
23
- .filter((c) => c.effects !== undefined)
24
+ .filter((c) => c.effects !== undefined && c.effects !== WITHHELD)
24
25
  .map((c) => ({ name: toolName(c, manifest.rootPath), description: describe(c), inputSchema: inputSchemaOf(c), annotations: annotationsOf(c.effects) }));
25
26
  }
26
27
  function optionArgs(node, args) {
package/dist/names.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  /** One canonical camelCase key per option; kebab-case on the command line (S5, yargs #1679). */
2
2
  /** `dryRun` → `dry-run`; a kebab key stays as it is. */
3
3
  export declare function kebab(name: string): string;
4
+ /**
5
+ * Option names as the caller types them. Every surface that publishes a list of options —
6
+ * `--schema`, help, the Fig spec — renders the flags, never the canonical keys, and one
7
+ * helper here is the difference between that being true and being true three times.
8
+ */
9
+ export declare function flagsOf(names: readonly string[]): string[];
4
10
  /** `--dry-run` on the command line reaches the handler as `dryRun`. */
5
11
  export declare function camel(flag: string): string;
package/dist/names.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export function kebab(name) {
2
2
  return name.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
3
3
  }
4
+ export function flagsOf(names) {
5
+ return names.map((n) => `--${kebab(n)}`);
6
+ }
4
7
  export function camel(flag) {
5
8
  return flag.replaceAll(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
6
9
  }