burgee 0.11.0 → 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.
Files changed (52) hide show
  1. package/README.md +19 -4
  2. package/dist/cli.d.ts +2 -90
  3. package/dist/cli.js +4 -151
  4. package/dist/commander/command.d.ts +33 -1
  5. package/dist/commander/command.js +49 -11
  6. package/dist/compat.d.ts +39 -2
  7. package/dist/compat.js +90 -2
  8. package/dist/complete-dynamic.d.ts +18 -0
  9. package/dist/complete-dynamic.js +19 -0
  10. package/dist/completions.js +33 -15
  11. package/dist/config-explain.d.ts +22 -0
  12. package/dist/config-explain.js +33 -0
  13. package/dist/define-error.d.ts +29 -0
  14. package/dist/define-error.js +29 -0
  15. package/dist/errors.d.ts +29 -0
  16. package/dist/errors.js +17 -0
  17. package/dist/execute.d.ts +6 -0
  18. package/dist/execute.js +74 -16
  19. package/dist/facade-failure.d.ts +17 -0
  20. package/dist/facade-failure.js +12 -0
  21. package/dist/fields.d.ts +32 -0
  22. package/dist/fields.js +45 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/manifest.d.ts +42 -5
  26. package/dist/manifest.js +6 -0
  27. package/dist/mcp.d.ts +5 -1
  28. package/dist/mcp.js +77 -11
  29. package/dist/migrate.d.ts +57 -7
  30. package/dist/migrate.js +413 -34
  31. package/dist/parse-hooks.d.ts +7 -0
  32. package/dist/parse-hooks.js +17 -0
  33. package/dist/plugin.d.ts +2 -6
  34. package/dist/plugin.js +1 -1
  35. package/dist/program-schema.json +1 -0
  36. package/dist/program.d.ts +90 -0
  37. package/dist/program.js +151 -0
  38. package/dist/runtime.d.ts +3 -1
  39. package/dist/runtime.js +3 -0
  40. package/dist/schema.d.ts +7 -0
  41. package/dist/schema.js +6 -11
  42. package/dist/schema.json +1 -1
  43. package/dist/stdin-dash.d.ts +15 -0
  44. package/dist/stdin-dash.js +12 -0
  45. package/dist/validate.d.ts +1 -27
  46. package/dist/validate.js +2 -17
  47. package/dist/yargs/factory.js +39 -13
  48. package/dist/yargs-parser.d.ts +8 -1
  49. package/dist/yargs-parser.js +1 -1
  50. package/dist/yargs.d.ts +1 -0
  51. package/dist/yargs.js +1 -0
  52. package/package.json +7 -6
package/README.md CHANGED
@@ -46,6 +46,7 @@ run(defineCommand({
46
46
  name: 'greet',
47
47
  description: 'Greet someone by name',
48
48
  options: { name: { type: 'string', required: true, description: 'who to greet' } },
49
+ effects: 'read_only', // what running it does to the world; required, and what --mcp reads
49
50
  run: ({ options }) => ({ greeting: `hello, ${options.name}` }),
50
51
  }));
51
52
  ```
@@ -55,7 +56,7 @@ $ node cli.mjs --name ada
55
56
  greeting: hello, ada
56
57
 
57
58
  $ node cli.mjs --json --name ada
58
- {"ok":true,"data":{"greeting":"hello, ada"}}
59
+ {"ok":true,"data":{"greeting":"hello, ada"},"meta":{"provenance":{"name":{"source":"flag","location":"--name"}}}}
59
60
 
60
61
  $ node cli.mjs # exit 2
61
62
  error: missing required option --name
@@ -94,6 +95,18 @@ published and ratcheting:
94
95
  Your code and your tests are unchanged. A façade is never called "compatible" until its
95
96
  host's own suite passes 100%; below that the rate is published instead of claimed.
96
97
 
98
+ Or let the codemod make that change, and the same one for chalk, ora, string-width,
99
+ cross-spawn, signal-exit and every other incumbent the family replaces at full grade:
100
+
101
+ ```bash
102
+ npx burgee migrate --dry-run
103
+ npx burgee migrate
104
+ ```
105
+
106
+ It rewrites import specifiers and nothing else, leaves a replacement that is not level yet
107
+ alone with its grade, refuses a file it cannot rewrite whole, and prints the install command
108
+ to run next — [Migrate](https://burgee.interlace.tools/docs/migrate).
109
+
97
110
  ## What is in the box
98
111
 
99
112
  | Import | Gives you |
@@ -154,9 +167,11 @@ the declaration read by a different reader.
154
167
 
155
168
  ### How do I expose a CLI over MCP?
156
169
 
157
- Run it with `--mcp`: the same manifest is served as MCP tools over stdio. A command becomes
158
- a tool only when it declares its `effects` (`read_only`, `idempotent` or `non_idempotent`),
159
- so nothing reaches an agent by accident. Register it with any stdio client:
170
+ Run it with `--mcp`: the same manifest is served as MCP tools over stdio. Every runnable
171
+ command declares its `effects` — `read_only`, `idempotent` or `non_idempotent`, which become
172
+ MCP's hints, or `withheld`, which keeps it out of the tool list — so nothing reaches an
173
+ agent by accident. On `burgee/commander` and `burgee/yargs`, a command that declared
174
+ nothing is still listed, marked `effects: 'undeclared'`. Register it with any stdio client:
160
175
 
161
176
  ```json
