burgee 0.11.1 → 0.12.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/README.md +12 -0
- package/dist/cli.d.ts +1 -90
- package/dist/cli.js +3 -151
- package/dist/commander/command.d.ts +22 -0
- package/dist/commander/command.js +41 -10
- package/dist/compat.d.ts +39 -2
- package/dist/compat.js +90 -2
- package/dist/complete-dynamic.d.ts +18 -0
- package/dist/complete-dynamic.js +19 -0
- package/dist/completions.js +32 -14
- package/dist/config-explain.d.ts +22 -0
- package/dist/config-explain.js +33 -0
- package/dist/define-error.d.ts +29 -0
- package/dist/define-error.js +29 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +17 -0
- package/dist/execute.d.ts +6 -0
- package/dist/execute.js +71 -13
- package/dist/facade-failure.d.ts +17 -0
- package/dist/facade-failure.js +12 -0
- package/dist/fields.d.ts +32 -0
- package/dist/fields.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +35 -5
- package/dist/manifest.js +6 -0
- package/dist/mcp.js +68 -4
- package/dist/migrate.d.ts +57 -7
- package/dist/migrate.js +413 -34
- package/dist/parse-hooks.d.ts +7 -0
- package/dist/parse-hooks.js +17 -0
- package/dist/plugin.d.ts +2 -6
- package/dist/plugin.js +1 -1
- package/dist/program-schema.json +1 -0
- package/dist/program.d.ts +90 -0
- package/dist/program.js +151 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +3 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +6 -11
- package/dist/schema.json +1 -1
- package/dist/stdin-dash.d.ts +15 -0
- package/dist/stdin-dash.js +12 -0
- package/dist/validate.d.ts +1 -27
- package/dist/validate.js +2 -17
- package/dist/yargs/factory.js +39 -13
- package/dist/yargs-parser.d.ts +8 -1
- package/dist/yargs-parser.js +1 -1
- package/dist/yargs.d.ts +1 -0
- package/dist/yargs.js +1 -0
- package/package.json +7 -6
package/dist/completions.js
CHANGED
|
@@ -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]) =>
|
|
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 —
|
|
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
|
|
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 —
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
179
|
-
|
|
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,
|
|
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 =
|
|
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).
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/dist/fields.d.ts
ADDED
|
@@ -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';
|