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