162
177
  { "mcpServers": { "mytool": { "command": "npx", "args": ["mytool", "--mcp"] } } }
package/dist/cli.d.ts CHANGED
@@ -1,90 +1,2 @@
1
- export declare const brandCommand: import("./execute.js").Command<{
2
- readonly lead: {
3
- readonly type: "string";
4
- readonly required: true;
5
- readonly description: "leading colour, hex. Your primary; it leads the charge upper-left";
6
- };
7
- readonly follow: {
8
- readonly type: "string";
9
- readonly required: true;
10
- readonly description: "following colour, hex. Your secondary; it follows lower-right";
11
- };
12
- readonly name: {
13
- readonly type: "string";
14
- readonly description: "brand name, used as the accessible label and card title";
15
- };
16
- readonly ground: {
17
- readonly type: "string";
18
- readonly default: "#0a0a0a";
19
- readonly description: "the field’s dark midpoint, which is what keeps the charge legible";
20
- };
21
- readonly charge: {
22
- readonly type: "string";
23
- readonly description: "path to an SVG whose contents replace the bars, drawn in a 0 0 100 100 box";
24
- };
25
- readonly bordure: {
26
- readonly type: "string";
27
- readonly description: "outline colour, hex. Omit for no outline";
28
- };
29
- readonly bordureWidth: {
30
- readonly type: "string";
31
- readonly default: "1.5";
32
- readonly description: "outline width";
33
- };
34
- readonly tagline: {
35
- readonly type: "string";
36
- readonly description: "one line under the name on the card and cover";
37
- };
38
- readonly out: {
39
- readonly type: "string";
40
- readonly description: "directory to write into. Omit to print the flag only";
41
- };
42
- readonly on: {
43
- readonly type: "string";
44
- readonly description: "page colour(s) the flag will fly on, comma separated. Checked for contrast";
45
- };
46
- readonly allowLowContrast: {
47
- readonly type: "boolean";
48
- readonly description: "emit anyway when a contrast check fails. Says so in the output";
49
- };
50
- }>;
51
- /**
52
- * `burgee dev <entry>` — watch the entry, reload it on change, serve it as MCP on stdio
53
- * and print every surface on each save (dev-loop). Loaded only when asked for, so the
54
- * CLI's own weight and the framework's stay what they were (W4).
55
- */
56
- export declare const devCommand: import("./execute.js").Command<{
57
- readonly noWatch: {
58
- readonly type: "boolean";
59
- readonly description: "load once and serve; do not watch for changes";
60
- };
61
- }>;
62
- /**
63
- * `burgee migrate [dir]` — rewrite a commander or yargs project's imports to burgee's
64
- * drop-in front-ends and report what changed, with the numbers that say why it was safe.
65
- *
66
- * The engine is loaded on this path only (K6), the same way `dev` is: a dynamic import, so
67
- * `burgee`'s own start-up and the framework's weight are what they were. `weight.test.ts`
68
- * denies `migrate.js` to the root entry by name, so that cannot drift back.
69
- *
70
- * `idempotent` is the honest answer rather than the conservative one, and it is earned:
71
- * `burgee/commander` is not a key in the mapping, so a second run over a migrated tree
72
- * rewrites nothing. N6 then requires the result to report `changed`, which it does.
73
- */
74
- export declare const migrateCommand: import("./execute.js").Command<{
75
- readonly dryRun: {
76
- readonly type: "boolean";
77
- readonly description: "scan and report; write nothing";
78
- };
79
- readonly force: {
80
- readonly type: "boolean";
81
- readonly description: "migrate even though the git tree has uncommitted changes";
82
- };
83
- }>;
84
- /**
85
- * `burgee check <plugin-file>` — the feedback loop PRINCIPLES 7 asks every extension surface
86
- * for, and the one burgee did not have. See `check.ts`: this one returns its report as data, so
87
- * `--json` is the form an agent that just wrote a plugin reads.
88
- */
89
- export declare const pluginCheckCommand: import("./execute.js").Command<import("./execute.js").OptionSpecs>;
90
- export declare const program: import("./manifest.js").Manifest;
1
+ #!/usr/bin/env node
2
+ export * from './program.js';
package/dist/cli.js CHANGED
@@ -1,152 +1,5 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { DEFAULT_GROUND, defineBurgee, opposedField } from './brand.js';
4
- import { auditBurgee, report } from './contrast.js';
5
- import { defineCommand, defineProgram, run } from './execute.js';
6
- const MASTER = 512;
7
- const CONTRAST_HINT = 'hint: darken the --ground stop under the charge, or pass --allow-low-contrast';
8
- const DEFAULT_BORDURE_WIDTH = '1.5';
9
- function surfaces(brand, tagline) {
10
- const burgee = defineBurgee(brand);
11
- const subtitle = tagline === '' ? {} : { subtitle: tagline };
12
- return [
13
- { file: 'flag.svg', svg: burgee.flag(MASTER) },
14
- { file: 'icon.svg', svg: burgee.favicon() },
15
- { file: 'og.svg', svg: burgee.og(subtitle) },
16
- { file: 'cover.svg', svg: burgee.cover(subtitle) },
17
- { file: 'lockup.svg', svg: burgee.lockup({ theme: 'dark' }) },
18
- { file: 'lockup-light.svg', svg: burgee.lockup({ theme: 'light' }) },
19
- ];
20
- }
21
- export const brandCommand = defineCommand({
22
- name: 'brand',
23
- description: 'Generate a burgee — flag, favicon, social card, cover and lockup — from two colours',
24
- effects: 'non_idempotent',
25
- options: {
26
- lead: {
27
- type: 'string',
28
- required: true,
29
- description: 'leading colour, hex. Your primary; it leads the charge upper-left',
30
- },
31
- follow: {
32
- type: 'string',
33
- required: true,
34
- description: 'following colour, hex. Your secondary; it follows lower-right',
35
- },
36
- name: { type: 'string', description: 'brand name, used as the accessible label and card title' },
37
- ground: {
38
- type: 'string',
39
- default: DEFAULT_GROUND,
40
- description: 'the field’s dark midpoint, which is what keeps the charge legible',
41
- },
42
- charge: {
43
- type: 'string',
44
- description: 'path to an SVG whose contents replace the bars, drawn in a 0 0 100 100 box',
45
- },
46
- bordure: { type: 'string', description: 'outline colour, hex. Omit for no outline' },
47
- bordureWidth: { type: 'string', default: DEFAULT_BORDURE_WIDTH, description: 'outline width' },
48
- tagline: { type: 'string', description: 'one line under the name on the card and cover' },
49
- out: { type: 'string', description: 'directory to write into. Omit to print the flag only' },
50
- 'on': {
51
- type: 'string',
52
- description: 'page colour(s) the flag will fly on, comma separated. Checked for contrast',
53
- },
54
- allowLowContrast: {
55
- type: 'boolean',
56
- description: 'emit anyway when a contrast check fails. Says so in the output',
57
- },
58
- },
59
- run: ({ options }) => {
60
- const lead = options.lead ?? '';
61
- const follow = options.follow ?? '';
62
- const colors = { lead, follow };
63
- const charge = options.charge === undefined
64
- ? {}
65
- : { charge: readFileSync(options.charge, 'utf8').replace(/<\/?svg[^>]*>/g, '').trim() };
66
- const bordure = options.bordure === undefined
67
- ? {}
68
- : {
69
- bordure: {
70
- color: options.bordure,
71
- width: Number(options.bordureWidth ?? DEFAULT_BORDURE_WIDTH),
72
- },
73
- };
74
- const brand = {
75
- ...(options.name === undefined ? {} : { name: options.name }),
76
- mark: colors,
77
- field: opposedField(colors, options.ground ?? DEFAULT_GROUND),
78
- ...charge,
79
- ...bordure,
80
- };
81
- const grounds = (options.on ?? '')
82
- .split(',')
83
- .map((g) => g.trim())
84
- .filter((g) => g !== '');
85
- const findings = auditBurgee(brand, grounds);
86
- const failed = findings.filter((f) => !f.passes);
87
- if (failed.length > 0 && options.allowLowContrast !== true) {
88
- throw new Error(`contrast below WCAG AA:\n${report(failed)}\n${CONTRAST_HINT}`);
89
- }
90
- const written = surfaces(brand, options.tagline ?? '');
91
- const contrast = findings.map((f) => ({ what: f.what, ratio: f.ratio, passes: f.passes }));
92
- if (options.out === undefined) {
93
- return { flag: defineBurgee(brand).flag(MASTER), files: [], contrast };
94
- }
95
- mkdirSync(options.out, { recursive: true });
96
- for (const s of written)
97
- writeFileSync(join(options.out, s.file), `${s.svg}\n`);
98
- return { out: options.out, files: written.map((s) => s.file), contrast };
99
- },
100
- });
101
- export const devCommand = defineCommand({
102
- name: 'dev',
103
- description: 'Watch a CLI entry, reload it on change, and serve it as MCP on stdio while you write it',
104
- arguments: [{ name: 'entry', description: 'the module that exports the program, as program or as its default export', required: true }],
105
- options: {
106
- noWatch: { type: 'boolean', description: 'load once and serve; do not watch for changes' },
107
- },
108
- effects: 'withheld',
109
- run: async ({ positionals, options }) => {
110
- const [entry] = positionals;
111
- if (entry === undefined)
112
- throw new Error('an entry file is required');
113
- const [{ dev }, { processRuntime }] = await Promise.all([import('./dev.js'), import('./runtime.js')]);
114
- const handle = dev({ entry, input: processRuntime.stdin, output: processRuntime.stdout, log: processRuntime.stderr, watch: options.noWatch !== true });
115
- await handle.done;
116
- },
117
- });
118
- export const migrateCommand = defineCommand({
119
- name: 'migrate',
120
- description: 'Rewrite commander and yargs imports to burgee’s drop-in front-ends, and report what changed',
121
- arguments: [{ name: 'dir', description: 'the project to migrate. Defaults to the current directory', required: false }],
122
- options: {
123
- dryRun: { type: 'boolean', description: 'scan and report; write nothing' },
124
- force: { type: 'boolean', description: 'migrate even though the git tree has uncommitted changes' },
125
- },
126
- effects: 'idempotent',
127
- examples: [
128
- { command: 'burgee migrate --dry-run', description: 'what it would change, without changing it' },
129
- { command: 'burgee migrate --json', description: 'the same report as data; exit 1 when anything was refused' },
130
- ],
131
- run: async ({ positionals, options }) => {
132
- const [{ migrate }, { host }] = await Promise.all([import('./migrate.js'), import('./runtime.js')]);
133
- return await migrate({ dir: positionals[0] ?? host.cwd(), dryRun: options.dryRun === true, force: options.force === true });
134
- },
135
- });
136
- export const pluginCheckCommand = defineCommand({
137
- name: 'check',
138
- description: 'Validate a burgee plugin, register it into a throwaway program, and report what it contributes',
139
- arguments: [{ name: 'file', description: 'the plugin module to check' }],
140
- effects: 'read_only',
141
- examples: [
142
- { command: 'burgee check ./my-plugin.mjs', description: 'what the plugin contributes, or why it was refused' },
143
- { command: 'burgee check ./my-plugin.mjs --json', description: 'the same, as data; exit 1 on a refusal' },
144
- ],
145
- run: async ({ positionals }) => (await import('./check.js')).checkPlugin(positionals[0] ?? ''),
146
- });
147
- export const program = defineProgram({
148
- name: 'burgee',
149
- description: 'The agent-native CLI framework, and the tools that come with it',
150
- commands: [brandCommand, devCommand, migrateCommand, pluginCheckCommand],
151
- });
1
+ #!/usr/bin/env node
2
+ import { run } from './execute.js';
3
+ import { program } from './program.js';
152
4
  run(program);
