burgee 0.12.0 → 0.13.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.
@@ -0,0 +1,90 @@
1
+ import { ConfigError } from 'seniority/precedence';
2
+ import { AuthError, UsageError } from './errors.js';
3
+ import { ExitCode, isExitCode } from './exit-code.js';
4
+ import { kebab } from './names.js';
5
+ function isParseArgsFailure(cause) {
6
+ if (!(cause instanceof Error))
7
+ return false;
8
+ const { code } = cause;
9
+ return typeof code === 'string' && code.startsWith('ERR_PARSE_ARGS_');
10
+ }
11
+ function exitSignal(cause) {
12
+ const code = cause?.code;
13
+ return typeof code === 'number' && isExitCode(code) ? code : undefined;
14
+ }
15
+ const CLASSIFIED = [
16
+ [UsageError, ExitCode.USAGE],
17
+ [AuthError, ExitCode.AUTH],
18
+ [ConfigError, ExitCode.CONFIG],
19
+ ];
20
+ const NAMED = new Map([
21
+ ['USAGE', ExitCode.USAGE],
22
+ ['CONFIG', ExitCode.CONFIG],
23
+ ['CANCELLED', ExitCode.CANCELLED],
24
+ ['AUTH', ExitCode.AUTH],
25
+ ]);
26
+ function namedCode(cause) {
27
+ return NAMED.get(cause?.code);
28
+ }
29
+ function messageOf(cause) {
30
+ if (cause instanceof Error)
31
+ return cause.message;
32
+ const said = cause?.message;
33
+ return typeof said === 'string' ? said : String(cause);
34
+ }
35
+ function carried(cause) {
36
+ const { hint, fix } = (cause ?? {});
37
+ return {
38
+ ...(typeof hint === 'string' ? { hint } : {}),
39
+ ...(typeof fix === 'string' ? { fix } : {}),
40
+ };
41
+ }
42
+ export async function describeFailure(cause, argv, node, action) {
43
+ const signal = exitSignal(cause);
44
+ if (signal !== undefined)
45
+ return { code: signal, message: '', silent: true };
46
+ const message = messageOf(cause);
47
+ if (action !== undefined)
48
+ return { code: ExitCode.CANCELLED, message, action, ...(action.hint === undefined ? {} : { hint: action.hint }) };
49
+ const named = CLASSIFIED.find(([Class]) => cause instanceof Class);
50
+ if (named !== undefined)
51
+ return { code: named[1], message, ...carried(cause) };
52
+ const own = cause?.constructor?.[Symbol.for('burgee.exitCode')];
53
+ if (typeof own === 'number')
54
+ return { code: own, message, ...carried(cause) };
55
+ const byName = namedCode(cause);
56
+ if (byName !== undefined)
57
+ return { code: byName, message, ...carried(cause) };
58
+ if (isParseArgsFailure(cause)) {
59
+ const explain = await import('./unknown-option.js');
60
+ const dash = explain.singleDashHint(argv);
61
+ if (dash !== undefined)
62
+ return { code: ExitCode.USAGE, message, hint: dash };
63
+ const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}).map(kebab));
64
+ return { code: ExitCode.USAGE, message, hint: 'run --help to see the available options', ...better };
65
+ }
66
+ return { code: ExitCode.RUNTIME, message };
67
+ }
68
+ function textFailure(failure) {
69
+ const hint = failure.hint === undefined ? '' : `hint: ${failure.hint}\n`;
70
+ const fix = failure.fix === undefined ? '' : `fix: ${failure.fix}\n`;
71
+ if (failure.action !== undefined) {
72
+ const next = (failure.action.next ?? []).map((n) => ` ${n.command} ${n.when}\n`).join('');
73
+ return `action required (${failure.action.reason}): ${failure.message}\n${next === '' ? '' : `next:\n${next}`}${hint}`;
74
+ }
75
+ return `error: ${failure.message}\n${hint}${fix}`;
76
+ }
77
+ function runnableNext(manifest, spec, json) {
78
+ const program = manifest.rootPath.join(' ');
79
+ return (spec.next ?? []).map((n) => ({ command: `${program} ${n.command}${json && !n.command.includes('--json') ? ' --json' : ''}`, when: n.when }));
80
+ }
81
+ export function failureText(failure, manifest, json) {
82
+ if (failure.action !== undefined) {
83
+ const next = runnableNext(manifest, failure.action, json);
84
+ const rendered = { ...failure, action: { ...failure.action, next } };
85
+ 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 } };
86
+ return json ? `${JSON.stringify(body)}\n` : textFailure(rendered);
87
+ }
88
+ const body = { code: failure.code, message: failure.message, hint: failure.hint, ...(failure.fix === undefined ? {} : { fix: failure.fix }) };
89
+ return json ? `${JSON.stringify({ ok: false, error: body })}\n` : textFailure(failure);
90
+ }
package/dist/fields.d.ts CHANGED
@@ -3,11 +3,6 @@
3
3
  * Licensed under the MIT License. Use of this source code is governed by the
