burgee 0.2.0 → 0.4.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -9
  3. package/dist/brand.d.ts +62 -1
  4. package/dist/brand.js +73 -7
  5. package/dist/cli.d.ts +11 -0
  6. package/dist/cli.js +17 -1
  7. package/dist/{commander-argument.js → commander/argument.js} +4 -1
  8. package/dist/{commander-command.d.ts → commander/command.d.ts} +15 -5
  9. package/dist/{commander-command.js → commander/command.js} +175 -22
  10. package/dist/{commander-error.js → commander/error.js} +2 -0
  11. package/dist/{commander-help.d.ts → commander/help.d.ts} +3 -3
  12. package/dist/{commander-help.js → commander/help.js} +18 -1
  13. package/dist/{commander-option.d.ts → commander/option.d.ts} +1 -1
  14. package/dist/{commander-option.js → commander/option.js} +15 -1
  15. package/dist/commander.d.ts +8 -8
  16. package/dist/commander.js +8 -8
  17. package/dist/completions.js +15 -4
  18. package/dist/dev.d.ts +47 -0
  19. package/dist/dev.js +132 -0
  20. package/dist/execute.d.ts +22 -1
  21. package/dist/execute.js +75 -24
  22. package/dist/help.d.ts +19 -7
  23. package/dist/help.js +50 -21
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +1 -1
  26. package/dist/manifest.d.ts +15 -0
  27. package/dist/manifest.js +12 -1
  28. package/dist/mcp.d.ts +14 -2
  29. package/dist/mcp.js +15 -2
  30. package/dist/runtime.d.ts +12 -0
  31. package/dist/runtime.js +7 -0
  32. package/dist/schema.d.ts +10 -0
  33. package/dist/schema.js +11 -0
  34. package/dist/testing-helpers.d.ts +18 -1
  35. package/dist/testing-helpers.js +35 -0
  36. package/dist/testing.d.ts +2 -2
  37. package/dist/testing.js +1 -1
  38. package/dist/unknown-option.d.ts +9 -0
  39. package/dist/unknown-option.js +27 -0
  40. package/dist/{yargs-burgee.d.ts → yargs/burgee.d.ts} +2 -2
  41. package/dist/{yargs-burgee.js → yargs/burgee.js} +15 -1
  42. package/dist/{yargs-cliui.js → yargs/cliui.js} +8 -0
  43. package/dist/{yargs-command.d.ts → yargs/command.d.ts} +5 -5
  44. package/dist/{yargs-command.js → yargs/command.js} +9 -2
  45. package/dist/{yargs-completion.d.ts → yargs/completion.d.ts} +3 -3
  46. package/dist/{yargs-completion.js → yargs/completion.js} +7 -2
  47. package/dist/{yargs-factory.d.ts → yargs/factory.d.ts} +8 -8
  48. package/dist/{yargs-factory.js → yargs/factory.js} +63 -13
  49. package/dist/{yargs-middleware.js → yargs/middleware.js} +6 -1
  50. package/dist/{yargs-shim.d.ts → yargs/shim.d.ts} +3 -3
  51. package/dist/{yargs-shim.js → yargs/shim.js} +24 -5
  52. package/dist/{yargs-usage.d.ts → yargs/usage.d.ts} +1 -1
  53. package/dist/{yargs-usage.js → yargs/usage.js} +9 -1
  54. package/dist/{yargs-utils.js → yargs/utils.js} +11 -0
  55. package/dist/{yargs-validation.d.ts → yargs/validation.d.ts} +1 -1
  56. package/dist/{yargs-validation.js → yargs/validation.js} +7 -1
  57. package/dist/{yargs-y18n.js → yargs/y18n.js} +6 -0
  58. package/dist/yargs-helpers.d.ts +1 -1
  59. package/dist/yargs-helpers.js +1 -1
  60. package/dist/yargs.d.ts +4 -4
  61. package/dist/yargs.js +5 -5
  62. package/package.json +3 -2
  63. /package/dist/{commander-argument.d.ts → commander/argument.d.ts} +0 -0
  64. /package/dist/{commander-error.d.ts → commander/error.d.ts} +0 -0
  65. /package/dist/{commander-suggest.d.ts → suggest.d.ts} +0 -0
  66. /package/dist/{commander-suggest.js → suggest.js} +0 -0
  67. /package/dist/{yargs-cliui.d.ts → yargs/cliui.d.ts} +0 -0
  68. /package/dist/{yargs-middleware.d.ts → yargs/middleware.d.ts} +0 -0
  69. /package/dist/{yargs-utils.d.ts → yargs/utils.d.ts} +0 -0
  70. /package/dist/{yargs-y18n.d.ts → yargs/y18n.d.ts} +0 -0
package/dist/execute.js CHANGED
@@ -8,7 +8,7 @@ import { serveMcp } from './mcp.js';
8
8
  import { camel, kebab } from './names.js';
