burgee 0.11.1 → 0.12.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.
Files changed (51) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.d.ts +1 -90
  3. package/dist/cli.js +3 -151
  4. package/dist/commander/command.d.ts +22 -0
  5. package/dist/commander/command.js +41 -10
  6. package/dist/compat.d.ts +39 -2
  7. package/dist/compat.js +90 -2
  8. package/dist/complete-dynamic.d.ts +18 -0
  9. package/dist/complete-dynamic.js +19 -0
  10. package/dist/completions.js +32 -14
  11. package/dist/config-explain.d.ts +22 -0
  12. package/dist/config-explain.js +33 -0
  13. package/dist/define-error.d.ts +29 -0
  14. package/dist/define-error.js +29 -0
  15. package/dist/errors.d.ts +29 -0
  16. package/dist/errors.js +17 -0
  17. package/dist/execute.d.ts +6 -0
  18. package/dist/execute.js +71 -13
  19. package/dist/facade-failure.d.ts +17 -0
  20. package/dist/facade-failure.js +12 -0
  21. package/dist/fields.d.ts +32 -0
  22. package/dist/fields.js +45 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/manifest.d.ts +35 -5
  26. package/dist/manifest.js +6 -0
  27. package/dist/mcp.js +88 -5
  28. package/dist/migrate.d.ts +57 -7
  29. package/dist/migrate.js +413 -34
  30. package/dist/parse-hooks.d.ts +7 -0
  31. package/dist/parse-hooks.js +17 -0
  32. package/dist/plugin.d.ts +2 -6
  33. package/dist/plugin.js +1 -1
  34. package/dist/program-schema.json +1 -0
  35. package/dist/program.d.ts +90 -0
  36. package/dist/program.js +151 -0
  37. package/dist/runtime.d.ts +3 -1
  38. package/dist/runtime.js +3 -0
  39. package/dist/schema.d.ts +7 -0
  40. package/dist/schema.js +6 -11
  41. package/dist/schema.json +1 -1
  42. package/dist/stdin-dash.d.ts +15 -0
  43. package/dist/stdin-dash.js +12 -0
  44. package/dist/validate.d.ts +1 -27
  45. package/dist/validate.js +2 -17
  46. package/dist/yargs/factory.js +39 -13
  47. package/dist/yargs-parser.d.ts +8 -1
  48. package/dist/yargs-parser.js +1 -1
  49. package/dist/yargs.d.ts +1 -0
  50. package/dist/yargs.js +1 -0
  51. package/package.json +7 -6
@@ -48,13 +48,18 @@ const flags = (name, spec) => {
48
48
  return spec.negatable === true ? [...spellings, `--no-${kebab(name)}`] : spellings;
49
49
  };
50
50
  const takesValue = (spec) => spec.type !== 'boolean';
51
+ const callback = (program, node, name, partial) => [program, '__complete', ...node.path, `--${kebab(name)}`, partial].join(' ');
52
+ const isDynamic = (spec) => spec.complete !== undefined;
53
+ const promise = (program, root) => walk(root).some((n) => Object.values(n.options).some(isDynamic)) ? `runs ${program} on TAB only for options that declare a completer` : `never runs ${program} on TAB`;
51
54
  const fname = (program, path) => `_${[program, ...path].join('_').replaceAll(/[^A-Za-z0-9_]/g, '_')}`;
52
55
  function bashCase(program, node) {
53
56
  const opts = Object.entries(node.options).flatMap(([n, s]) => flags(n, s));
54
57
  const subs = node.children.map((c) => c.path[c.path.length - 1] ?? '');
55
58
  const valued = Object.entries(node.options)
56
59
  .filter(([, s]) => takesValue(s))
57
- .map(([n, s]) => ` ${flags(n, s).join('|')}) COMPREPLY=($(compgen -W ${sq((s.choices ?? []).join(' '))} -- "$cur")); return ;;`)
60
+ .map(([n, s]) => isDynamic(s)
61
+ ? ` ${flags(n, s).join('|')}) COMPREPLY=($(compgen -W "$(${callback(program, node, n, '"$cur"')} 2>/dev/null)" -- "$cur")); return ;;`
62
+ : ` ${flags(n, s).join('|')}) COMPREPLY=($(compgen -W ${sq((s.choices ?? []).join(' '))} -- "$cur")); return ;;`)
58
63
  .join('\n');
59
64
  return ` ${fname(program, node.path)}() {
60
65
  case "$prev" in
@@ -70,7 +75,7 @@ function bash(program, root) {
70
75
  .filter((n) => n.path.length > 0)
71
76
  .map((n) => ` ${sq(n.path.join(' '))}) ${fname(program, n.path)}; return ;;`)
72
77
  .join('\n');
73
- return `# ${program} completion for bash, generated by burgee — never runs ${program} on TAB.
78
+ return `# ${program} completion for bash, generated by burgee — ${promise(program, root)}.
74
79
  # Install: ${program} completion bash > ~/.local/share/bash-completion/completions/${program}
75
80
  _${program}_main() {
76
81
  local cur prev words cword
@@ -95,16 +100,17 @@ ${dispatch}
95
100
  complete -F _${program}_main ${program}
96
101
  `;