4
4
  * MIT license that can be found in the LICENSE file.
5
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
6
  import { type CommandNode } from './manifest.js';
12
7
  /** `--json=` with nothing after it: the declared fields, without running the handler. */
13
8
  export declare function listFields(node: CommandNode): {
@@ -25,8 +20,13 @@ export declare function selectFields(data: unknown, fields: readonly string[]):
25
20
  * N14 — `--json=a,b` becomes `--json` plus the fields it names; `--json=` alone is an empty
26
21
  * list, which asks for the valid set. Only the `=` form takes fields, so `cmd --json name`
27
22
  * keeps `name` a positional (D-114). Nothing after `--` is read (G5).
23
+ *
24
+ * N15 — `--format=agent` is taken out before the parser sees it and hands back the formatter.
25
+ * A command that declares its own `format` option keeps the flag, because the program wins;
26
+ * and after `--` it is the command's, like everything else there.
28
27
  */
29
- export declare function jsonFields(args: readonly string[]): {
28
+ export declare function jsonFields(args: readonly string[], node: CommandNode): {
30
29
  args: string[];
31
30
  fields?: string[];
31
+ lines?: (data: unknown) => string;
32
32
  };
package/dist/fields.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { agentLines } from './agent-format.js';
1
2
  import { UsageError } from './validate.js';
2
3
  const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
3
4
  function declared(node) {
@@ -33,13 +34,18 @@ export function selectFields(data, fields) {
33
34
  const pick = (row) => (isPlainObject(row) ? Object.fromEntries(fields.filter((f) => f in row).map((f) => [f, row[f]])) : row);
34
35
  return Array.isArray(data) ? data.map(pick) : pick(data);
35
36
  }
36
- export function jsonFields(args) {
37
+ const AGENT = '--format=agent';
38
+ export function jsonFields(args, node) {
37
39
  const end = args.indexOf('--');
38
- const head = end === -1 ? args : args.slice(0, end);
40
+ const all = end === -1 ? args : args.slice(0, end);
41
+ const agent = node.options['format'] === undefined && all.includes(AGENT);
42
+ const head = agent ? all.filter((a) => a !== AGENT) : all;
43
+ const lines = agent ? { lines: agentLines } : {};
44
+ const tail = end === -1 ? [] : args.slice(end);
39
45
  const given = head.findLast((a) => a.startsWith('--json='));
40
46
  if (given === undefined)
41
- return { args: [...args] };
47
+ return { args: [...head, ...tail], ...lines };
42
48
  const fields = given.slice('--json='.length).split(',').map((f) => f.trim()).filter((f) => f !== '');
43
49
  const kept = head.map((a) => (a.startsWith('--json=') ? '--json' : a));
44
- return { args: end === -1 ? kept : [...kept, ...args.slice(end)], fields };
50
+ return { args: [...kept, ...tail], fields, ...lines };
45
51
  }
package/dist/mcp.js CHANGED
@@ -62,6 +62,24 @@ export function argvOf(node, root, args) {
62
62
  }
63
63
  const STDOUT_CONSOLE = ['log', 'info', 'debug', 'dir', 'dirxml', 'table', 'group', 'groupCollapsed', 'groupEnd', 'count', 'countReset', 'time', 'timeLog', 'timeEnd'];
64
64
  let framing = false;
65
+ let sessions = 0;
66
+ let release = () => undefined;
67
+ function holdStdout() {
68
+ if (sessions++ === 0) {
69
+ const stdout = host.stdout;
70
+ const write = stdout.write;
71
+ stdout.write = function (...args) {
72
+ return Reflect.apply(framing ? write : host.stderr.write, framing ? stdout : host.stderr, args);
73
+ };
74
+ release = () => void (stdout.write = write);
75
+ }
76
+ let held = true;
77
+ return () => {
78
+ if (held && --sessions === 0)
79
+ release();
80
+ held = false;
81
+ };
82
+ }
65
83
  async function printedBy(run) {
66
84
  const chunks = [];
67
85
  const decoder = new TextDecoder();
@@ -162,7 +180,8 @@ export function startMcp(manifest, opts) {
162
180
  session.invoke = invoke;
163
181
  reply({ method: 'notifications/tools/list_changed' });
164
182
  };
165
- const done = serve(session, opts.input, reply);
183
+ const unhold = holdStdout();
184
+ const done = serve(session, opts.input, reply).finally(unhold);
166
185
  return { done, swap };
167
186
  }
168
187
  export async function serveMcp(manifest, opts) {
package/dist/migrate.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { ambientRuntime, run } from 'bellpull';
5
- import { DROP_INS, GRADED, GRADED_VERSIONS, isLevel } from './compat.js';
5
+ import { DROP_INS, GRADED, GRADED_VERSIONS, isLevel, SUPPORTED_MAJORS } from './compat.js';
6
6
  import { ExitCode } from './exit-code.js';
7
7
  export function packageOf(specifier) {
8
8
  const parts = specifier.split('/');
@@ -707,7 +707,7 @@ async function offMajorOf(dir, dependencies) {
707
707
  if (version === undefined || graded === undefined)
708
708
  return [];
709
709
  const major = majorOf(version);
710
- return major === undefined || major === majorOf(graded) ? [] : [{ from, found: version, graded }];
710
+ return major === undefined || (SUPPORTED_MAJORS[from] ?? []).includes(major) ? [] : [{ from, found: version, graded }];
711
711
  });
712
712
  }
713
713
  function installer(dir) {
@@ -1 +1 @@
1
- {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://burgee.dev/program-schema.json","title":"burgee --schema","description":"The document `<program> --schema` prints: the program as data, for an agent or an MCP host to read without running anything. Published so a reader can validate what a program says about itself (burgee F1, D-123); burgee's own test validates `--schema` output against this file on every run.","type":"object","required":["schemaVersion","name","exitCodes","commands"],"additionalProperties":false,"properties":{"schemaVersion":{"const":1,"description":"Bumped only when a field changes meaning."},"name":{"type":"string","description":"The program as typed."},"version":{"type":"string"},"description":{"type":"string"},"exitCodes":{"type":"object","additionalProperties":{"type":"integer"},"description":"What each exit code means (F1): the contract's names and numbers, so a caller branches on the code without reading prose."},"commands":{"type":"array","description":"Every runnable, visible command, in declaration order.","items":{"$ref":"#/$defs/command"}}},"$defs":{"command":{"type":"object","required":["name","arguments","options","examples","inputSchema"],"additionalProperties":false,"properties":{"name":{"type":"string","description":"The command as typed, without the program name: `config get`."},"description":{"type":"string"},"summary":{"type":"string"},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world, or `withheld` (N6)."},"deprecated":{"description":"`true`, or the replacement's name (M5)."},"group":{"type":"string"},"lazy":{"const":true,"description":"Its handler loads on dispatch (M2)."},"plugin":{"type":"string","description":"The plugin that contributed it (M3)."},"fields":{"type":"array","items":{"type":"string"},"description":"The result's top-level fields, what `--json=` selects from (N14)."},"arguments":{"type":"array","items":{"$ref":"#/$defs/argument"}},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"relations":{"type":"array","items":{"type":"object"},"description":"Constraints between options (S2, S6): exactlyOneOf, atLeastOneOf, atMostOneOf, conflicts, implies."},"examples":{"type":"array","items":{"$ref":"#/$defs/example"}},"inputSchema":{"$ref":"#/$defs/inputSchema"}}},"argument":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string"},"required":{"type":"boolean"},"variadic":{"type":"boolean"},"default":{"type":"string"},"type":{"type":"string","pattern":"^file$","description":"`file`: `-` means standard input (S4)."}}},"option":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"required":{"type":"boolean"},"short":{"type":"string"},"default":{"description":"A string, boolean, number, or a list of strings or numbers."},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"multiple":{"type":"boolean"},"separator":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"exclusive":{"type":"array","items":{"type":"string"}},"minimum":{"type":"number"},"maximum":{"type":"number"},"integer":{"type":"boolean"},"schema":{"type":"object","description":"A Standard Schema validator, as its own library serialises it."},"placeholder":{"type":"string"},"deprecated":{"description":"`true`, or the replacement's name."},"hidden":{"type":"boolean"},"sharedFrom":{"type":"string"}}},"example":{"type":"object","required":["command"],"additionalProperties":false,"properties":{"command":{"type":"string"},"description":{"type":"string"}}},"inputSchema":{"type":"object","required":["type","properties","required","additionalProperties"],"additionalProperties":false,"description":"The arguments and options as one JSON Schema object — what an MCP tool call takes.","properties":{"type":{"const":"object"},"properties":{"type":"object"},"required":{"type":"array","items":{"type":"string"}},"additionalProperties":{"const":false}}}}}
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://burgee.dev/program-schema.json","title":"burgee --schema","description":"The document `<program> --schema` prints: the program as data, for an agent or an MCP host to read without running anything. Published so a reader can validate what a program says about itself (burgee F1, D-123); burgee's own test validates `--schema` output against this file on every run.","type":"object","required":["schemaVersion","name","exitCodes","commands"],"additionalProperties":false,"properties":{"schemaVersion":{"const":1,"description":"Bumped only when a field changes meaning."},"name":{"type":"string","description":"The program as typed."},"version":{"type":"string"},"description":{"type":"string"},"exitCodes":{"type":"object","additionalProperties":{"type":"integer"},"description":"What each exit code means (F1): the contract's names and numbers, so a caller branches on the code without reading prose."},"commands":{"type":"array","description":"Every runnable, visible command, in declaration order.","items":{"$ref":"#/$defs/command"}},"shadows":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^(--json|--mcp|completion)$"},"description":"The reserved surfaces a commander- or yargs-syntax program declares for itself, which burgee therefore withholds (J4): the program wins, and the document says so. Absent when it shadows none; a native program cannot shadow them (V5)."}},"$defs":{"command":{"type":"object","required":["name","arguments","options","examples","inputSchema"],"additionalProperties":false,"properties":{"name":{"type":"string","description":"The command as typed, without the program name: `config get`."},"description":{"type":"string"},"summary":{"type":"string"},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world, or `withheld` (N6)."},"deprecated":{"description":"`true`, or the replacement's name (M5)."},"group":{"type":"string"},"lazy":{"const":true,"description":"Its handler loads on dispatch (M2)."},"plugin":{"type":"string","description":"The plugin that contributed it (M3)."},"fields":{"type":"array","items":{"type":"string"},"description":"The result's top-level fields, what `--json=` selects from (N14)."},"arguments":{"type":"array","items":{"$ref":"#/$defs/argument"}},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"relations":{"type":"array","items":{"type":"object"},"description":"Constraints between options (S2, S6): exactlyOneOf, atLeastOneOf, atMostOneOf, conflicts, implies."},"examples":{"type":"array","items":{"$ref":"#/$defs/example"}},"inputSchema":{"$ref":"#/$defs/inputSchema"}}},"argument":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string"},"required":{"type":"boolean"},"variadic":{"type":"boolean"},"default":{"type":"string"},"type":{"type":"string","pattern":"^file$","description":"`file`: `-` means standard input (S4)."}}},"option":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"required":{"type":"boolean"},"short":{"type":"string"},"default":{"description":"A string, boolean, number, or a list of strings or numbers."},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"multiple":{"type":"boolean"},"separator":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"exclusive":{"type":"array","items":{"type":"string"}},"minimum":{"type":"number"},"maximum":{"type":"number"},"integer":{"type":"boolean"},"schema":{"type":"object","description":"A Standard Schema validator, as its own library serialises it."},"placeholder":{"type":"string"},"deprecated":{"description":"`true`, or the replacement's name."},"hidden":{"type":"boolean"},"negatable":{"type":"boolean","description":"Booleans only: whether `--no-<name>` is accepted. Absent means yes; a commander-syntax program's option says `false` where commander would refuse the negation (D-134)."},"sharedFrom":{"type":"string"}}},"example":{"type":"object","required":["command"],"additionalProperties":false,"properties":{"command":{"type":"string"},"description":{"type":"string"}}},"inputSchema":{"type":"object","required":["type","properties","required","additionalProperties"],"additionalProperties":false,"description":"The arguments and options as one JSON Schema object — what an MCP tool call takes.","properties":{"type":{"const":"object"},"properties":{"type":"object"},"required":{"type":"array","items":{"type":"string"}},"additionalProperties":{"const":false}}}}}
@@ -0,0 +1,7 @@
1
+ import { type Relation } from './manifest.js';
2
+ type Sources = Record<string, {
3
+ source: string;
4
+ }>;
5
+ /** Relations before anything else (S6, yargs #1186); `--no-x` counts as set, because it was typed (yargs #898). */
6
+ export declare function checkRelations(relations: readonly Relation[] | undefined, values: Record<string, unknown>, sources: Sources): void;
7
+ export {};
@@ -0,0 +1,49 @@
1
+ import { UsageError } from './errors.js';
2
+ import { flagsOf, kebab } from './names.js';
3
+ const isSet = (values, key, sources) => values[key] !== undefined && sources[key]?.source !== 'default';
4
+ const flagList = (keys) => flagsOf(keys).join(', ');
5
+ function exactlyOne(keys, on) {
6
+ if (on.length === 1)
7
+ return;
8
+ throw new UsageError(`exactly one of ${flagList(keys)} is required`, on.length === 0 ? 'pass one of them' : `drop all but one of ${flagList(on)}`);
9
+ }
10
+ function atLeastOne(keys, on) {
11
+ if (on.length === 0)
12
+ throw new UsageError(`at least one of ${flagList(keys)} is required`, 'pass one of them');
13
+ }
14
+ function atMostOne(keys, on) {
15
+ if (on.length > 1)
16
+ throw new UsageError(`at most one of ${flagList(keys)} may be given`, `drop all but one of ${flagList(on)}`);
17
+ }
18
+ function noConflict(on) {
19
+ if (on.length > 1)
20
+ throw new UsageError(`${flagList(on)} cannot be used together`, 'drop one of them');
21
+ }
22
+ function implied(a, b, values, sources) {
23
+ if (!isSet(values, a, sources))
24
+ return;
25
+ if (typeof b === 'string') {
26
+ if (!isSet(values, b, sources))
27
+ throw new UsageError(`--${kebab(a)} requires --${kebab(b)}`, `pass --${kebab(b)}`);
28
+ return;
29
+ }
30
+ if (!b(values))
31
+ throw new UsageError(`--${kebab(a)} is not allowed with these values`, `check the values --${kebab(a)} is declared to require`);
32
+ }
33
+ function checkRelation(rel, values, sources) {
34
+ const set = (keys) => keys.filter((k) => isSet(values, k, sources));
35
+ if ('exactlyOneOf' in rel)
36
+ exactlyOne(rel.exactlyOneOf, set(rel.exactlyOneOf));
37
+ else if ('atLeastOneOf' in rel)
38
+ atLeastOne(rel.atLeastOneOf, set(rel.atLeastOneOf));
39
+ else if ('atMostOneOf' in rel)
40
+ atMostOne(rel.atMostOneOf, set(rel.atMostOneOf));
41
+ else if ('conflicts' in rel)
42
+ noConflict(set(rel.conflicts));
43
+ else
44
+ implied(rel.implies[0], rel.implies[1], values, sources);
45
+ }
46
+ export function checkRelations(relations, values, sources) {
47
+ for (const rel of relations ?? [])
48
+ checkRelation(rel, values, sources);
49
+ }
package/dist/schema.d.ts CHANGED
@@ -107,6 +107,13 @@ export interface ProgramSchema {
107
107
  */
108
108
  exitCodes: Readonly<Record<string, number>>;
109
109
  commands: CommandSchema[];
110
+ /**
111
+ * J4 — the reserved surfaces a façade program declares for itself, which burgee therefore
112
+ * withholds: the program wins, and this says so rather than leaving a caller to find the
113
+ * surface missing. Absent when it shadows none. A native program cannot shadow them —
114
+ * `defineProgram` refuses the names (V5) — so only the commander and yargs façades set it.
115
+ */
116
+ shadows?: ('--json' | '--mcp' | 'completion')[];
110
117
  }
111
118
  export declare function inputSchemaOf(node: CommandNode): JsonSchema;
112
119
  /** The typed name of a node: its path without the program's own name. */
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Everything the engine answers without running a command — help, `--version`, `help
3
+ * [command…]`, `completion <shell>`, `__complete`, `config explain`, `--schema` and `--mcp` —
4
+ * kept off the startup path (U5).
5
+ *
6
+ * `execute.ts` imports this module only when argv asks for one of them, or when argv resolves
7
+ * no runnable command. A run that dispatches a handler, which is every run a program exists
8
+ * for, never loads a byte of it. Each surface was already asynchronous, and each already loaded
9
+ * its own body (`help.js`, `schema.js`, `completions.js`, `mcp.js` …) behind an `await
10
+ * import()`; what moved here is the routing and the glue that used to sit in the entry chunk.
11
+ */
12
+ import { type Resolution } from 'seniority/precedence';
13
+ import { type ExitCode as ExitCodeType } from './exit-code.js';
14
+ import { type CommandNode, type Manifest, type OptionSpec } from './manifest.js';
15
+ import { type Package } from './pkg.js';
16
+ /** The slice of the engine's `Io` a surface reads. */
17
+ export interface SurfaceIo {
18
+ out: {
19
+ write: (s: string) => unknown;
20
+ };
21
+ env: Record<string, string | undefined>;
22
+ width: number;
23
+ stdin: NodeJS.ReadableStream;
24
+ /** The package.json owning the entry file, read once (V4). */
25
+ pkg: Package | undefined;
26
+ tty: boolean;
27
+ }
28
+ /** What a surface needs back from the engine: precedence for `config explain`, a run for `--mcp`. */
29
+ export interface Engine {
30
+ resolution: (specs: Record<string, OptionSpec>, flags: Record<string, unknown>) => Promise<Resolution>;
31
+ execute: (manifest: Manifest, opts: {
32
+ argv: string[];
33
+ env: Record<string, string | undefined>;
34
+ stdout: {
35
+ write: (s: string) => unknown;
36
+ };
37
+ stderr: {
38
+ write: (s: string) => unknown;
39
+ };
40
+ exit: (code: number) => void;
41
+ }) => Promise<void>;
42
+ }
43
+ /** What to do when argv resolves to no runnable command: help, or a usage error naming it. */
44
+ interface Resolving {
45
+ manifest: Manifest;
46
+ root: string[];
47
+ io: SurfaceIo;
48
+ }
49
+ /**
50
+ * `--help` on a command that resolved: prose, or — with `--json` beside it — the help document
51
+ * (F2). It printed the same prose as `--help` with both flags, so a caller who asked for a
52
+ * machine-readable answer got one they had to parse: the exact failure the `--json` surface
53
+ * exists to avoid, on the flag people type first.
54
+ */
55
+ export declare function helpFor(manifest: Manifest, node: CommandNode, io: SurfaceIo, json: boolean): Promise<string>;
56
+ /** `--version`: the declared version, else the owning package.json's (V4). */
57
+ export declare function versionOf(manifest: Manifest, io: Pick<SurfaceIo, 'pkg'>): string;
58
+ /**
59
+ * What to do when argv resolves to no runnable command: help, the version, or a usage error
60
+ * naming it.
61
+ *
62
+ * `--version` is the same courtesy `--help` gets, for the flag people type first. A program
63
+ * that is a pure command group resolves nothing for `burgee --version`, so it fell through
64
+ * to `unknown command "--version"` and **exit 2** — which under E1 means *rewrite the
65
+ * command*, so an agent asked for the version would rewrite it until it gave up. Real
66
+ * commander and real yargs both print the version and exit 0 for the identical program.
67
+ *
68
+ * `-V` is commander's spelling, and `commander-command.ts` already defaults to
69
+ * `-V, --version`. Like `HELP_FLAGS` above, this does not check whether the root declares
70
+ * an option of the same name: a root that is not runnable has no path that would answer it.
71
+ */
72
+ export declare function unresolved({ manifest, root, io }: Resolving, argv: string[], at: CommandNode | undefined): Promise<{
73
+ text: string;
74
+ code: ExitCodeType;
75
+ }>;
76
+ /**
77
+ * `--schema` (F1, N8) and `--mcp` (N1) are served for every program from the manifest
78
+ * alone, before any command resolves: no config, no network, no handler runs.
79
+ */
80
+ export declare function serve(manifest: Manifest, argv: string[], io: SurfaceIo, engine: Engine): Promise<boolean>;
81
+ export {};
@@ -0,0 +1,116 @@
1
+ import { ConfigError } from 'seniority/precedence';
2
+ import { detectAgent } from './agent.js';
3
+ import { beforeTerminator, isJsonFlag } from './argv.js';
4
+ import { UsageError } from './errors.js';
5
+ import { ExitCode } from './exit-code.js';
6
+ const HELP_FLAGS = new Set(['--help', '-h']);
7
+ async function helpDocumentOf(manifest, node) {
8
+ const { commandSchemaOf, typedName } = await import('./schema.js');
9
+ const root = manifest.rootPath;
10
+ const children = manifest.commands
11
+ .filter((c) => c.path.length === node.path.length + 1 && c.path.slice(0, node.path.length).join(' ') === node.path.join(' '))
12
+ .map((c) => typedName(c, root));
13
+ return {
14
+ schemaVersion: 1,
15
+ ...commandSchemaOf(node, root),
16
+ ...(children.length === 0 ? {} : { commands: children }),
17
+ };
18
+ }
19
+ function rootNode(manifest, root) {
20
+ return manifest.find(root) ?? { path: root, options: {} };
21
+ }
22
+ const renderHelp = async (manifest, node, io) => {
23
+ const help = await import('./help.js');
24
+ return help.renderHelp(manifest, node, { width: io.width, color: help.colorFor(io.env, detectAgent(io.env, io.tty).interactive) });
25
+ };
26
+ const machineJson = async (...args) => (await import('./schema.js')).machineJson(...args);
27
+ export async function helpFor(manifest, node, io, json) {
28
+ if (json)
29
+ return `${await machineJson(await helpDocumentOf(manifest, node), ['--json'])}\n`;
30
+ return await renderHelp(manifest, node, io);
31
+ }
32
+ export function versionOf(manifest, io) {
33
+ const declared = manifest.version ?? (typeof io.pkg?.data['version'] === 'string' ? io.pkg.data['version'] : undefined);
34
+ if (declared === undefined)
35
+ throw new ConfigError('no version declared', 'pass version to defineProgram, or set "version" in the owning package.json');
36
+ return declared;
37
+ }
38
+ export async function unresolved({ manifest, root, io }, argv, at) {
39
+ const node = at ?? rootNode(manifest, root);
40
+ const typed = argv.slice(node.path.length - root.length);
41
+ const first = typed[0] ?? '';
42
+ if (typed.length > 0 && HELP_FLAGS.has(first)) {
43
+ if (beforeTerminator(typed).some(isJsonFlag))
44
+ return { text: `${await machineJson(await helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
45
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.OK };
46
+ }
47
+ if (first === '--version' || first === '-V')
48
+ return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
49
+ if (typed.length === 0)
50
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.USAGE };
51
+ throw new UsageError(`unknown command "${typed[0] ?? ''}"`, 'run --help to see the available commands');
52
+ }
53
+ async function completion(manifest, argv, io) {
54
+ if (argv[0] !== 'completion' || manifest.find([...manifest.rootPath, 'completion']) !== undefined)
55
+ return false;
56
+ const { renderCompletion, renderFigSpec, SHELLS } = await import('./completions.js');
57
+ const shell = argv[1] ?? '';
58
+ if (shell === 'fig') {
59
+ io.out.write(`${JSON.stringify(renderFigSpec(manifest), null, 2)}\n`);
60
+ return true;
61
+ }
62
+ const known = SHELLS.find((s) => s === shell);
63
+ if (known === undefined)
64
+ throw new UsageError(`unknown shell "${shell}"`, `completion ${SHELLS.join('|')}|fig`);
65
+ io.out.write(renderCompletion(manifest, known));
66
+ return true;
67
+ }
68
+ async function helpCommand(manifest, argv, root, io) {
69
+ const { node } = manifest.resolve(argv, root);
70
+ return renderHelp(manifest, node ?? rootNode(manifest, root), io);
71
+ }
72
+ export async function serve(manifest, argv, io, engine) {
73
+ const head = beforeTerminator(argv);
74
+ if (argv[0] === '__complete') {
75
+ await (await import('./complete-dynamic.js')).completeDynamic(manifest, argv.slice(1), (t) => io.out.write(t));
76
+ return true;
77
+ }
78
+ const root = manifest.rootPath;
79
+ if (head[0] === 'config' && head[1] === 'explain' && manifest.config !== undefined && manifest.find([...root, 'config', 'explain']) === undefined) {
80
+ const { explainConfig } = await import('./config-explain.js');
81
+ io.out.write(await explainConfig(manifest, head.slice(2), engine.resolution));
82
+ return true;
83
+ }
84
+ if (await completion(manifest, argv, io))
85
+ return true;
86
+ if (argv[0] === 'help') {
87
+ io.out.write(await helpCommand(manifest, argv.slice(1), manifest.rootPath, io));
88
+ return true;
89
+ }
90
+ if (head.includes('--schema')) {
91
+ const { schemaSurface } = await import('./schema-surface.js');
92
+ io.out.write(`${await machineJson(await schemaSurface(manifest, argv), head)}\n`);
93
+ return true;
94
+ }
95
+ if (head[0] === '--mcp') {
96
+ const invoke = async (args) => {
97
+ const out = [];
98
+ const err = [];
99
+ let code = 0;
100
+ await engine.execute(manifest, {
101
+ argv: args,
102
+ env: io.env,
103
+ stdout: { write: (s) => out.push(s) },
104
+ stderr: { write: (s) => err.push(s) },
105
+ exit: (c) => {
106
+ code = c;
107
+ },
108
+ });
109
+ return { stdout: out.join(''), stderr: err.join(''), code };
110
+ };
111
+ const { serveMcp } = await import('./mcp.js');
112
+ await serveMcp(manifest, { input: io.stdin, output: io.out, invoke });
113
+ return true;
114
+ }
115
+ return false;
116
+ }
@@ -1,9 +1,4 @@
1
- import { type OptionSpec, type Relation } from './manifest.js';
2
- type Sources = Record<string, {
3
- source: string;
4
- }>;
5
- /** Relations before anything else (S6, yargs #1186); `--no-x` counts as set, because it was typed (yargs #898). */
6
- export declare function checkRelations(relations: readonly Relation[] | undefined, values: Record<string, unknown>, sources: Sources): void;
1
+ import { type OptionSpec } from './manifest.js';
7
2
  export declare function toNumber(key: string, spec: OptionSpec, raw: unknown): number;
8
3
  /** Split a repeatable option's raw values on its separator; arrays from config pass through (S8, yargs #846). */
9
4
  export declare function splitMultiple(spec: OptionSpec, raw: unknown): unknown[];
package/dist/validate.js CHANGED
@@ -1,52 +1,5 @@
1
1
  import { UsageError } from './errors.js';
2
- import { flagsOf, kebab } from './names.js';
3
- const isSet = (values, key, sources) => values[key] !== undefined && sources[key]?.source !== 'default';
4
- const flagList = (keys) => flagsOf(keys).join(', ');
5
- function exactlyOne(keys, on) {
6
- if (on.length === 1)
7
- return;
8
- throw new UsageError(`exactly one of ${flagList(keys)} is required`, on.length === 0 ? 'pass one of them' : `drop all but one of ${flagList(on)}`);
9
- }
10
- function atLeastOne(keys, on) {
11
- if (on.length === 0)
12
- throw new UsageError(`at least one of ${flagList(keys)} is required`, 'pass one of them');
13
- }
14
- function atMostOne(keys, on) {
15
- if (on.length > 1)
16
- throw new UsageError(`at most one of ${flagList(keys)} may be given`, `drop all but one of ${flagList(on)}`);
17
- }
18
- function noConflict(on) {
19
- if (on.length > 1)
20
- throw new UsageError(`${flagList(on)} cannot be used together`, 'drop one of them');
21
- }
22
- function implied(a, b, values, sources) {
23
- if (!isSet(values, a, sources))
24
- return;
25
- if (typeof b === 'string') {
26
- if (!isSet(values, b, sources))
27
- throw new UsageError(`--${kebab(a)} requires --${kebab(b)}`, `pass --${kebab(b)}`);
28
- return;
29
- }
30
- if (!b(values))
31
- throw new UsageError(`--${kebab(a)} is not allowed with these values`, `check the values --${kebab(a)} is declared to require`);
32
- }
33
- function checkRelation(rel, values, sources) {
34
- const set = (keys) => keys.filter((k) => isSet(values, k, sources));
35
- if ('exactlyOneOf' in rel)
36
- exactlyOne(rel.exactlyOneOf, set(rel.exactlyOneOf));
37
- else if ('atLeastOneOf' in rel)
38
- atLeastOne(rel.atLeastOneOf, set(rel.atLeastOneOf));
39
- else if ('atMostOneOf' in rel)
40
- atMostOne(rel.atMostOneOf, set(rel.atMostOneOf));
41
- else if ('conflicts' in rel)
42
- noConflict(set(rel.conflicts));
43
- else
44
- implied(rel.implies[0], rel.implies[1], values, sources);
45
- }
46
- export function checkRelations(relations, values, sources) {
47
- for (const rel of relations ?? [])
48
- checkRelation(rel, values, sources);
49
- }
2
+ import { kebab } from './names.js';
50
3
  const isFinite = (n) => Number.isFinite(n);
51
4
  const numberOf = (raw) => {
52
5
  if (typeof raw === 'number')
@@ -61,6 +61,13 @@ export interface BurgeeSeam {
61
61
  stdout?: Writer;
62
62
  stderr?: Writer;
63
63
  exit?: (code: number) => void;
64
+ /**
65
+ * The behavioural floor (J3, D-121), off by default because yargs' own suite asserts the old
66
+ * behaviour: a usage failure exits 2 (E1) rather than 1, and a handler that throws or rejects
67
+ * prints one line and exits with its E1 code, never the help screen and a stack. On its own
68
+ * it injects nothing — the program's output stays yargs' own.
69
+ */
70
+ floor?: boolean;
64
71
  }
65
72
  export declare function YargsFactory(shim: PlatformShim): (processArgs?: string | string[], cwd?: string, parentRequire?: NodeJS.Require) => YargsInstance;
66
73
  export declare class YargsInstance {
@@ -187,7 +194,7 @@ export declare class YargsInstance {
187
194
  * instead of the console, and `exit` receives an E1 code: OK for help and version, USAGE
188
195
  * for a validation failure, RUNTIME for a handler that threw.
189
196
  */
190
- burgee(seam: BurgeeSeam): this;
197
+ burgee({ floor, ...seam }: BurgeeSeam): this;
191
198
  }
192
199
  export declare function isYargsInstance(y: any): y is YargsInstance;
193
200
  export {};