9
9
  import { nearestPackage } from './pkg.js';
10
10
  import { ConfigError, explain, resolve as resolveLayers } from './precedence.js';
11
- import { commandSchemaOf, schemaOf, summaryOf } from './schema.js';
11
+ import { commandSchemaOf, machineJson, schemaOf, summaryOf } from './schema.js';
12
12
  import { checkDefinition, checkRelations, coerce, UsageError } from './validate.js';
13
13
  function helpFields(c) {
14
14
  const node = {};
@@ -51,11 +51,15 @@ function addTree(manifest, parent, commands) {
51
51
  ...helpFields(c),
52
52
  options: c.options ?? {},
53
53
  ...(c.run === undefined ? {} : { run: c.run }),
54
+ ...(c.load === undefined ? {} : { load: c.load }),
54
55
  });
55
56
  if (c.commands !== undefined)
56
57
  addTree(manifest, path, c.commands);
57
58
  }
58
59
  }
60
+ export function sharedOptions(name, specs) {
61
+ return Object.fromEntries(Object.entries(specs).map(([key, spec]) => [key, { ...spec, sharedFrom: name }]));
62
+ }
59
63
  export function defineProgram(program) {
60
64
  const manifest = new Manifest();
61
65
  manifest.rootPath = [program.name];
@@ -112,6 +116,7 @@ function render(value) {
112
116
  }
113
117
  return String(value);
114
118
  }