97
102
  }
98
- function zshOptionSpec(name, spec) {
103
+ function zshOptionSpec(program, node, name, spec) {
99
104
  const desc = (spec.description ?? '').replaceAll(/[[\]:]/g, ' ');
100
- const action = spec.choices === undefined ? ' ' : `(${spec.choices.join(' ')})`;
105
+ const listed = spec.choices === undefined ? ' ' : `(${spec.choices.join(' ')})`;
106
+ const action = isDynamic(spec) ? `{compadd -- \${(f)"$(${callback(program, node, name, '"$PREFIX"')} 2>/dev/null)"}}` : listed;
101
107
  const value = takesValue(spec) ? `:${name}:${action}` : '';
102
108
  return flags(name, spec)
103
109
  .map((f) => sq(`${f}[${desc}]${value}`))
104
110
  .join(' ');
105
111
  }
106
112
  function zshFunction(program, node) {
107
- const options = Object.entries(node.options).map(([n, s]) => zshOptionSpec(n, s));
113
+ const options = Object.entries(node.options).map(([n, s]) => zshOptionSpec(program, node, n, s));
108
114
  if (node.children.length === 0) {
109
115
  return `${fname(program, node.path)}() {\n _arguments -s ${options.join(' ')} '*: :_files'\n}`;
110
116
  }
@@ -124,7 +130,7 @@ ${dispatch}
124
130
  }
125
131
  function zsh(program, root) {
126
132
  return `#compdef ${program}
127
- # ${program} completion for zsh, generated by burgee — never runs ${program} on TAB.
133
+ # ${program} completion for zsh, generated by burgee — ${promise(program, root)}.
128
134
  # Install: ${program} completion zsh > "\${fpath[1]}/_${program}" && compinit
129
135
  ${walk(root).map((n) => zshFunction(program, n)).join('\n')}
130
136
  if [[ $zsh_eval_context[-1] == loadautofunc ]]; then ${fname(program, [])} "$@"; else compdef ${fname(program, [])} ${program}; fi
@@ -135,9 +141,10 @@ function fishCondition(node) {
135
141
  return '__fish_use_subcommand';
136
142
  return node.path.map((seg) => `__fish_seen_subcommand_from ${seg}`).join('; and ');
137
143
  }
138
- function fishOption(program, cond, name, spec) {
144
+ function fishOption({ program, cond, node }, name, spec) {
139
145
  const short = spec.short === undefined ? '' : ` -s ${spec.short}`;
140
- const choices = spec.choices === undefined ? '' : ` -f -a ${sq(spec.choices.join(' '))}`;
146
+ const listed = spec.choices === undefined ? '' : ` -f -a ${sq(spec.choices.join(' '))}`;
147
+ const choices = isDynamic(spec) && node !== undefined ? ` -f -a ${sq(`(${callback(program, node, name, '(commandline -ct)')} 2>/dev/null)`)}` : listed;
141
148
  const value = takesValue(spec) ? ` -r${choices}` : '';
142
149
  return `complete -c ${program} -n ${cond}${short} -l ${name}${value} -d ${sq(spec.description ?? '')}`;
143
150
  }
@@ -148,14 +155,14 @@ function fish(program, root) {
148
155
  for (const c of node.children)
149
156
  lines.push(`complete -c ${program} -n ${cond} -f -a ${c.path[c.path.length - 1] ?? ''} -d ${sq(c.description)}`);
150
157
  for (const [name, spec] of Object.entries(node.options)) {
151
- lines.push(fishOption(program, cond, kebab(name), spec));
158
+ lines.push(fishOption({ program, cond, node }, kebab(name), spec));
152
159
  if (spec.negatable === true) {
153
160
  const { short: _short, ...unshort } = spec;
154
- lines.push(fishOption(program, cond, `no-${kebab(name)}`, unshort));
161
+ lines.push(fishOption({ program, cond }, `no-${kebab(name)}`, unshort));
155
162
  }
156
163
  }
157
164
  }
158
- return `# ${program} completion for fish, generated by burgee — never runs ${program} on TAB.
165
+ return `# ${program} completion for fish, generated by burgee — ${promise(program, root)}.
159
166
  # Install: ${program} completion fish > ~/.config/fish/completions/${program}.fish
160
167
  ${lines.join('\n')}
161
168
  `;
@@ -166,10 +173,14 @@ function pwshEntry(node) {
166
173
  const values = Object.entries(node.options)
167
174
  .filter(([, s]) => s.choices !== undefined)
168
175
  .map(([n, s]) => `'--${n}' = @(${(s.choices ?? []).map((c) => `'${c}'`).join(', ')})`);
169
- return ` '${node.path.join(' ')}' = @{ Items = @(${[...subs, ...opts].join(', ')}); Values = @{ ${values.join('; ')} } }`;
176
+ const dynamic = Object.entries(node.options)
177
+ .filter(([, s]) => isDynamic(s))
178
+ .map(([n]) => `'--${kebab(n)}' = $true`);
179
+ const callbacks = dynamic.length === 0 ? '' : `; Dynamic = @{ ${dynamic.join('; ')} }`;
180
+ return ` '${node.path.join(' ')}' = @{ Items = @(${[...subs, ...opts].join(', ')}); Values = @{ ${values.join('; ')} }${callbacks} }`;
170
181
  }
171
182
  function pwsh(program, root) {
172
- return `# ${program} completion for PowerShell, generated by burgee — never runs ${program} on TAB.
183
+ return `# ${program} completion for PowerShell, generated by burgee — ${promise(program, root)}.
173
184
  # Install: ${program} completion pwsh > ${program}-completion.ps1, then dot-source it from $PROFILE
174
185
  Register-ArgumentCompleter -Native -CommandName '${program}' -ScriptBlock {
175
186
  param($wordToComplete, $commandAst, $cursorPosition)
@@ -186,7 +197,14 @@ ${walk(root).map(pwshEntry).join('\n')}
186
197
  }
187
198
  $entry = $table[$path]
188
199
  $prev = if ($words.Count -gt 0) { $words[$words.Count - 1] } else { '' }
189
- if ($entry.Values.ContainsKey($prev)) {
200
+ ${walk(root).some((n) => Object.values(n.options).some(isDynamic))
201
+ ? ` if ($entry.Dynamic -and $entry.Dynamic.ContainsKey($prev)) {
202
+ $steps = @($path -split ' ' | Where-Object { $_ -ne '' })
203
+ & '${program}' __complete @steps $prev $wordToComplete 2>$null | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
204
+ return
205
+ }
206
+ `
207
+ : ''} if ($entry.Values.ContainsKey($prev)) {
190
208
  $entry.Values[$prev] | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
191
209
  return
192
210
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2026 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /**
7
+ * V8 / D-117 — `config explain [command…]`: the precedence, and every option's value with the
8
+ * source that won, **generated from the resolver** rather than written in a README. Ten of ten
9
+ * surveyed CLIs read config and env; three of ten document the order. This one prints it,
10
+ * from `seniority`'s own `ORDER`, so the table cannot disagree with what a run does.
11
+ *
12
+ * It resolves without the command line's own flags — it explains the configuration, not one
13
+ * invocation — except `--config <path>` and `--no-config`, which choose what is read. With
14
+ * `--json` it answers as data.
15
+ *
16
+ * Imported by `execute.ts` only for `config explain` (M2).
17
+ */
18
+ import { type Resolution } from 'seniority/precedence';
19
+ import { type Manifest, type OptionSpec } from './manifest.js';
20
+ type Resolve = (specs: Record<string, OptionSpec>, flags: Record<string, unknown>) => Promise<Resolution>;
21
+ export declare function explainConfig(manifest: Manifest, argv: readonly string[], resolve: Resolve): Promise<string>;
22
+ export {};
@@ -0,0 +1,33 @@
1
+ import { ORDER } from 'seniority/precedence';
2
+ import { kebab } from './names.js';
3
+ const label = (source) => (source === 'package' ? 'package.json' : source);
4
+ function where(p) {
5
+ if (p?.location === undefined)
6
+ return '';
7
+ return p.line === undefined ? p.location : `${p.location}:${String(p.line)}`;
8
+ }
9
+ function shown(value) {
10
+ if (value === undefined)
11
+ return '—';
12
+ return typeof value === 'string' ? value : JSON.stringify(value);
13
+ }
14
+ export async function explainConfig(manifest, argv, resolve) {
15
+ const json = argv.includes('--json');
16
+ const at = argv.indexOf('--config');
17
+ const flags = { ...(at === -1 ? {} : { config: argv[at + 1] }), ...(argv.includes('--no-config') ? { noConfig: true } : {}) };
18
+ const path = argv.filter((a, i) => !a.startsWith('--') && (at === -1 || i !== at + 1));
19
+ const { node } = manifest.resolve([...path], manifest.rootPath);
20
+ const specs = node?.options ?? {};
21
+ const resolved = await resolve(specs, flags);
22
+ const rows = Object.keys(specs).map((name) => {
23
+ const p = resolved.provenance[name];
24
+ const location = where(p);
25
+ return { option: `--${kebab(name)}`, value: resolved.values[name], source: p === undefined ? 'unset' : label(p.source), ...(location === '' ? {} : { location }) };
26
+ });
27
+ const precedence = ORDER.map(label);
28
+ if (json)
29
+ return `${JSON.stringify({ ok: true, data: { precedence, options: rows }, meta: {} })}\n`;
30
+ const width = Math.max(0, ...rows.map((r) => r.option.length));
31
+ const lines = rows.map((r) => ` ${r.option.padEnd(width)} ${shown(r.value)} (${r.source}${r.location === undefined ? '' : ` ${r.location}`})`);
32
+ return `precedence: ${precedence.join(' > ')} — the first that sets a value wins\n${lines.length === 0 ? ' (this command takes no options)' : lines.join('\n')}\n`;
33
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Copyright (c) 2026 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /** What an author declares. */
7
+ export interface ErrorDefinition {
8
+ /** The class name, shown as the error's `name`. */
9
+ name: string;
10
+ /** The exit code, 7–125, owned by this class alone. */
11
+ code: number;
12
+ }
13
+ /** What `new` takes: the message, and optionally the E3 `hint` and exact `fix`. */
14
+ export interface DefinedErrorOptions {
15
+ hint?: string;
16
+ fix?: string;
17
+ }
18
+ /** An author-defined error class; `exitCode` is the code it leaves with. */
19
+ export interface DefinedErrorClass {
20
+ new (message: string, options?: DefinedErrorOptions): Error & DefinedErrorOptions;
21
+ readonly exitCode: number;
22
+ }
23
+ /**
24
+ * Where the engine reads the code: a registered symbol on the class, so `execute.ts` finds it
25
+ * without importing this module — only a program that defines an error pays for defining one.
26
+ * A subclass inherits it the way statics are inherited.
27
+ */
28
+ export declare const EXIT_CODE: unique symbol;
29
+ export declare function defineError(definition: ErrorDefinition): DefinedErrorClass;
@@ -0,0 +1,29 @@
1
+ const MIN = 7;
2
+ const MAX = 125;
3
+ export const EXIT_CODE = Symbol.for('burgee.exitCode');
4
+ const owners = new Map();
5
+ export function defineError(definition) {
6
+ const { name, code } = definition;
7
+ if (!Number.isInteger(code) || code < MIN || code > MAX) {
8
+ throw new RangeError(`burgee: defineError "${name}" claims exit code ${String(code)}; use an integer from ${String(MIN)} to ${String(MAX)} — 0–6 and 130 are the contract's own`);
9
+ }
10
+ const owner = owners.get(code);
11
+ if (owner !== undefined)
12
+ throw new Error(`burgee: defineError "${name}" claims exit code ${String(code)}, which "${owner}" already owns; one code, one class (E7)`);
13
+ owners.set(code, name);
14
+ const Defined = class extends Error {
15
+ static exitCode = code;
16
+ static [EXIT_CODE] = code;
17
+ hint;
18
+ fix;
19
+ constructor(message, options = {}) {
20
+ super(message);
21
+ this.name = name;
22
+ if (options.hint !== undefined)
23
+ this.hint = options.hint;
24
+ if (options.fix !== undefined)
25
+ this.fix = options.fix;
26
+ }
27
+ };
28
+ return Defined;
29
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The error classes an author throws to name an exit code (E6, E7).
3
+ *
4
+ * Their own module, and a small one, so the commander and yargs façades can recognise an
5
+ * `AuthError` without reaching `validate.ts` and the whole run-time validator behind it. Before
6
+ * they could, both filed every handler failure as `runtime`, exit 1, and an `AuthError` thrown
7
+ * from a commander-syntax action told the caller *read the message* where E6 promises *log in
8
+ * and run it again* (D-140). `validate.ts` re-exports both, so no import path changed.
9
+ */
10
+ /** A usage problem the caller can fix, carrying the flag that fixes it (E3). */
11
+ export declare class UsageError extends Error {
12
+ readonly hint?: string | undefined;
13
+ constructor(message: string, hint?: string | undefined);
14
+ }
15
+ /**
16
+ * E6 — the far side said no. Throw this and the run leaves with `ExitCode.AUTH`.
17
+ *
18
+ * The one error class whose *response* is unambiguous: not "read the message and decide" but
19
+ * "get a credential and run it again". A handler that throws a bare `Error` for a 401 gets
20
+ * `RUNTIME`, which is the code for everything, and a caller retrying on it retries forever.
21
+ *
22
+ * `fix` is the exact command that gets the credential, where the program knows it — `hint` is
23
+ * prose a person reads and `fix` is a line a caller runs, which is the turn the field saves.
24
+ */
25
+ export declare class AuthError extends Error {
26
+ readonly hint?: string | undefined;
27
+ readonly fix?: string | undefined;
28
+ constructor(message: string, hint?: string | undefined, fix?: string | undefined);
29
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,17 @@
1
+ export class UsageError extends Error {
2
+ hint;
3
+ constructor(message, hint) {
4
+ super(message);
5
+ this.hint = hint;
6
+ }
7
+ }
8
+ export class AuthError extends Error {
9
+ hint;
10
+ fix;
11
+ constructor(message, hint, fix) {
12
+ super(message);
13
+ this.hint = hint;
14
+ this.fix = fix;
15
+ this.name = 'AuthError';
16
+ }
17
+ }
package/dist/execute.d.ts CHANGED
@@ -47,6 +47,12 @@ export interface Command<S extends OptionSpecs = OptionSpecs> {
47
47
  effects?: DeclaredEffects;
48
48
  /** Relationships between options, validated before choices and the handler (S2, S6). */
49
49
  relations?: readonly Relation[];
50
+ /**
51
+ * The top-level fields of this command's result (N14). With them, `--json=` lists them
52
+ * without running the handler and `--json=a,b` refuses an unknown field before it runs.
53
+ * Without them `--json=a,b` still selects, checked against the result's own keys.
54
+ */
55
+ fields?: readonly string[];
50
56
  /** Absent on a group that only holds subcommands. `NoInfer`: the spec fixes S, the handler only reads it. */
51
57
  run?: (ctx: CommandContext<InferOptions<NoInfer<S>>>) => unknown;
52
58
  /** The handler's module, imported on dispatch only (M2); everything else about the command is declared here. */
package/dist/execute.js CHANGED
@@ -32,6 +32,8 @@ function helpFields(c) {
32
32
  node.effects = c.effects;
33
33
  if (c.relations !== undefined)
34
34
  node.relations = c.relations;
35
+ if (c.fields !== undefined)
36
+ node.fields = c.fields;
35
37
  return node;
36
38
  }
37
39
  export function defineCommand(command) {
@@ -169,17 +171,20 @@ async function configLayers(name, values, io) {
169
171
  out.pkg = pkg;
170
172
  return out;
171
173
  }
172
- async function resolveValues(manifest, specs, values, io) {
174
+ async function resolution(manifest, specs, values, io) {
173
175
  const layers = { flags: values, env: io.env };
174
176
  if (manifest.envPrefix !== undefined)
175
177
  layers.envPrefix = manifest.envPrefix;
176
178
  if (manifest.config !== undefined)
177
179
  Object.assign(layers, await configLayers(manifest.config.name, values, io));
178
- const resolution = resolveLayers(specs, layers);
179
- const out = { values: resolution.values, provenance: resolution.provenance };
180
+ return resolveLayers(specs, layers);
181
+ }
182
+ async function resolveValues(manifest, specs, values, io) {
183
+ const resolved = await resolution(manifest, specs, values, io);
184
+ const out = { values: resolved.values, provenance: resolved.provenance };
180
185
  const asked = values['explain'];
181
186
  if (typeof asked === 'string')
182
- out.explainText = (await import('seniority/explain')).explain(asked, resolution);
187
+ out.explainText = (await import('seniority/explain')).explain(asked, resolved);
183
188
  for (const [name, spec] of Object.entries(specs)) {
184
189
  if (out.values[name] === undefined && spec.required === true && out.explainText === undefined) {
185
190
  throw new UsageError(`missing required option --${kebab(name)}`, `pass --${kebab(name)} <value>`);
@@ -216,6 +221,21 @@ const CLASSIFIED = [
216
221
  [AuthError, ExitCode.AUTH],
217
222
  [ConfigError, ExitCode.CONFIG],
218
223
  ];
224
+ const NAMED = new Map([
225
+ ['USAGE', ExitCode.USAGE],
226
+ ['CONFIG', ExitCode.CONFIG],
227
+ ['CANCELLED', ExitCode.CANCELLED],
228
+ ['AUTH', ExitCode.AUTH],
229
+ ]);
230
+ function namedCode(cause) {
231
+ return NAMED.get(cause?.code);
232
+ }
233
+ function messageOf(cause) {
234
+ if (cause instanceof Error)
235
+ return cause.message;
236
+ const said = cause?.message;
237
+ return typeof said === 'string' ? said : String(cause);
238
+ }
219
239
  function carried(cause) {
220
240
  const { hint, fix } = (cause ?? {});
221
241
  return {
@@ -227,12 +247,18 @@ async function describeFailure(cause, argv, node) {
227
247
  const signal = exitSignal(cause);
228
248
  if (signal !== undefined)
229
249
  return { code: signal, message: '', silent: true };
230
- const message = cause instanceof Error ? cause.message : String(cause);
250
+ const message = messageOf(cause);
231
251
  if (cause instanceof ActionRequired)
232
252
  return { code: ExitCode.CANCELLED, message, action: cause.spec, ...(cause.spec.hint === undefined ? {} : { hint: cause.spec.hint }) };
233
253
  const named = CLASSIFIED.find(([Class]) => cause instanceof Class);
234
254
  if (named !== undefined)
235
255
  return { code: named[1], message, ...carried(cause) };
256
+ const own = cause?.constructor?.[Symbol.for('burgee.exitCode')];
257
+ if (typeof own === 'number')
258
+ return { code: own, message, ...carried(cause) };
259
+ const byName = namedCode(cause);
260
+ if (byName !== undefined)
261
+ return { code: byName, message, ...carried(cause) };
236
262
  if (isParseArgsFailure(cause)) {
237
263
  const explain = await import('./unknown-option.js');
238
264
  const dash = explain.singleDashHint(argv);
@@ -292,7 +318,7 @@ async function unresolved({ manifest, root, io }, argv, at) {
292
318
  const typed = argv.slice(node.path.length - root.length);
293
319
  const first = typed[0] ?? '';
294
320
  if (typed.length > 0 && HELP_FLAGS.has(first)) {
295
- if (beforeTerminator(typed).includes('--json'))
321
+ if (beforeTerminator(typed).some(isJsonFlag))
296
322
  return { text: `${await machineJson(await helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
297
323
  return { text: await renderHelp(manifest, node, io), code: ExitCode.OK };
298
324
  }
@@ -319,6 +345,16 @@ async function completion(manifest, argv, io) {
319
345
  }
320
346
  async function surface(manifest, argv, io) {
321
347
  const head = beforeTerminator(argv);
348
+ if (argv[0] === '__complete') {
349
+ await (await import('./complete-dynamic.js')).completeDynamic(manifest, argv.slice(1), (t) => io.out.write(t));
350
+ return true;
351
+ }
352
+ const root = manifest.rootPath;
353
+ if (head[0] === 'config' && head[1] === 'explain' && manifest.config !== undefined && manifest.find([...root, 'config', 'explain']) === undefined) {
354
+ const { explainConfig } = await import('./config-explain.js');
355
+ io.out.write(await explainConfig(manifest, head.slice(2), (specs, flags) => resolution(manifest, specs, flags, io)));
356
+ return true;
357
+ }
322
358
  if (await completion(manifest, argv, io))
323
359
  return true;
324
360
  if (argv[0] === 'help') {
@@ -369,13 +405,22 @@ function exitCodeOf(data) {
369
405
  const code = isPlainObject(data) ? data['exitCode'] : undefined;
370
406
  return isExitCode(code) ? code : ExitCode.OK;
371
407
  }
408
+ function isJsonFlag(arg) {
409
+ return arg === '--json' || arg.startsWith('--json=');
410
+ }
372
411
  function versionOf(manifest, io) {
373
412
  const declared = manifest.version ?? (typeof io.pkg?.data['version'] === 'string' ? io.pkg.data['version'] : undefined);
374
413
  if (declared === undefined)
375
414
  throw new ConfigError('no version declared', 'pass version to defineProgram, or set "version" in the owning package.json');
376
415
  return declared;
377
416
  }
378
- async function dispatch(manifest, { node, rest, name }, io) {
417
+ async function dispatch(manifest, { node, rest: typed, name }, io) {
418
+ const select = typed.some((a) => a.startsWith('--json=')) ? await import('./fields.js') : undefined;
419
+ const { args: rest, fields } = select?.jsonFields(typed) ?? { args: typed };
420
+ if (fields?.length === 0)
421
+ return { json: true, text: `${JSON.stringify(select?.listFields(node))}\n` };
422
+ if (fields !== undefined)
423
+ select?.checkFields(fields, node);
379
424
  const parsed = parseArgs({ args: rest, options: toParseConfig(node.options, manifest.config !== undefined), allowPositionals: true, strict: true, tokens: true });
380
425
  const flags = canonical(parsed.values, node.options, parsed.tokens);
381
426
  const json = flags.json === true;
@@ -397,10 +442,12 @@ async function dispatch(manifest, { node, rest, name }, io) {
397
442
  await manifest.fire('preRun', name, values);
398
443
  const detection = detectAgent(io.env, io.tty);
399
444
  const onExit = (handler, label) => io.teardown.add(handler, label);
400
- const data = await node.run({ options: values, positionals, passthrough, env: io.env, exit: ctxExit, onExit, actionRequired, ...detection });
445
+ const stdin = positionals.includes('-') ? (await import('./stdin-dash.js')).stdinFor(node, positionals, io.stdin) : {};
446
+ const data = await node.run({ options: values, positionals, passthrough, ...stdin, env: io.env, exit: ctxExit, onExit, actionRequired, ...detection });
401
447
  await manifest.fire('postRun', name, values);
402
448
  const changed = changedOf(node, data);
403
- return { json, data, provenance, ...(changed === undefined ? {} : { changed }) };
449
+ const selected = fields === undefined || select === undefined ? data : select.selectFields(data, fields);
450
+ return { json, data: selected, provenance, ...(changed === undefined ? {} : { changed }) };
404
451
  }
405
452
  function requirePositionals(node, positionals) {
406
453
  const required = (node.arguments ?? []).filter((a) => a.required !== false && a.variadic !== true);
@@ -440,11 +487,17 @@ async function report(cause, { manifest, io, argv, json, name }) {
440
487
  const next = runnableNext(manifest, failure.action, json);
441
488
  const rendered = { ...failure, action: { ...failure.action, next } };
442
489
  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 } };
443
- io.err.write(json ? `${JSON.stringify(body)}\n` : textFailure(rendered));
490
+ if (json)
491
+ io.out.write(`${JSON.stringify(body)}\n`);
492
+ else
493
+ io.err.write(textFailure(rendered));
444
494
  return await leave(io, failure.code);
445
495
  }
446
496
  const body = { code: failure.code, message: failure.message, hint: failure.hint, ...(failure.fix === undefined ? {} : { fix: failure.fix }) };
447
- io.err.write(json ? `${JSON.stringify({ ok: false, error: body })}\n` : textFailure(failure));
497
+ if (json)
498
+ io.out.write(`${JSON.stringify({ ok: false, error: body })}\n`);
499
+ else
500
+ io.err.write(textFailure(failure));
448
501
  return await leave(io, failure.code);
449
502
  }
450
503
  function ioOf(opts) {
@@ -465,11 +518,16 @@ function ioOf(opts) {
465
518
  export async function execute(manifest, opts = {}) {
466
519
  const io = ioOf(opts);
467
520
  const raw = opts.argv ?? host.argv;
468
- const argv = opts.argv === undefined || opts.from === 'node' ? raw.slice(2) : raw;
521
+ const typed = opts.argv === undefined || opts.from === 'node' ? raw.slice(2) : raw;
469
522
  const root = opts.root ?? manifest.rootPath;
470
- let json = beforeTerminator(argv).includes('--json');
523
+ let json = beforeTerminator(typed).some(isJsonFlag);
471
524
  let name = '';
525
+ if (manifest.declares('shutdown'))
526
+ io.teardown.add(async () => await manifest.fire('shutdown', name, {}), 'plugin shutdown hooks');
527
+ let argv = typed;
472
528
  try {
529
+ argv = manifest.declares('parse') ? await manifest.parse([...typed]) : typed;
530
+ json = beforeTerminator(argv).some(isJsonFlag);
473
531
  if (await surface(manifest, argv, io))
474
532
  return await leave(io, ExitCode.OK);
475
533
  const { node, rest } = manifest.resolve(argv, root);
@@ -0,0 +1,17 @@
1
+ import { ExitCode } from './exit-code.js';
2
+ /** A handler's failure as a façade reports it: the E1 code, the envelope's word for it, and the E3 fields. */
3
+ export interface HandlerFailure {
4
+ exit: ExitCode;
5
+ error: {
6
+ code: 'usage' | 'auth' | 'runtime';
7
+ message: string;
8
+ hint?: string;
9
+ fix?: string;
10
+ };
11
+ }
12
+ /**
13
+ * Anything a handler threw or rejected with — an `Error`, a subclass that names its code, or a
14
+ * value that is not an Error at all — as one failure. Never a stack: the envelope is for a
15
+ * caller that branches on `code`, and the prose line carries the message.
16
+ */
17
+ export declare function handlerFailure(cause: unknown): HandlerFailure;
@@ -0,0 +1,12 @@
1
+ import { AuthError, UsageError } from './errors.js';
2
+ import { ExitCode } from './exit-code.js';
3
+ const CLASSES = [
4
+ [AuthError, [ExitCode.AUTH, 'auth']],
5
+ [UsageError, [ExitCode.USAGE, 'usage']],
6
+ ];
7
+ export function handlerFailure(cause) {
8
+ const message = cause instanceof Error ? cause.message : String(cause);
9
+ const [exit, code] = CLASSES.find(([Class]) => cause instanceof Class)?.[1] ?? [ExitCode.RUNTIME, 'runtime'];
10
+ const { hint, fix } = (cause ?? {});
11
+ return { exit, error: { code, message, ...(typeof hint === 'string' ? { hint } : {}), ...(typeof fix === 'string' ? { fix } : {}) } };
12
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2026 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /**
7
+ * N14 — what `--json=<fields>` does once it is typed: list the declared fields, refuse an
8
+ * unknown one naming the valid set, select the named fields of a result. Its own chunk,
9
+ * imported by `execute.ts` only when `--json=` is on the command line (M2).
10
+ */
11
+ import { type CommandNode } from './manifest.js';
12
+ /** `--json=` with nothing after it: the declared fields, without running the handler. */
13
+ export declare function listFields(node: CommandNode): {
14
+ ok: true;
15
+ data: {
16
+ fields: readonly string[];
17
+ };
18
+ meta: Record<string, never>;
19
+ };
20
+ /** An unknown field is refused with the valid set named — before the handler when they are declared. */
21
+ export declare function checkFields(fields: readonly string[], node: CommandNode): void;
22
+ /** The named fields of a result: of the object, or of each object in a list. */
23
+ export declare function selectFields(data: unknown, fields: readonly string[]): unknown;
24
+ /**
25
+ * N14 — `--json=a,b` becomes `--json` plus the fields it names; `--json=` alone is an empty
26
+ * list, which asks for the valid set. Only the `=` form takes fields, so `cmd --json name`
27
+ * keeps `name` a positional (D-114). Nothing after `--` is read (G5).
28
+ */
29
+ export declare function jsonFields(args: readonly string[]): {
30
+ args: string[];
31
+ fields?: string[];
32
+ };
package/dist/fields.js ADDED
@@ -0,0 +1,45 @@
1
+ import { UsageError } from './validate.js';
2
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ function declared(node) {
4
+ const { fields } = node;
5
+ if (fields === undefined)
6
+ return undefined;
7
+ if (fields.every((f) => f !== '' && !f.includes(',')) && new Set(fields).size === fields.length)
8
+ return fields;
9
+ throw new Error(`burgee: command "${node.path.slice(1).join(' ')}" declares fields ${JSON.stringify(fields)}; declare distinct, non-empty names without commas`);
10
+ }
11
+ export function listFields(node) {
12
+ const fields = declared(node);
13
+ if (fields === undefined) {
14
+ const typed = node.path.slice(1).join(' ');
15
+ throw new UsageError(`"${typed}" declares no fields to list`, `run "${typed} --json" to see its whole result`);
16
+ }
17
+ return { ok: true, data: { fields }, meta: {} };
18
+ }
19
+ export function checkFields(fields, node) {
20
+ const valid = declared(node);
21
+ if (valid !== undefined)
22
+ refuseUnknown(fields, valid);
23
+ }
24
+ function refuseUnknown(fields, valid) {
25
+ const unknown = fields.filter((f) => !valid.includes(f));
26
+ if (unknown.length > 0)
27
+ throw new UsageError(`unknown field${unknown.length === 1 ? '' : 's'} ${unknown.map((f) => `"${f}"`).join(', ')}`, `valid fields: ${valid.join(', ')}`);
28
+ }
29
+ export function selectFields(data, fields) {
30
+ const list = Array.isArray(data) ? data : [data];
31
+ const rows = list.filter(isPlainObject);
32
+ refuseUnknown(fields, [...new Set(rows.flatMap((row) => Object.keys(row)))]);
33
+ const pick = (row) => (isPlainObject(row) ? Object.fromEntries(fields.filter((f) => f in row).map((f) => [f, row[f]])) : row);
34
+ return Array.isArray(data) ? data.map(pick) : pick(data);
35
+ }
36
+ export function jsonFields(args) {
37
+ const end = args.indexOf('--');
38
+ const head = end === -1 ? args : args.slice(0, end);
39
+ const given = head.findLast((a) => a.startsWith('--json='));
40
+ if (given === undefined)
41
+ return { args: [...args] };
42
+ const fields = given.slice('--json='.length).split(',').map((f) => f.trim()).filter((f) => f !== '');
43
+ const kept = head.map((a) => (a.startsWith('--json=') ? '--json' : a));
44
+ return { args: end === -1 ? kept : [...kept, ...args.slice(end)], fields };
45
+ }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@
28
28
  * **Every `type` stays.** A type re-export is erased and costs a consumer nothing, so the
29
29
  * whole type surface is still importable from `burgee` and no typed program has to move.
30
30
  */
31
+ export { defineError, type DefinedErrorClass, type DefinedErrorOptions, type ErrorDefinition } from './define-error.js';
31
32
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
32
33
  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';
33
34
  export { checkCommand, checkDefinition } from './definition.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { defineError } from './define-error.js';
1
2
  export { ExitCode, isExitCode } from './exit-code.js';
2
3
  export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, } from './execute.js';
3
4
  export { checkCommand, checkDefinition } from './definition.js';