5
+ export * from './program.js';
@@ -14,6 +14,7 @@ import { EventEmitter } from 'node:events';
14
14
  * then reports through the E1 taxonomy — the harness's seam (T1).
15
15
  */
16
16
  import * as crossSpawn from 'bellpull/cross-spawn';
17
+ import { ExitCode } from '../exit-code.js';
17
18
  import { type DeclaredEffects, Manifest, type OptionSpec, type Plugin } from '../manifest.js';
18
19
  import { Argument, type ParseArg } from './argument.js';
19
20
  import { CommanderError } from './error.js';
@@ -261,6 +262,14 @@ export declare class Command extends EventEmitter {
261
262
  error(message: string, errorOptions?: ErrorOptions): never;
262
263
  /** One failure as the envelope (E3, G5); `fix` only when one candidate was named. */
263
264
  _reportJson(code: string, message: string): void;
265
+ /** The failure envelope on stdout, once per run however many paths see the same failure (G5). */
266
+ _writeFailure(error: Record<string, unknown>): void;
267
+ /**
268
+ * A handler's failure — a throw, a rejection, a thrown non-Error — reported on the surface the
269
+ * run asked for, with the E1 code its error names: `AuthError` is AUTH, `UsageError` USAGE,
270
+ * anything else RUNTIME (E6, D-140). Returns that code.
271
+ */
272
+ _reportHandlerFailure(err: unknown): ExitCode;
264
273
  /** Apply environment variables to options that have no value from the cli or client code. */
265
274
  _parseOptionsEnv(): void;
266
275
  /** Apply implied option values where the option is undefined or at its default. */
@@ -329,7 +338,17 @@ export declare class Command extends EventEmitter {
329
338
  */
330
339
  get manifest(): Manifest;
331
340
  _project(manifest: Manifest): void;
332
- /** Options as the manifest describes them, on a null-prototype record. */
341
+ /**
342
+ * Options as the manifest describes them, on a null-prototype record — keyed so that the
343
+ * spelling every surface derives from a key, `--${kebab(key)}`, is a flag commander accepts.
344
+ *
345
+ * Commander negates only what it was told to, where burgee's own parser negates every
346
+ * boolean. So a boolean is `negatable` here only when the program declared its `--no-` twin,
347
+ * which folds onto it rather than appearing twice; and a `--no-x` declared alone, which has
348
+ * no `--x`, is described as the switch it is, under `noX`. Before this, `--no-color` was
349
+ * published as `color` with `flag: '--color'`: completions offered `--color` and
350
+ * `--no-skip-blank`, and `--mcp` sent `--color` — each refused by the parser behind them.
351
+ */
333
352
  _optionSpecs(): Record<string, OptionSpec>;
334
353
  /** burgee: declare what the command does to the world (N6). This is what exposes it as an MCP tool (N2). */
335
354
  effects(value: DeclaredEffects): this;
@@ -352,6 +371,19 @@ export declare class Command extends EventEmitter {
352
371
  _prepareBurgee(parseOptions?: BurgeeParseOptions): ParseOptions | undefined;
353
372
  /** In burgee mode the whole run settles to one E1 exit; otherwise commander's behaviour, untouched. */
354
373
  _runBurgee(run: () => unknown): unknown;
374
+ /**
375
+ * Nothing injected — `program.parseAsync(process.argv)`, the way every commander program is
376
+ * run. Commander's own contract, with one exception: a handler that fails under `--json`
377
+ * settles to the envelope on stdout and the E1 code its error names, set as the process's
378
+ * exit code, instead of escaping `parse`/`parseAsync` for Node to print as a stack (D-140).
379
+ *
380
+ * The seam check in `_runBurgee` cannot see this case, and that was the defect: it runs
381
+ * before argv is parsed, and `--json` is only recognised *during* the parse, so the run was
382
+ * already committed to "no burgee" when the handler threw. A `CommanderError` is left alone —
383
+ * `error()` has reported it already, and an `exitOverride` caller is owed the throw — and so
384
+ * is every failure without `--json`, which is commander's to surface as it always has.
385
+ */
386
+ _runCommanderWay(run: () => unknown): unknown;
355
387
  /** Any command from here down declares this flag, so burgee does not serve it: additive only. */
356
388
  _declares(flag: string): boolean;
357
389
  /** The action, wrapped in the plugin hooks and followed by the envelope or the rendering. */
@@ -4,7 +4,9 @@ import path from 'node:path';
4
4
  import { stripVTControlCharacters } from 'node:util';
5
5
  import * as crossSpawn from 'bellpull/cross-spawn';
6
6
  import { ExitCode } from '../exit-code.js';
7
+ import { handlerFailure } from '../facade-failure.js';
7
8
  import { Manifest } from '../manifest.js';
9
+ import { camel } from '../names.js';
8
10
  import { host } from '../runtime.js';
9
11
  import { machineJson, schemaOf } from '../schema.js';
10
12
  import { suggestSimilar } from '../suggest.js';
@@ -1034,13 +1036,24 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
1034
1036
  this._exit(exitCode, code, message);
1035
1037
  }
1036
1038
  _reportJson(code, message) {
1039
+ const [first = '', ...rest] = message.replace(/^error: /, '').split('\n');
1040
+ const guess = /\(Did you mean (\S+)\?\)/.exec(rest.join(''));
1041
+ this._writeFailure({ code, message: first, ...(guess === null ? {} : { fix: guess[1] }) });
1042
+ }
1043
+ _writeFailure(error) {
1037
1044
  const burgee = this._root()._burgee;
1038
1045
  if (burgee === undefined || !burgee.json || burgee.reported)
1039
1046
  return;
1040
1047
  burgee.reported = true;
1041
- const [first = '', ...rest] = message.replace(/^error: /, '').split('\n');
1042
- const guess = /\(Did you mean (\S+)\?\)/.exec(rest.join(''));
1043
- this._outputConfiguration.writeOut(`${JSON.stringify({ ok: false, error: { code, message: first, ...(guess === null ? {} : { fix: guess[1] }) } })}\n`);
1048
+ this._outputConfiguration.writeOut(`${JSON.stringify({ ok: false, error })}\n`);
1049
+ }
1050
+ _reportHandlerFailure(err) {
1051
+ const { exit, error } = handlerFailure(err);
1052
+ if (this._burgee?.json === true)
1053
+ this._writeFailure(error);
1054
+ else
1055
+ this._outputConfiguration.writeErr(`error: ${error.message}\n`);
1056
+ return exit;
1044
1057
  }
1045
1058
  _parseOptionsEnv() {
1046
1059
  for (const option of this.options) {
@@ -1413,6 +1426,10 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1413
1426
  _optionSpecs() {
1414
1427
  const specs = Object.create(null);
1415
1428
  for (const option of this.options) {
1429
+ const name = option.attributeName();
1430
+ const paired = this.options.some((o) => o.negate !== option.negate && o.attributeName() === name);
1431
+ if (option.negate && paired)
1432
+ continue;
1416
1433
  const spec = { type: option.required || option.optional ? 'string' : 'boolean' };
1417
1434
  if (option.description)
1418
1435
  spec.description = option.description;
@@ -1424,7 +1441,9 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1424
1441
  spec.default = option.defaultValue;
1425
1442
  if (option.envVar)
1426
1443
  spec.env = option.envVar;
1427
- Object.defineProperty(specs, option.attributeName(), { value: spec, enumerable: true, writable: true, configurable: true });
1444
+ if (spec.type === 'boolean')
1445
+ spec.negatable = paired;
1446
+ Object.defineProperty(specs, option.negate ? camel(option.name()) : name, { value: spec, enumerable: true, writable: true, configurable: true });
1428
1447
  }
1429
1448
  return specs;
1430
1449
  }
@@ -1510,7 +1529,7 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1510
1529
  const root = this._root();
1511
1530
  const burgee = root._burgee;
1512
1531
  if (burgee === undefined)
1513
- return run();
1532
+ return root._runCommanderWay(run);
1514
1533
  const finish = (code) => {
1515
1534
  root._burgee = undefined;
1516
1535
  burgee.exit?.(code);
@@ -1522,12 +1541,7 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1522
1541
  finish(e1(err));
1523
1542
  return;
1524
1543
  }
1525
- const message = err instanceof Error ? err.message : String(err);
1526
- if (burgee.json)
1527
- root._reportJson('runtime', message);
1528
- else
1529
- root._outputConfiguration.writeErr(`error: ${message}\n`);
1530
- finish(ExitCode.RUNTIME);
1544
+ finish(root._reportHandlerFailure(err));
1531
1545
  };
1532
1546
  try {
1533
1547
  const result = run();
@@ -1541,6 +1555,30 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1541
1555
  return undefined;
1542
1556
  }
1543
1557
  }
1558
+ _runCommanderWay(run) {
1559
+ const done = () => {
1560
+ this._burgee = undefined;
1561
+ };
1562
+ const fail = (err) => {
1563
+ const json = this._burgee?.json === true && !(err instanceof CommanderError);
1564
+ if (json)
1565
+ host.exitCode = this._reportHandlerFailure(err);
1566
+ done();
1567
+ if (!json)
1568
+ throw err;
1569
+ };
1570
+ try {
1571
+ const result = run();
1572
+ if (isThenable(result))
1573
+ return Promise.resolve(result).then(done, fail);
1574
+ done();
1575
+ return undefined;
1576
+ }
1577
+ catch (err) {
1578
+ fail(err);
1579
+ return undefined;
1580
+ }
1581
+ }
1544
1582
  _declares(flag) {
1545
1583
  return this._findOption(flag) !== undefined || this.commands.some((sub) => sub._declares(flag));
1546
1584
  }
package/dist/compat.d.ts CHANGED
@@ -26,5 +26,42 @@ export interface Graded {
26
26
  /** `passed / reference`, carried rather than recomputed so it is the oracle's own division. */
27
27
  rate: number;
28
28
  }
29
- /** The two hosts `migrate` rewrites, and nothing else — a row here without a mapping would claim a path that does not exist. */
30
- export declare const GRADED: Readonly<Record<string, Graded>>;
29
+ /** One host's grade, with the control beside it: the incumbent's own suite run against the incumbent in the same harness. */
30
+ export interface Row extends Graded {
31
+ /**
32
+ * Cases the incumbent itself passes here — from the published compatibility page, which
33
+ * `npm run compat:page` generates on ubuntu. A drop-in that passes as many as the control
34
+ * is **level**: every case it misses, the incumbent misses too, in this harness.
35
+ */
36
+ control: number;
37
+ }
38
+ /**
39
+ * Every host the oracle grades, keyed by its `hosts.ts` name. The lock holds `reference`,
40
+ * `passed` and `rate` equal to `baseline/<host>.json` and `control` equal to the page's
41
+ * control column.
42
+ */
43
+ export declare const GRADED: Readonly<Record<string, Row>>;
44
+ /** One graded path: an incumbent's specifier, and the family specifier that replaces it. */
45
+ export interface DropIn {
46
+ /** The `hosts.ts` name, which keys `GRADED`. */
47
+ host: string;
48
+ from: string;
49
+ to: string;
50
+ }
51
+ /**
52
+ * Every drop-in pair the oracle grades, derived from `hosts.ts`: `from` is what the
53
+ * incumbent's own tests import (`<host><subpath>`, or the import's `control` where the
54
+ * incumbent is a separate package, as `yargs-parser` is), `to` is `<target><subpath>`.
55
+ * `scripts/migrate-drop-ins-lock.test.ts` re-derives this list and fails on any difference.
56
+ */
57
+ export declare const DROP_INS: readonly DropIn[];
58
+ /**
59
+ * The version each incumbent package was graded at: `vendor/<host>/.source.json`, or for
60
+ * `yargs-parser` the copy the yargs control resolves. The grade is for that major and no other
61
+ * — `signal-exit` 3 exports a function where 4 exports `onExit`, and `chalk` 4 is CommonJS where
62
+ * 6 is not — so `migrate` leaves a project on another major alone and says so.
63
+ * `scripts/migrate-drop-ins-lock.test.ts` holds each equal to the oracle's.
64
+ */
65
+ export declare const GRADED_VERSIONS: Readonly<Record<string, string>>;
66
+ /** Level: the drop-in passes every case the incumbent passes against its own suite (D-137). */
67
+ export declare const isLevel: (host: string) => boolean;
package/dist/compat.js CHANGED
@@ -1,4 +1,92 @@
1
1
  export const GRADED = {
2
- commander: { reference: 1360, passed: 1360, rate: 1 },
3
- yargs: { reference: 804, passed: 804, rate: 1 },
2
+ commander: { reference: 1360, passed: 1360, rate: 1, control: 1360 },
3
+ yargs: { reference: 804, passed: 804, rate: 1, control: 802 },
4
+ chalk: { reference: 58, passed: 58, rate: 1, control: 58 },
5
+ ora: { reference: 99, passed: 99, rate: 1, control: 99 },
6
+ 'log-update': { reference: 99, passed: 99, rate: 1, control: 99 },
7
+ boxen: { reference: 84, passed: 84, rate: 1, control: 84 },
8
+ 'cli-table3': { reference: 29, passed: 29, rate: 1, control: 29 },
9
+ 'string-width': { reference: 229, passed: 229, rate: 1, control: 229 },
10
+ 'strip-ansi': { reference: 8, passed: 8, rate: 1, control: 8 },
11
+ 'cross-spawn': { reference: 68, passed: 68, rate: 1, control: 68 },
12
+ which: { reference: 5, passed: 5, rate: 1, control: 5 },
13
+ rc: { reference: 1, passed: 1, rate: 1, control: 1 },
14
+ 'wrap-ansi': { reference: 80, passed: 80, rate: 1, control: 80 },
15
+ 'slice-ansi': { reference: 15, passed: 15, rate: 1, control: 15 },
16
+ cosmiconfig: { reference: 243, passed: 186, rate: 0.7654320987654321, control: 240 },
17
+ lilconfig: { reference: 77, passed: 77, rate: 1, control: 77 },
18
+ dotenv: { reference: 141, passed: 106, rate: 0.75177304964539, control: 141 },
19
+ clack: { reference: 17, passed: 14, rate: 0.8235294117647058, control: 17 },
20
+ 'inquirer-core': { reference: 41, passed: 41, rate: 1, control: 41 },
21
+ meow: { reference: 148, passed: 132, rate: 0.8918918918918919, control: 146 },
22
+ 'ansi-escapes': { reference: 4, passed: 4, rate: 1, control: 4 },
23
+ 'terminal-link': { reference: 8, passed: 8, rate: 1, control: 8 },
24
+ 'term-img': { reference: 18, passed: 12, rate: 0.6666666666666666, control: 18 },
25
+ 'restore-cursor': { reference: 6, passed: 6, rate: 1, control: 6 },
26
+ 'exit-hook': { reference: 21, passed: 21, rate: 1, control: 21 },
27
+ 'signal-exit': { reference: 135, passed: 134, rate: 0.9925925925925926, control: 134 },
28
+ };
29
+ export const DROP_INS = [
30
+ { host: 'commander', from: 'commander', to: 'burgee/commander' },
31
+ { host: 'yargs', from: 'yargs', to: 'burgee/yargs' },
32
+ { host: 'yargs', from: 'yargs/helpers', to: 'burgee/yargs/helpers' },
33
+ { host: 'yargs', from: 'yargs-parser', to: 'burgee/yargs/parser' },
34
+ { host: 'chalk', from: 'chalk', to: 'roundel/chalk' },
35
+ { host: 'ora', from: 'ora', to: 'flagstaff/ora' },
36
+ { host: 'log-update', from: 'log-update', to: 'flagstaff/log-update' },
37
+ { host: 'boxen', from: 'boxen', to: 'flagstaff/boxen' },
38
+ { host: 'cli-table3', from: 'cli-table3', to: 'flagstaff/cli-table3' },
39
+ { host: 'string-width', from: 'string-width', to: 'linegauge' },
40
+ { host: 'strip-ansi', from: 'strip-ansi', to: 'linegauge/strip' },
41
+ { host: 'cross-spawn', from: 'cross-spawn', to: 'bellpull/cross-spawn' },
42
+ { host: 'which', from: 'which', to: 'bellpull/node-which' },
43
+ { host: 'rc', from: 'rc', to: 'seniority/rc' },
44
+ { host: 'wrap-ansi', from: 'wrap-ansi', to: 'linegauge/wrap' },
45
+ { host: 'slice-ansi', from: 'slice-ansi', to: 'linegauge/slice' },
46
+ { host: 'cosmiconfig', from: 'cosmiconfig', to: 'seniority' },
47
+ { host: 'lilconfig', from: 'lilconfig', to: 'seniority/lilconfig' },
48
+ { host: 'dotenv', from: 'dotenv', to: 'seniority/dotenv' },
49
+ { host: 'clack', from: '@clack/prompts', to: 'caique/clack' },
50
+ { host: 'inquirer-core', from: '@inquirer/core', to: 'caique/inquirer' },
51
+ { host: 'meow', from: 'meow', to: 'burgee/meow' },
52
+ { host: 'ansi-escapes', from: 'ansi-escapes', to: 'paratext' },
53
+ { host: 'terminal-link', from: 'terminal-link', to: 'paratext/terminal-link' },
54
+ { host: 'term-img', from: 'term-img', to: 'paratext/term-img' },
55
+ { host: 'restore-cursor', from: 'restore-cursor', to: 'closeout/restore-cursor' },
56
+ { host: 'exit-hook', from: 'exit-hook', to: 'closeout/exit-hook' },
57
+ { host: 'signal-exit', from: 'signal-exit', to: 'closeout/signal-exit' },
58
+ { host: 'signal-exit', from: 'signal-exit/signals', to: 'closeout/signal-exit/signals' },
59
+ ];
60
+ export const GRADED_VERSIONS = {
61
+ '@clack/prompts': '1.8.1',
62
+ '@inquirer/core': '12.0.3',
63
+ 'ansi-escapes': '7.3.0',
64
+ boxen: '8.0.1',
65
+ chalk: '6.0.0',
66
+ 'cli-table3': '0.6.5',
67
+ commander: '15.0.0',
68
+ cosmiconfig: '10.0.1',
69
+ 'cross-spawn': '7.0.6',
70
+ dotenv: '17.4.2',
71
+ 'exit-hook': '5.1.0',
72
+ lilconfig: '3.1.3',
73
+ 'log-update': '8.0.0',
74
+ meow: '14.1.0',
75
+ ora: '9.4.1',
76
+ rc: '1.2.8',
77
+ 'restore-cursor': '5.1.0',
78
+ 'signal-exit': '4.1.0',
79
+ 'slice-ansi': '7.1.2',
80
+ 'string-width': '8.2.2',
81
+ 'strip-ansi': '7.2.0',
82
+ 'term-img': '7.1.0',
83
+ 'terminal-link': '5.0.0',
84
+ which: '7.0.0',
85
+ 'wrap-ansi': '10.0.1',
86
+ yargs: '18.1.0',
87
+ 'yargs-parser': '22.0.0',
88
+ };
89
+ export const isLevel = (host) => {
90
+ const row = GRADED[host];
91
+ return row !== undefined && row.passed >= row.control;
4
92
  };
@@ -0,0 +1,18 @@
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
+ * D3 / D-119 — the program's side of a dynamic completion. A generated script calls
8
+ * `<program> __complete <command…> --<option> <partial>` for an option that declared
9
+ * `complete`, and prints what comes back, one candidate per line.
10
+ *
11
+ * It never fails loudly. A TAB that prints an error into someone's prompt is worse than one
12
+ * that offers nothing, so an unknown command, an option without a completer, or a completer
13
+ * that throws all print nothing and leave 0.
14
+ *
15
+ * Imported by `execute.ts` only when argv starts with `__complete` (M2).
16
+ */
17
+ import { type Manifest } from './manifest.js';
18
+ export declare function completeDynamic(manifest: Manifest, argv: readonly string[], write: (s: string) => unknown): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { kebab } from './names.js';
2
+ export async function completeDynamic(manifest, argv, write) {
3
+ const flag = argv.findIndex((a) => a.startsWith('--'));
4
+ if (flag === -1)
5
+ return;
6
+ const { node } = manifest.resolve(argv.slice(0, flag), manifest.rootPath);
7
+ const name = argv[flag]?.slice(2);
8
+ const spec = Object.entries(node?.options ?? {}).find(([key]) => kebab(key) === name)?.[1];
9
+ if (spec?.complete === undefined)
10
+ return;
11
+ const partial = argv[flag + 1] ?? '';
12
+ const { complete } = spec;
13
+ const offered = await Promise.resolve()
14
+ .then(async () => [...(await complete(partial))])
15
+ .then((all) => all, () => []);
16
+ const found = offered.filter((c) => c.startsWith(partial));
17
+ if (found.length > 0)
18
+ write(`${found.join('\n')}\n`);
19
+ }