119
+ const NO = 'no-';
115
120
  function toParseConfig(specs, withConfig) {
116
121
  const config = { json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' }, explain: { type: 'string' } };
117
122
  if (withConfig) {
@@ -124,11 +129,28 @@ function toParseConfig(specs, withConfig) {
124
129
  ...(spec.short === undefined ? {} : { short: spec.short }),
125
130
  ...(spec.multiple === true ? { multiple: true } : {}),
126
131
  };
132
+ if (spec.type === 'boolean')
133
+ config[`${NO}${kebab(name)}`] = { type: 'boolean' };
127
134
  }
128
135
  return config;
129
136
  }
130
- function canonical(values) {
131
- return Object.fromEntries(Object.entries(values).map(([k, v]) => [camel(k), v]));
137
+ function canonical(values, specs, tokens) {
138
+ const out = {};
139
+ const negatable = (key) => {
140
+ const bare = camel(key.startsWith(NO) ? key.slice(NO.length) : key);
141
+ return specs[bare]?.type === 'boolean' ? bare : '';
142
+ };
143
+ for (const [k, v] of Object.entries(values))
144
+ if (!k.startsWith(NO) || negatable(k) === '')
145
+ out[camel(k)] = v;
146
+ for (const token of tokens) {
147
+ if (token.kind !== 'option')
148
+ continue;
149
+ const bare = negatable(token.name);
150
+ if (bare !== '')
151
+ out[bare] = !token.name.startsWith(NO);
152
+ }
153
+ return out;
132
154
  }
133
155
  function packageLayer(pkg, name) {
134
156
  if (pkg === undefined || name === undefined)
@@ -191,18 +213,7 @@ function exitSignal(cause) {
191
213
  const code = cause?.code;
192
214
  return typeof code === 'number' && isExitCode(code) ? code : undefined;
193
215
  }
194
- const SINGLE_DASH_WORD = /^-([a-zA-Z][\w-]+)(?:=.*)?$/;
195
- function singleDashHint(argv) {
196
- for (const token of argv) {
197
- if (token === '--')
198
- return undefined;
199
- const found = SINGLE_DASH_WORD.exec(token);
200
- if (found?.[1] !== undefined)
201
- return `did you mean --${found[1]}? a single dash introduces one-letter options`;
202
- }
203
- return undefined;
204
- }
205
- function describeFailure(cause, argv) {
216
+ async function describeFailure(cause, argv, node) {
206
217
  const signal = exitSignal(cause);
207
218
  if (signal !== undefined)
208
219
  return { code: signal, message: '', silent: true };
@@ -216,7 +227,12 @@ function describeFailure(cause, argv) {
216
227
  return { code: ExitCode.CONFIG, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
217
228
  }
218
229
  if (isParseArgsFailure(cause)) {
219
- return { code: ExitCode.USAGE, message, hint: singleDashHint(argv) ?? 'run --help to see the available options' };
230
+ const explain = await import('./unknown-option.js');
231
+ const dash = explain.singleDashHint(argv);
232
+ if (dash !== undefined)
233
+ return { code: ExitCode.USAGE, message, hint: dash };
234
+ const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}));
235
+ return { code: ExitCode.USAGE, message, hint: 'run --help to see the available options', ...better };
220
236
  }
221
237
  return { code: ExitCode.RUNTIME, message };
222
238
  }
@@ -242,13 +258,16 @@ export function beforeTerminator(argv) {
242
258
  function rootNode(manifest, root) {
243
259
  return manifest.find(root) ?? { path: root, options: {} };
244
260
  }
245
- function unresolved({ manifest, root, io: { width } }, argv, at) {
261
+ function unresolved({ manifest, root, io }, argv, at) {
246
262
  const node = at ?? rootNode(manifest, root);
247
263
  const typed = argv.slice(node.path.length - root.length);
248
- if (typed.length > 0 && HELP_FLAGS.has(typed[0] ?? ''))
249
- return { text: renderHelp(manifest, node, { width }), code: ExitCode.OK };
264
+ const first = typed[0] ?? '';
265
+ if (typed.length > 0 && HELP_FLAGS.has(first))
266
+ return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.OK };
267
+ if (first === '--version' || first === '-V')
268
+ return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
250
269
  if (typed.length === 0)
251
- return { text: renderHelp(manifest, node, { width }), code: ExitCode.USAGE };
270
+ return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.USAGE };
252
271
  throw new UsageError(`unknown command "${typed[0] ?? ''}"`, 'run --help to see the available commands');
253
272
  }
254
273
  async function completion(manifest, argv, io) {
@@ -275,7 +294,7 @@ async function surface(manifest, argv, io) {
275
294
  return true;
276
295
  }
277
296
  if (head.includes('--schema')) {
278
- io.out.write(`${JSON.stringify(schemaSurface(manifest, argv), null, 2)}\n`);
297
+ io.out.write(`${machineJson(schemaSurface(manifest, argv), head)}\n`);
279
298
  return true;
280
299
  }
281
300
  if (head[0] === '--mcp') {
@@ -301,7 +320,7 @@ async function surface(manifest, argv, io) {
301
320
  }
302
321
  const SCHEMA_BUDGET = 48_000;
303
322
  function schemaSurface(manifest, argv) {
304
- const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema'), manifest.rootPath);
323
+ const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema' && !a.startsWith('--format=')), manifest.rootPath);
305
324
  if (node?.run !== undefined)
306
325
  return commandSchemaOf(node, manifest.rootPath);
307
326
  const full = schemaOf(manifest);
@@ -329,7 +348,7 @@ function versionOf(manifest, io) {
329
348
  }
330
349
  async function dispatch(manifest, { node, rest, name }, io) {
331
350
  const parsed = parseArgs({ args: rest, options: toParseConfig(node.options, manifest.config !== undefined), allowPositionals: true, strict: true, tokens: true });
332
- const flags = canonical(parsed.values);
351
+ const flags = canonical(parsed.values, node.options, parsed.tokens);
333
352
  const json = flags.json === true;
334
353
  if (flags.help === true)
335
354
  return { json, text: renderHelp(manifest, node, { width: io.width }) };
@@ -342,6 +361,8 @@ async function dispatch(manifest, { node, rest, name }, io) {
342
361
  checkRelations(node.relations, resolved.values, provenance);
343
362
  const values = await coerce(node.options, resolved.values);
344
363
  const { positionals, passthrough } = splitPositionals(parsed.tokens);
364
+ requirePositionals(node, positionals);
365
+ warnDeprecated(node, io);
345
366
  await manifest.fire('preRun', name, values);
346
367
  const exit = (code) => {
347
368
  io.exit(code);
@@ -353,6 +374,25 @@ async function dispatch(manifest, { node, rest, name }, io) {
353
374
  const changed = changedOf(node, data);
354
375
  return { json, data, provenance, ...(changed === undefined ? {} : { changed }) };
355
376
  }
377
+ function requirePositionals(node, positionals) {
378
+ const required = (node.arguments ?? []).filter((a) => a.required !== false && a.variadic !== true);
379
+ const missing = required[positionals.length];
380
+ if (positionals.length < required.length && missing !== undefined) {
381
+ throw new UsageError(`missing required argument "${missing.name}"`, `run --help to see what "${node.path.slice(1).join(' ')}" takes`);
382
+ }
383
+ }
384
+ const warned = new Set();
385
+ function warnDeprecated(node, io) {
386
+ if (node.deprecated === undefined || node.deprecated === false)
387
+ return;
388
+ const key = node.path.join(' ');
389
+ if (warned.has(key))
390
+ return;
391
+ warned.add(key);
392
+ const typed = node.path.slice(1).join(' ') || key;
393
+ const use = typeof node.deprecated === 'string' ? `, use '${node.deprecated}'` : '';
394
+ io.err.write(`warning: '${typed}' is deprecated${use}\n`);
395
+ }
356
396
  function emit(io, outcome) {
357
397
  if (outcome.text !== undefined) {
358
398
  io.out.write(outcome.text);
@@ -364,7 +404,7 @@ function emit(io, outcome) {
364
404
  return io.exit(ExitCode.OK);
365
405
  }
366
406
  async function report(cause, { manifest, io, argv, json, name }) {
367
- const failure = describeFailure(cause, argv);
407
+ const failure = await describeFailure(cause, argv, resolveCommand(manifest, argv) ?? undefined);
368
408
  if (failure.silent === true)
369
409
  return io.exit(failure.code);
370
410
  await manifest.fire('onError', name, {});
@@ -418,6 +458,17 @@ export async function execute(manifest, opts = {}) {
418
458
  return await report(cause, { manifest, io, argv, json, name });
419
459
  }
420
460
  }
461
+ export function resolveCommand(manifest, argv) {
462
+ const { node } = manifest.resolve(beforeTerminator(argv), manifest.rootPath);
463
+ return node ?? null;
464
+ }
465
+ export async function runCommand(manifest, argv, opts = {}) {
466
+ const out = [];
467
+ const err = [];
468
+ let code = ExitCode.OK;
469
+ await execute(manifest, { ...opts, argv: [...argv], from: 'user', stdout: { write: (s) => out.push(s) }, stderr: { write: (s) => err.push(s) }, exit: (c) => void (code = c) });
470
+ return { code, stdout: out.join(''), stderr: err.join('') };
471
+ }
421
472
  export async function run(target, opts = {}) {
422
473
  if (target instanceof Manifest)
423
474
  return await execute(target, opts);
package/dist/help.d.ts CHANGED
@@ -1,22 +1,34 @@
1
+ import type { CommandNode, Manifest } from './manifest.js';
2
+ /** The token names of `roundel`'s R3, typed structurally: burgee never imports them (U13). */
3
+ export type HelpToken = 'error' | 'warn' | 'ok' | 'hint' | 'muted' | 'command' | 'flag' | 'value' | 'heading';
1
4
  /**
2
- * Help, rendered from the manifest and nothing else (H1). One layout for every node,
3
- * so a subcommand's help has everything the root's has (yargs #1500, #1331, #1025).
5
+ * Per-token styling for help (R7). A user who has `roundel` passes its tokens; a user
6
+ * who does not gets the defaults. Help reads `heading`, `command`, `flag` and `value`;
7
+ * the others are accepted so one theme object serves the whole output stack.
4
8
  *
5
- * Section order is fixed (R2): usage, description, arguments, options, global options,
6
- * commands (grouped, yargs #684), examples, environment, epilogue. Empty sections are
7
- * omitted. Width comes from the caller — the runtime, in practice (H3) — default 100.
9
+ * What the theme does not touch: the command name after `Usage:` and every `$ example`
10
+ * line are rendered plain, whatever the theme says. Styling wraps a finished cell, so a
11
+ * name is measured and padded plain and never coloured in the manifest (yargs #1699).
8
12
  */
9
- import type { CommandNode, Manifest } from './manifest.js';
13
+ export type HelpTheme = Partial<Record<HelpToken, (s: string) => string>>;
10
14
  export interface HelpOptions {
11
15
  /** Columns available; 100 when unknown, never `process.stdout` directly (H3). */
12
16
  width?: number;
13
17
  /** Show type hints such as `[string]`; off by default (H6). */
14
18
  verbose?: boolean;
19
+ /** Apply colour (R7). Off by default: the renderer is pure, so the TTY and NO_COLOR decision stays with the caller. */
20
+ color?: boolean;
21
+ /**
22
+ * Replaces the default styling token by token; read only when `color` is on. See
23
+ * `HelpTheme` for the lines it leaves plain: the `Usage:` command name and `$ example`.
24
+ */
25
+ theme?: HelpTheme;
15
26
  }
16
27
  /** Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120). */
17
28
  export declare function wrap(text: string, width: number): string[];
18
29
  /**
19
30
  * Render help for one node — a runnable command, a group, or both — as text.
20
- * Deterministic for a given node and width; a snapshot suite pins it.
31
+ * Deterministic for a given node and width; a snapshot suite pins it. With `color`
32
+ * off — the default — a theme changes nothing; with it on, only ANSI is added.
21
33
  */
22
34
  export declare function renderHelp(manifest: Manifest, node: CommandNode, opts?: HelpOptions): string;
package/dist/help.js CHANGED
@@ -1,11 +1,36 @@
1
+ import { styleText } from 'node:util';
1
2
  import { kebab } from './names.js';
3
+ const identity = (s) => s;
4
+ const PLAIN = { heading: identity, command: identity, flag: identity, value: identity };
5
+ const DEFAULTS = {
6
+ heading: (s) => styleText('bold', s, { validateStream: false }),
7
+ command: (s) => styleText('bold', s, { validateStream: false }),
8
+ flag: (s) => styleText('cyan', s, { validateStream: false }),
9
+ value: (s) => styleText('dim', s, { validateStream: false }),
10
+ };
11
+ function painterFor(paint, kind) {
12
+ if (kind === 'command')
13
+ return paint.command;
14
+ return kind === 'flag' ? paint.flag : paint.value;
15
+ }
16
+ function paintOf(opts) {
17
+ if (opts.color !== true)
18
+ return PLAIN;
19
+ const theme = opts.theme ?? {};
20
+ return {
21
+ heading: theme.heading ?? DEFAULTS.heading,
22
+ command: theme.command ?? DEFAULTS.command,
23
+ flag: theme.flag ?? DEFAULTS.flag,
24
+ value: theme.value ?? DEFAULTS.value,
25
+ };
26
+ }
2
27
  const DEFAULT_WIDTH = 100;
3
28
  const INDENT = ' ';
4
29
  const GUTTER = 2;
5
30
  const TERM_SHARE = 0.4;
6
31
  const GLOBAL = [
7
- { term: '--json', text: 'machine-readable output' },
8
- { term: '--help', text: 'show this help' },
32
+ { term: '--json', text: 'machine-readable output', kind: 'flag' },
33
+ { term: '--help', text: 'show this help', kind: 'flag' },
9
34
  ];
10
35
  function deprecation(d) {
11
36
  if (d === undefined || d === false)
@@ -41,7 +66,7 @@ function optionTerm(name, spec) {
41
66
  function optionRows(options, verbose) {
42
67
  return Object.entries(options)
43
68
  .filter(([, spec]) => spec.hidden !== true)
44
- .map(([name, spec]) => ({ term: optionTerm(name, spec), text: annotate(spec.description ?? '', spec, verbose) }));
69
+ .map(([name, spec]) => ({ term: optionTerm(name, spec), text: annotate(spec.description ?? '', spec, verbose), kind: 'flag' }));
45
70
  }
46
71
  const argumentTerm = (a) => {
47
72
  const name = a.variadic === true ? `${a.name}...` : a.name;
@@ -51,6 +76,7 @@ function argumentRows(args) {
51
76
  return args.map((a) => ({
52
77
  term: argumentTerm(a),
53
78
  text: [a.description ?? '', a.default === undefined ? '' : `(default: ${a.default})`].filter((p) => p !== '').join(' '),
79
+ kind: 'value',
54
80
  }));
55
81
  }
56
82
  function commandSections(manifest, node) {
@@ -59,7 +85,7 @@ function commandSections(manifest, node) {
59
85
  for (const c of children) {
60
86
  const heading = c.group ?? 'Commands:';
61
87
  const rows = groups.get(heading) ?? [];
62
- rows.push({ term: c.path[c.path.length - 1] ?? '', text: `${c.summary ?? c.description ?? ''}${deprecation(c.deprecated)}`.trim() });
88
+ rows.push({ term: c.path[c.path.length - 1] ?? '', text: `${c.summary ?? c.description ?? ''}${deprecation(c.deprecated)}`.trim(), kind: 'command' });
63
89
  groups.set(heading, rows);
64
90
  }
65
91
  return [...groups].map(([title, rows]) => ({ title, rows }));
@@ -67,9 +93,9 @@ function commandSections(manifest, node) {
67
93
  function environmentRows(options) {
68
94
  return Object.entries(options)
69
95
  .filter(([, spec]) => spec.env !== undefined && spec.hidden !== true)
70
- .map(([name, spec]) => ({ term: spec.env ?? '', text: `--${kebab(name)}` }));
96
+ .map(([name, spec]) => ({ term: spec.env ?? '', text: `--${kebab(name)}`, kind: 'value' }));
71
97
  }
72
- function usageLine(node, root, hasChildren) {
98
+ function usageLine(node, root, hasChildren, paint) {
73
99
  const shown = node.path.slice(root.length).join(' ') || node.path.join(' ');
74
100
  const parts = [shown];
75
101
  if (hasChildren)
@@ -77,7 +103,7 @@ function usageLine(node, root, hasChildren) {
77
103
  parts.push('[options]');
78
104
  for (const a of node.arguments ?? [])
79
105
  parts.push(argumentTerm(a));
80
- return `Usage: ${parts.join(' ')}`;
106
+ return `${paint.heading('Usage:')} ${parts.join(' ')}`;
81
107
  }
82
108
  export function wrap(text, width) {
83
109
  const out = [];
@@ -103,21 +129,23 @@ function termColumn(rows, width) {
103
129
  const longest = Math.max(0, ...rows.map((r) => r.term.length));
104
130
  return Math.min(longest, Math.floor(width * TERM_SHARE));
105
131
  }
106
- function layout(rows, width, column) {
132
+ function layout(rows, width, column, paint) {
107
133
  const textWidth = Math.max(1, width - INDENT.length - column - GUTTER);
108
134
  const lines = [];
109
- for (const { term, text } of rows) {
135
+ for (const { term, text, kind } of rows) {
136
+ const cell = painterFor(paint, kind)(term);
110
137
  if (text === '') {
111
- lines.push(`${INDENT}${term}`);
138
+ lines.push(`${INDENT}${cell}`);
112
139
  continue;
113
140
  }
114
141
  const wrapped = wrap(text, textWidth);
115
142
  const continuation = INDENT + ' '.repeat(column + GUTTER);
116
143
  if (term.length > column) {
117
- lines.push(`${INDENT}${term}`, ...wrapped.map((l) => `${continuation}${l}`));
144
+ lines.push(`${INDENT}${cell}`, ...wrapped.map((l) => `${continuation}${l}`));
118
145
  continue;
119
146
  }
120
- lines.push(`${INDENT}${term.padEnd(column + GUTTER)}${wrapped[0] ?? ''}`, ...wrapped.slice(1).map((l) => `${continuation}${l}`));
147
+ const pad = ' '.repeat(column + GUTTER - term.length);
148
+ lines.push(`${INDENT}${cell}${pad}${wrapped[0] ?? ''}`, ...wrapped.slice(1).map((l) => `${continuation}${l}`));
121
149
  }
122
150
  return lines;
123
151
  }
@@ -130,28 +158,29 @@ function exampleLines(examples, width) {
130
158
  }
131
159
  return lines;
132
160
  }
133
- function section(title, body) {
134
- return body.length === 0 ? [] : [title, ...body, ''];
161
+ function section(title, body, paint) {
162
+ return body.length === 0 ? [] : [paint.heading(title), ...body, ''];
135
163
  }
136
164
  export function renderHelp(manifest, node, opts = {}) {
137
165
  const width = opts.width ?? DEFAULT_WIDTH;
138
166
  const verbose = opts.verbose === true;
167
+ const paint = paintOf(opts);
139
168
  const root = manifest.rootPath;
140
169
  const commands = commandSections(manifest, node);
141
170
  const args = argumentRows(node.arguments ?? []);
142
171
  const options = optionRows(node.options, verbose);
143
172
  const env = environmentRows(node.options);
144
173
  const column = termColumn([...args, ...options, ...GLOBAL, ...commands.flatMap((s) => s.rows), ...env], width);
145
- const lines = [usageLine(node, root, commands.length > 0), ''];
174
+ const lines = [usageLine(node, root, commands.length > 0, paint), ''];
146
175
  if (node.description !== undefined)
147
176
  lines.push(...wrap(`${node.description}${deprecation(node.deprecated)}`, width), '');
148
- lines.push(...section('Arguments:', layout(args, width, column)));
149
- lines.push(...section('Options:', layout(options, width, column)));
150
- lines.push(...section('Global options:', layout(GLOBAL, width, column)));
177
+ lines.push(...section('Arguments:', layout(args, width, column, paint), paint));
178
+ lines.push(...section('Options:', layout(options, width, column, paint), paint));
179
+ lines.push(...section('Global options:', layout(GLOBAL, width, column, paint), paint));
151
180
  for (const s of commands)
152
- lines.push(...section(s.title, layout(s.rows, width, column)));
153
- lines.push(...section('Examples:', exampleLines(node.examples ?? [], width)));
154
- lines.push(...section('Environment:', layout(env, width, column)));
181
+ lines.push(...section(s.title, layout(s.rows, width, column, paint), paint));
182
+ lines.push(...section('Examples:', exampleLines(node.examples ?? [], width), paint));
183
+ lines.push(...section('Environment:', layout(env, width, column, paint), paint));
155
184
  if (node.epilogue !== undefined)
156
185
  lines.push(...wrap(node.epilogue, width), '');
157
186
  return `${lines.join('\n').trimEnd()}\n`;
package/dist/index.d.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * façade can import it without pulling this barrel.
7
7
  */
8
8
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
9
- export { defineCommand, defineProgram, execute, run, type AnyCommand, type Command, type CommandContext, type InferOptions, type OptionSpecs, type Program, type RunOptions, } from './execute.js';
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
10
  export { camel, checkDefinition, kebab, UsageError } from './validate.js';
11
11
  export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
12
- export { renderHelp, type HelpOptions } from './help.js';
12
+ export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
13
13
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
14
14
  export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from './precedence.js';
15
15
  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 OptionSpec, type ActionRequiredSpec, type Plugin, type Relation, type RunContext, type StandardResult, type StandardSchemaV1, } from './manifest.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';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ExitCode, isExitCode } from './exit-code.js';
2
- export { defineCommand, defineProgram, execute, run, } from './execute.js';
2
+ export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, } from './execute.js';
3
3
  export { camel, checkDefinition, kebab, UsageError } from './validate.js';
4
4
  export { AGENT_PROBES, detectAgent } from './agent.js';
5
5
  export { renderHelp } from './help.js';
@@ -53,6 +53,13 @@ export interface OptionSpec {
53
53
  /** `true` renders `(deprecated)`; a string names the replacement: `(deprecated: use --force)` (yargs #2248). */
54
54
  deprecated?: boolean | string;
55
55
  hidden?: boolean;
56
+ /** The shared set this option was copied from (M4); `--schema` carries it, help lists the option like any other. */
57
+ sharedFrom?: string;
58
+ }
59
+ /** What a lazily loaded command module exports: the handler as `run` or as the default export (M2). */
60
+ export interface LazyModule {
61
+ default?: (ctx: RunContext) => unknown;
62
+ run?: (ctx: RunContext) => unknown;
56
63
  }
57
64
  /**
58
65
  * What running a command does to the world (N6). Declared, never inferred: it decides
@@ -133,9 +140,17 @@ export interface CommandNode {
133
140
  /** Required for a command to be served as an MCP tool (N2, N6). */
134
141
  effects?: Effects;
135
142
  run?: (ctx: RunContext) => unknown;
143
+ /**
144
+ * The handler's module, imported on dispatch only (M2): the manifest — help, schema,
145
+ * completions, MCP tool list — is complete from this node without loading it. A node
146
+ * with `load` and no `run` gets a `run` that imports on first call.
147
+ */
148
+ load?: () => Promise<LazyModule>;
136
149
  /** Which plugin contributed this, if any. Declared, never diffed (M3). */
137
150
  plugin?: string;
138
151
  }
152
+ /** `run` for a lazy node: the module is imported on the first call and never before (M2). */
153
+ export declare function lazyRun(load: () => Promise<LazyModule>): (ctx: RunContext) => unknown;
139
154
  /** A hook may declare which commands it applies to, as data. */
140
155
  export interface HookFilter {
141
156
  command?: RegExp;
package/dist/manifest.js CHANGED
@@ -1,3 +1,14 @@
1
+ export function lazyRun(load) {
2
+ let loaded;
3
+ return async (ctx) => {
4
+ loaded ??= load();
5
+ const mod = await loaded;
6
+ const handler = mod.run ?? mod.default;
7
+ if (handler === undefined)
8
+ throw new Error('burgee: a lazy command module must export its handler as run or as the default export');
9
+ return await handler(ctx);
10
+ };
11
+ }
1
12
  export function definePlugin(plugin) {
2
13
  return plugin;
3
14
  }
@@ -16,7 +27,7 @@ export class Manifest {
16
27
  config;
17
28
  schemaBudget;
18
29
  add(node) {
19
- this.commands.push(node);
30
+ this.commands.push(node.load !== undefined && node.run === undefined ? { ...node, run: lazyRun(node.load) } : node);
20
31
  }
21
32
  use(plugin) {
22
33
  this.plugins.push(plugin);
package/dist/mcp.d.ts CHANGED
@@ -33,8 +33,20 @@ export interface ServeOptions {
33
33
  };
34
34
  invoke: Invoke;
35
35
  }
36
+ /** A running server: `done` settles when the input closes; `swap` serves a new manifest and says so (W2). */
37
+ export interface McpServer {
38
+ done: Promise<void>;
39
+ /**
40
+ * Serve this manifest (and its invoke) from the next request on, and emit
41
+ * `notifications/tools/list_changed` so a connected client re-lists. A call already in
42
+ * flight finishes against the manifest it started on.
43
+ */
44
+ swap: (manifest: Manifest, invoke?: Invoke) => void;
45
+ }
36
46
  /**
37
- * Serve until the input closes. Notifications (no `id`) get no reply; a malformed line gets
38
- * a JSON-RPC error with a null id, as the spec asks.
47
+ * Start serving; `done` settles when the input closes. Notifications (no `id`) get no
48
+ * reply; a malformed line gets a JSON-RPC error with a null id, as the spec asks.
39
49
  */
50
+ export declare function startMcp(manifest: Manifest, opts: ServeOptions): McpServer;
51
+ /** Serve until the input closes. */
40
52
  export declare function serveMcp(manifest: Manifest, opts: ServeOptions): Promise<void>;
package/dist/mcp.js CHANGED
@@ -81,14 +81,27 @@ async function handle(session, request) {
81
81
  return { error: { code: JSON_RPC_METHOD_NOT_FOUND, message: `method not found: ${request.method}` } };
82
82
  }
83
83
  }
84
- export async function serveMcp(manifest, opts) {
84
+ export function startMcp(manifest, opts) {
85
85
  const session = {
86
86
  manifest,
87
87
  invoke: opts.invoke,
88
88
  serverInfo: { name: manifest.rootPath.join(' ') || 'burgee', version: manifest.version ?? '0.0.0' },
89
89
  };
90
90
  const reply = (body) => void opts.output.write(`${JSON.stringify({ jsonrpc: '2.0', ...body })}\n`);
91
- for await (const line of createInterface({ input: opts.input, crlfDelay: Infinity })) {
91
+ const swap = (next, invoke) => {
92
+ session.manifest = next;
93
+ if (invoke !== undefined)
94
+ session.invoke = invoke;
95
+ reply({ method: 'notifications/tools/list_changed' });
96
+ };
97
+ const done = serve(session, opts.input, reply);
98
+ return { done, swap };
99
+ }
100
+ export async function serveMcp(manifest, opts) {
101
+ return await startMcp(manifest, opts).done;
102
+ }
103
+ async function serve(session, input, reply) {
104
+ for await (const line of createInterface({ input, crlfDelay: Infinity })) {
92
105
  if (line.trim() === '')
93
106
  continue;
94
107
  let request;
package/dist/runtime.d.ts CHANGED
@@ -3,6 +3,16 @@ import { type ExitCode } from './exit-code.js';
3
3
  export interface Writer {
4
4
  write(chunk: string): unknown;
5
5
  }
6
+ /**
7
+ * Time, as the layers above the parser see it (R14 of `cli-output-stack`). `now` is
8
+ * monotonic milliseconds from an arbitrary origin; `schedule` runs `fn` after `ms` and
9
+ * hands back the cancel. Typed structurally so the output stack can accept a `Runtime`
10
+ * without importing one.
11
+ */
12
+ export interface Clock {
13
+ now(): number;
14
+ schedule(fn: () => void, ms: number): () => void;
15
+ }
6
16
  /**
7
17
  * The world, as the layers above the parser see it (design R1 of
8
18
  * `cli-testing-harness`). Nothing above the parser reads `process.*` directly; it
@@ -22,6 +32,8 @@ export interface Runtime {
22
32
  };
23
33
  /** Ends the run with an E1 code. In the real runtime this never returns. */
24
34
  exit(code: ExitCode): never;
35
+ /** `performance.now` and `setTimeout` in the real runtime; a manual tick in the harness. */
36
+ clock: Clock;
25
37
  }
26
38
  /** The one place in the layer that touches `process`. Locked by `process-reference.lock.test.ts`. */
27
39
  export declare const processRuntime: Runtime;
package/dist/runtime.js CHANGED
@@ -14,4 +14,11 @@ export const processRuntime = {
14
14
  exit(code) {
15
15
  process.exit(code);
16
16
  },
17
+ clock: {
18
+ now: () => performance.now(),
19
+ schedule(fn, ms) {
20
+ const handle = setTimeout(fn, ms);
21
+ return () => clearTimeout(handle);
22
+ },
23
+ },
17
24
  };
package/dist/schema.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface JsonSchemaProperty {
24
24
  maximum?: number;
25
25
  /** The command-line spelling of the option (S5). */
26
26
  flag?: string;
27
+ /** The shared set this option was copied from (M4). */
28
+ sharedFrom?: string;
27
29
  }
28
30
  export interface CommandSchema {
29
31
  /** The command as typed, without the program name: `config get`. */
@@ -32,6 +34,12 @@ export interface CommandSchema {
32
34
  summary?: string;
33
35
  effects?: Effects;
34
36
  deprecated?: boolean | string;
37
+ /** The heading it is listed under (M1). */
38
+ group?: string;
39
+ /** Its handler loads on dispatch (M2): this schema was complete without it. */
40
+ lazy?: true;
41
+ /** Which plugin contributed it (M3). */
42
+ plugin?: string;
35
43
  arguments: ArgumentSpec[];
36
44
  options: Record<string, OptionSpec>;
37
45
  examples: Example[];
@@ -69,3 +77,5 @@ export interface SchemaSummary {
69
77
  /** Above the budget (N13): every command by name and summary, and the drilling command for one in full. */
70
78
  export declare function summaryOf(manifest: Manifest, budget: number): SchemaSummary;
71
79
  export declare function schemaOf(manifest: Manifest): ProgramSchema;
80
+ /** The machine document, compact unless a person asked for the readable one. */
81
+ export declare const machineJson: (value: unknown, head: readonly string[]) => string;
package/dist/schema.js CHANGED
@@ -30,6 +30,8 @@ function optionProperty(name, spec) {
30
30
  p.minimum = spec.minimum;
31
31
  if (spec.maximum !== undefined)
32
32
  p.maximum = spec.maximum;
33
+ if (spec.sharedFrom !== undefined)
34
+ p.sharedFrom = spec.sharedFrom;
33
35
  return p;
34
36
  }
35
37
  export function inputSchemaOf(node) {
@@ -68,6 +70,12 @@ export function commandSchemaOf(node, root) {
68
70
  out.effects = node.effects;
69
71
  if (node.deprecated !== undefined)
70
72
  out.deprecated = node.deprecated;
73
+ if (node.group !== undefined)
74
+ out.group = node.group;
75
+ if (node.load !== undefined)
76
+ out.lazy = true;
77
+ if (node.plugin !== undefined)
78
+ out.plugin = node.plugin;
71
79
  return out;
72
80
  }
73
81
  export function runnable(manifest) {
@@ -106,3 +114,6 @@ export function schemaOf(manifest) {
106
114
  out.description = description;
107
115
  return out;
108
116
  }
117
+ const JSON_PRETTY = '--format=json-pretty';
118
+ const INDENT = 2;
119
+ export const machineJson = (value, head) => JSON.stringify(value, null, head.includes(JSON_PRETTY) ? INDENT : 0);