burgee 0.7.1 → 0.9.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 (65) hide show
  1. package/README.md +1 -0
  2. package/dist/check.d.ts +22 -0
  3. package/dist/check.js +35 -0
  4. package/dist/cli.d.ts +31 -3
  5. package/dist/cli.js +36 -7
  6. package/dist/commander/argument.js +0 -3
  7. package/dist/commander/command.d.ts +7 -3
  8. package/dist/commander/command.js +54 -187
  9. package/dist/commander/error.js +0 -2
  10. package/dist/commander/help.js +0 -17
  11. package/dist/commander/option.js +0 -14
  12. package/dist/compat.d.ts +30 -0
  13. package/dist/compat.js +4 -0
  14. package/dist/config.d.ts +10 -0
  15. package/dist/config.js +2 -0
  16. package/dist/definition.d.ts +24 -6
  17. package/dist/definition.js +18 -14
  18. package/dist/execute.d.ts +1 -0
  19. package/dist/execute.js +45 -27
  20. package/dist/exit-code.d.ts +17 -1
  21. package/dist/exit-code.js +1 -0
  22. package/dist/help-entry.d.ts +2 -0
  23. package/dist/help-entry.js +1 -0
  24. package/dist/help.d.ts +12 -0
  25. package/dist/help.js +6 -0
  26. package/dist/index.d.ts +32 -7
  27. package/dist/index.js +1 -7
  28. package/dist/mcp-entry.d.ts +2 -0
  29. package/dist/mcp-entry.js +1 -0
  30. package/dist/mcp.d.ts +33 -13
  31. package/dist/mcp.js +4 -3
  32. package/dist/meow/parse.d.ts +21 -0
  33. package/dist/meow/parse.js +43 -0
  34. package/dist/meow/present.d.ts +35 -0
  35. package/dist/meow/present.js +58 -0
  36. package/dist/meow/types.d.ts +47 -0
  37. package/dist/meow/types.js +3 -0
  38. package/dist/meow/validate.d.ts +35 -0
  39. package/dist/meow/validate.js +144 -0
  40. package/dist/meow.d.ts +6 -0
  41. package/dist/meow.js +146 -0
  42. package/dist/migrate.d.ts +142 -0
  43. package/dist/migrate.js +284 -0
  44. package/dist/plugin.d.ts +1 -1
  45. package/dist/plugin.js +1 -1
  46. package/dist/runtime.d.ts +2 -0
  47. package/dist/runtime.js +3 -0
  48. package/dist/schema-entry.d.ts +3 -0
  49. package/dist/schema-entry.js +2 -0
  50. package/dist/schema.json +1 -1
  51. package/dist/testing-helpers.js +3 -1
  52. package/dist/validate.d.ts +15 -0
  53. package/dist/validate.js +10 -0
  54. package/dist/yargs/burgee.js +0 -14
  55. package/dist/yargs/cliui.js +0 -53
  56. package/dist/yargs/command.js +0 -7
  57. package/dist/yargs/completion.js +0 -5
  58. package/dist/yargs/factory.js +7 -57
  59. package/dist/yargs/middleware.js +0 -5
  60. package/dist/yargs/shim.js +0 -20
  61. package/dist/yargs/usage.js +0 -8
  62. package/dist/yargs/utils.js +0 -11
  63. package/dist/yargs/validation.js +0 -6
  64. package/dist/yargs/y18n.js +0 -6
  65. package/package.json +32 -7
@@ -1,4 +1,3 @@
1
- /** commander's error classes, byte-for-byte in shape: `code`, `exitCode`, `nestedError`. */
2
1
  export class CommanderError extends Error {
3
2
  code;
4
3
  exitCode;
@@ -19,4 +18,3 @@ export class InvalidArgumentError extends CommanderError {
19
18
  this.name = this.constructor.name;
20
19
  }
21
20
  }
22
- //# sourceMappingURL=error.js.map
@@ -1,13 +1,11 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
2
  import { humanReadableArgName } from './argument.js';
3
- /** Methods are static in style so a subclass, or plain functions via `configureHelp`, can override them. */
4
3
  export class Help {
5
4
  helpWidth = undefined;
6
5
  minWidthToWrap = 40;
7
6
  sortSubcommands = false;
8
7
  sortOptions = false;
9
8
  showGlobalOptions = false;
10
- /** Called after `configureHelp` overrides are applied and just before `formatHelp`. */
11
9
  prepareContext(contextOptions) {
12
10
  this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
13
11
  }
@@ -28,7 +26,6 @@ export class Help {
28
26
  const visibleOptions = cmd.options.filter((option) => !option.hidden);
29
27
  const helpOption = cmd._getHelpOption();
30
28
  if (helpOption && !helpOption.hidden) {
31
- // Historical: hide the built-in help flags a user option already took.
32
29
  const removeShort = helpOption.short && cmd._findOption(helpOption.short);
33
30
  const removeLong = helpOption.long && cmd._findOption(helpOption.long);
34
31
  if (!removeShort && !removeLong)
@@ -54,7 +51,6 @@ export class Help {
54
51
  return globalOptions;
55
52
  }
56
53
  visibleArguments(cmd) {
57
- // Side effect: apply the legacy descriptions before the arguments are displayed.
58
54
  if (cmd._argsDescription) {
59
55
  for (const argument of cmd.registeredArguments) {
60
56
  argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
@@ -110,7 +106,6 @@ export class Help {
110
106
  commandDescription(cmd) {
111
107
  return cmd.description();
112
108
  }
113
- /** Summary, falling back to description for backwards compatibility. */
114
109
  subcommandDescription(cmd) {
115
110
  return cmd.summary() || cmd.description();
116
111
  }
@@ -120,7 +115,6 @@ export class Help {
120
115
  extraInfo.push(`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
121
116
  }
122
117
  if (option.defaultValue !== undefined) {
123
- // Defaults for boolean and negated are more for the programmer than the end user.
124
118
  const showDefault = option.required || option.optional || (option.isBoolean() && typeof option.defaultValue === 'boolean');
125
119
  if (showDefault)
126
120
  extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
@@ -154,7 +148,6 @@ export class Help {
154
148
  return [];
155
149
  return [helper.styleTitle(heading), ...items, ''];
156
150
  }
157
- /** Groups in order of first appearance among all items; members in order of the visible items. */
158
151
  groupItems(unsortedItems, visibleItems, getGroup) {
159
152
  const result = new Map();
160
153
  for (const item of unsortedItems) {
@@ -203,7 +196,6 @@ export class Help {
203
196
  }
204
197
  return output.join('\n');
205
198
  }
206
- /** Width ignoring ANSI escapes, for padding and wrapping. */
207
199
  displayWidth(str) {
208
200
  return stripVTControlCharacters(str).length;
209
201
  }
@@ -211,7 +203,6 @@ export class Help {
211
203
  return str;
212
204
  }
213
205
  styleUsage(str) {
214
- // Assume the default usage shape: command subcommand [options] [command] <foo> [bar]
215
206
  return str
216
207
  .split(' ')
217
208
  .map((word) => {
@@ -273,15 +264,9 @@ export class Help {
273
264
  padWidth(cmd, helper) {
274
265
  return Math.max(helper.longestOptionTermLength(cmd, helper), helper.longestGlobalOptionTermLength(cmd, helper), helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper));
275
266
  }
276
- /** Manually wrapped text: a line break followed by whitespace. */
277
267
  preformatted(str) {
278
268
  return /\n[^\S\r\n]/.test(str);
279
269
  }
280
- /**
281
- * Pad the term and wrap the description, indenting the following lines:
282
- * TTT DDD DDDD
283
- * DD DDD
284
- */
285
270
  formatItem(term, termWidth, description, helper) {
286
271
  const itemIndent = 2;
287
272
  const itemIndentStr = ' '.repeat(itemIndent);
@@ -301,7 +286,6 @@ export class Help {
301
286
  }
302
287
  return itemIndentStr + paddedTerm + ' '.repeat(spacerWidth) + formattedDescription.replace(/\n/g, `\n${itemIndentStr}`);
303
288
  }
304
- /** Wrap at whitespace, preserving existing line breaks; skipped below `minWidthToWrap`. */
305
289
  boxWrap(str, width) {
306
290
  if (width < this.minWidthToWrap)
307
291
  return str;
@@ -333,4 +317,3 @@ export class Help {
333
317
  return wrappedLines.join('\n');
334
318
  }
335
319
  }
336
- //# sourceMappingURL=help.js.map
@@ -1,4 +1,3 @@
1
- import {} from './argument.js';
2
1
  import { InvalidArgumentError } from './error.js';
3
2
  export class Option {
4
3
  flags;
@@ -25,7 +24,6 @@ export class Option {
25
24
  this.description = description || '';
26
25
  this.required = flags.includes('<');
27
26
  this.optional = flags.includes('[');
28
- // `<value,...>` describes custom splitting of one argument, not a variadic option.
29
27
  this.variadic = /\w\.\.\.[>\]]$/.test(flags);
30
28
  const { shortFlag, longFlag } = splitOptionFlags(flags);
31
29
  this.short = shortFlag;
@@ -38,7 +36,6 @@ export class Option {
38
36
  this.defaultValueDescription = description;
39
37
  return this;
40
38
  }
41
- /** Value used when the option appears without an option-argument. `parseArg` still runs. */
42
39
  preset(arg) {
43
40
  this.presetArg = arg;
44
41
  return this;
@@ -47,7 +44,6 @@ export class Option {
47
44
  this.conflictsWith = this.conflictsWith.concat(names);
48
45
  return this;
49
46
  }
50
- /** Values implied for other options when this one is set and they are not. `parseArg` does not run on them. */
51
47
  implies(impliedOptionValues) {
52
48
  const newImplied = typeof impliedOptionValues === 'string' ? { [impliedOptionValues]: true } : impliedOptionValues;
53
49
  this.implied = Object.assign(this.implied ?? {}, newImplied);
@@ -90,7 +86,6 @@ export class Option {
90
86
  return this.long.replace(/^--/, '');
91
87
  return (this.short ?? '').replace(/^-/, '');
92
88
  }
93
- /** camelCase key used on the options object; `--no-foo` shares `foo` with `--foo`. */
94
89
  attributeName() {
95
90
  return camelcase(this.negate ? this.name().replace(/^no-/, '') : this.name());
96
91
  }
@@ -101,15 +96,10 @@ export class Option {
101
96
  is(arg) {
102
97
  return this.short === arg || this.long === arg;
103
98
  }
104
- /** Options are one of boolean, negated, required-argument or optional-argument. */
105
99
  isBoolean() {
106
100
  return !this.required && !this.optional && !this.negate;
107
101
  }
108
102
  }
109
- /**
110
- * `--build` and `--no-build` share one value. This works out which of the pair a
111
- * value (probably) came from, so implied values are only applied for the right one.
112
- */
113
103
  export class DualOptions {
114
104
  positiveOptions = new Map();
115
105
  negativeOptions = new Map();
@@ -135,7 +125,6 @@ export class DualOptions {
135
125
  function camelcase(str) {
136
126
  return str.split('-').reduce((acc, word) => acc + (word[0] ?? '').toUpperCase() + word.slice(1));
137
127
  }
138
- /** Split `'-m,--mixed <value>'` into its short and long flags, failing noisily on anything else. */
139
128
  export function splitOptionFlags(flags) {
140
129
  let shortFlag;
141
130
  let longFlag;
@@ -147,10 +136,8 @@ export function splitOptionFlags(flags) {
147
136
  shortFlag = flagParts.shift();
148
137
  if (longFlagExp.test(head()))
149
138
  longFlag = flagParts.shift();
150
- // Long then short. Rarely used but fine.
151
139
  if (!shortFlag && shortFlagExp.test(head()))
152
140
  shortFlag = flagParts.shift();
153
- // Two long flags, like '--ws, --workspace': the supported way to have a shortish flag.
154
141
  if (!shortFlag && longFlagExp.test(head())) {
155
142
  shortFlag = longFlag;
156
143
  longFlag = flagParts.shift();
@@ -175,4 +162,3 @@ export function splitOptionFlags(flags) {
175
162
  }
176
163
  return { shortFlag, longFlag };
177
164
  }
178
- //# sourceMappingURL=option.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A7 — the graded pass rate per host, as `burgee migrate` reports it.
3
+ *
4
+ * These are `compat-oracle`'s own numbers: the incumbent's test suite, vendored and run
5
+ * against burgee's front-end, which is what "drop-in" means here and the only claim in the
6
+ * package that is measured rather than argued.
7
+ *
8
+ * **They live here as a data module rather than as a literal in the report**, and
9
+ * `compat-baseline-lock.test.ts` holds this file equal to
10
+ * `packages/compat-oracle/baseline/<host>.json` — the same files the compat page renders.
11
+ * Changing a number here without the measurement moving fails that lock, which is the
12
+ * whole point: a number typed into a report template is a number that goes stale silently,
13
+ * and this repository has published four of those and caught them all late.
14
+ *
15
+ * It cannot be read from the oracle at run time. `compat-oracle` is `private: true` and is
16
+ * never published, so a user who installs `burgee` has no baseline directory to read; a
17
+ * copy with a lock on it is the strongest control available on the published side, and the
18
+ * lock is what makes it a copy rather than a claim.
19
+ */
20
+ /** One row of the baseline, in the shape `baseline/<host>.json` stores it. */
21
+ export interface Graded {
22
+ /** Cases in the incumbent's own suite. */
23
+ reference: number;
24
+ /** Cases that pass against burgee's front-end. */
25
+ passed: number;
26
+ /** `passed / reference`, carried rather than recomputed so it is the oracle's own division. */
27
+ rate: number;
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>>;
package/dist/compat.js ADDED
@@ -0,0 +1,4 @@
1
+ export const GRADED = {
2
+ commander: { reference: 1360, passed: 1360, rate: 1 },
3
+ yargs: { reference: 804, passed: 804, rate: 1 },
4
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `burgee/config` — the configuration layer, by itself.
3
+ *
4
+ * Precedence, provenance and `--explain` come from `seniority`, the package whose job that
5
+ * is. They were re-exported from the root barrel until the barrel's cost was measured
6
+ * (see `index.ts`): 3,135 bundled bytes on the startup path of every program, for a
7
+ * surface a program only touches when it wants to read or explain its own configuration.
8
+ */
9
+ export { ConfigError, envName, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority/precedence';
10
+ export { explain } from 'seniority/explain';
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ export { ConfigError, envName, resolve, screaming } from 'seniority/precedence';
2
+ export { explain } from 'seniority/explain';
@@ -26,11 +26,20 @@ import { type OptionSpec } from './manifest.js';
26
26
  export declare const WITHHELD = "withheld";
27
27
  /**
28
28
  * What must be true of a declaration before anything runs (yargs #1198, #887, #1679):
29
- * a known type, one short alias per command, no two keys that meet on the command line.
29
+ * a known type, one short alias per command, no two keys that meet on the command line, and
30
+ * none of V5's reserved surfaces.
31
+ *
32
+ * The reserved names used to be a second loop over the same keys in {@link checkCommand},
33
+ * and the numeric bound used to be a fourth branch in this one. Both moved to pay for the
34
+ * canonical-key check below without raising a weight ceiling — `./plugin` had 5 bytes of
35
+ * headroom — and both are better where they are now: `flag` is computed once here and
36
+ * `kebab` is the identity on every reserved name, and a numeric bound is a fact about a
37
+ * spec rather than about a name. The file is 51 bytes smaller than before the check existed.
30
38
  */
31
39
  export declare function checkDefinition(name: string, options: Record<string, OptionSpec>): void;
32
40
  /**
33
- * The whole door: the reserved names of V5, {@link checkDefinition}, and {@link checkEffects}.
41
+ * The whole door: {@link checkDefinition} — which carries V5's reserved names — and
42
+ * {@link checkEffects}.
34
43
  *
35
44
  * This exists as one function because it was two. `defineCommand` ran both; `Manifest.use()`
36
45
  * ran neither, so a plugin's command was admitted unread — and a plugin option named `json`
@@ -38,8 +47,17 @@ export declare function checkDefinition(name: string, options: Record<string, Op
38
47
  * second copy of the guard beside `use()`; it is that there is one guard and both callers
39
48
  * reach it, which is the only arrangement a reader can check by looking.
40
49
  *
41
- * `effects` and `runs` are required rather than optional for exactly that reason. An optional
42
- * third argument would be a check a caller can decline by writing nothing, which is the shape
43
- * of the defect `checkEffects` exists to remove, one level up.
50
+ * It takes the declaration whole, for exactly that reason. It took `effects` and `runs` as
51
+ * required arguments so a caller could not decline a check by writing nothing; D1 would have
52
+ * made that five, and a sixth field would make it six. Reading the object means a field the
53
+ * door checks is one no caller has to remember to forward — including whether it runs.
44
54
  */
45
- export declare function checkCommand(name: string, options: Record<string, OptionSpec>, effects: unknown, runs: boolean): void;
55
+ export declare function checkCommand(name: string, declared: Declared): void;
56
+ /** What the door reads of a command: a first-party declaration and a plugin's have the same fields. */
57
+ export interface Declared {
58
+ options?: Record<string, OptionSpec>;
59
+ effects?: unknown;
60
+ deprecated?: boolean | string;
61
+ run?: unknown;
62
+ load?: unknown;
63
+ }
@@ -1,10 +1,14 @@
1
- import { kebab } from './names.js';
1
+ import { camel, kebab } from './names.js';
2
2
  const TYPES = new Set(['string', 'boolean', 'number']);
3
3
  const EFFECTS = ['read_only', 'idempotent', 'non_idempotent'];
4
4
  export const WITHHELD = 'withheld';
5
5
  const DECLARED = [...EFFECTS, WITHHELD];
6
6
  const RESERVED = new Set(['json', 'help', 'schema', 'mcp', 'version', 'explain']);
7
- function checkRelationNames(name, key, spec, options) {
7
+ function checkSpec(name, key, spec, options) {
8
+ checkDeprecated(`option "${key}" of "${name}"`, spec.deprecated);
9
+ if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
10
+ throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
11
+ }
8
12
  for (const field of ['dependsOn', 'exclusive']) {
9
13
  for (const other of spec[field] ?? []) {
10
14
  if (other !== key && other in options)
@@ -27,14 +31,13 @@ export function checkDefinition(name, options) {
27
31
  shorts.set(spec.short, key);
28
32
  }
29
33
  const flag = kebab(key);
30
- const clash = flags.get(flag);
34
+ if (RESERVED.has(flag))
35
+ throw new Error(`burgee: option "${key}" is reserved and cannot be redefined`);
36
+ const clash = flags.get(flag) ?? (camel(flag) === key ? undefined : camel(flag));
31
37
  if (clash !== undefined)
32
38
  throw new Error(`burgee: options "${clash}" and "${key}" of "${name}" are both --${flag}`);
33
39
  flags.set(flag, key);
34
- if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
35
- throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
36
- }
37
- checkRelationNames(name, key, spec, options);
40
+ checkSpec(name, key, spec, options);
38
41
  }
39
42
  }
40
43
  function checkEffects(name, effects, runs) {
@@ -47,11 +50,12 @@ function checkEffects(name, effects, runs) {
47
50
  throw new Error(`burgee: command "${name}" declares effects ${JSON.stringify(effects)}, which is not an effects; use ${DECLARED.join(', ')}`);
48
51
  }
49
52
  }
50
- export function checkCommand(name, options, effects, runs) {
51
- for (const key of Object.keys(options)) {
52
- if (RESERVED.has(key) || RESERVED.has(kebab(key)))
53
- throw new Error(`burgee: option "${key}" is reserved and cannot be redefined`);
54
- }
55
- checkDefinition(name, options);
56
- checkEffects(name, effects, runs);
53
+ function checkDeprecated(what, deprecated) {
54
+ if (deprecated === true || deprecated === '')
55
+ throw new Error(`burgee: ${what} is deprecated with no replacement; name it, e.g. deprecated: '--force'`);
56
+ }
57
+ export function checkCommand(name, declared) {
58
+ checkDeprecated(`command "${name}"`, declared.deprecated);
59
+ checkDefinition(name, declared.options ?? {});
60
+ checkEffects(name, declared.effects, declared.run !== undefined || declared.load !== undefined);
57
61
  }
package/dist/execute.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface Command<S extends OptionSpecs = OptionSpecs> {
32
32
  group?: string;
33
33
  epilogue?: string;
34
34
  hidden?: boolean;
35
+ /** What replaces this command, e.g. `'deploy'`: shown in help, `--schema` and the warning. `true` alone is refused (D1). */
35
36
  deprecated?: boolean | string;
36
37
  /**
37
38
  * What running it does to the world (N6). Declaring one of the three is what exposes the
package/dist/execute.js CHANGED
@@ -1,18 +1,15 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
- import { ConfigError, explain, resolve as resolveLayers } from 'seniority/precedence';
3
+ import { ConfigError, resolve as resolveLayers } from 'seniority/precedence';
4
4
  import { detectAgent } from './agent.js';
5
5
  import { checkCommand } from './definition.js';
6
6
  import { ExitCode, isExitCode } from './exit-code.js';
7
- import { renderHelp } from './help.js';
8
7
  import { Manifest, relationsOf } from './manifest.js';
9
- import { serveMcp } from './mcp.js';
10
8
  import { camel, kebab } from './names.js';
11
9
  import { nearestPackage } from './pkg.js';
12
10
  import { host } from './runtime.js';
13
- import { commandSchemaOf, machineJson, schemaOf, summaryOf, typedName } from './schema.js';
14
11
  import { detachedTeardown, processTeardown } from './shutdown.js';
15
- import { checkRelations, coerce, UsageError } from './validate.js';
12
+ import { AuthError, checkRelations, coerce, UsageError } from './validate.js';
16
13
  function helpFields(c) {
17
14
  const node = {};
18
15
  if (c.description !== undefined)
@@ -38,7 +35,7 @@ function helpFields(c) {
38
35
  return node;
39
36
  }
40
37
  export function defineCommand(command) {
41
- checkCommand(command.name, command.options ?? {}, command.effects, command.run !== undefined || command.load !== undefined);
38
+ checkCommand(command.name, command);
42
39
  return command;
43
40
  }
44
41
  function addTree(manifest, parent, commands) {
@@ -182,7 +179,7 @@ async function resolveValues(manifest, specs, values, io) {
182
179
  const out = { values: resolution.values, provenance: resolution.provenance };
183
180
  const asked = values['explain'];
184
181
  if (typeof asked === 'string')
185
- out.explainText = explain(asked, resolution);
182
+ out.explainText = (await import('seniority/explain')).explain(asked, resolution);
186
183
  for (const [name, spec] of Object.entries(specs)) {
187
184
  if (out.values[name] === undefined && spec.required === true && out.explainText === undefined) {
188
185
  throw new UsageError(`missing required option --${kebab(name)}`, `pass --${kebab(name)} <value>`);
@@ -214,6 +211,18 @@ function exitSignal(cause) {
214
211
  const code = cause?.code;
215
212
  return typeof code === 'number' && isExitCode(code) ? code : undefined;
216
213
  }
214
+ const CLASSIFIED = [
215
+ [UsageError, ExitCode.USAGE],
216
+ [AuthError, ExitCode.AUTH],
217
+ [ConfigError, ExitCode.CONFIG],
218
+ ];
219
+ function carried(cause) {
220
+ const { hint, fix } = (cause ?? {});
221
+ return {
222
+ ...(typeof hint === 'string' ? { hint } : {}),
223
+ ...(typeof fix === 'string' ? { fix } : {}),
224
+ };
225
+ }
217
226
  async function describeFailure(cause, argv, node) {
218
227
  const signal = exitSignal(cause);
219
228
  if (signal !== undefined)
@@ -221,12 +230,9 @@ async function describeFailure(cause, argv, node) {
221
230
  const message = cause instanceof Error ? cause.message : String(cause);
222
231
  if (cause instanceof ActionRequired)
223
232
  return { code: ExitCode.CANCELLED, message, action: cause.spec, ...(cause.spec.hint === undefined ? {} : { hint: cause.spec.hint }) };
224
- if (cause instanceof UsageError) {
225
- return { code: ExitCode.USAGE, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
226
- }
227
- if (cause instanceof ConfigError) {
228
- return { code: ExitCode.CONFIG, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
229
- }
233
+ const named = CLASSIFIED.find(([Class]) => cause instanceof Class);
234
+ if (named !== undefined)
235
+ return { code: named[1], message, ...carried(cause) };
230
236
  if (isParseArgsFailure(cause)) {
231
237
  const explain = await import('./unknown-option.js');
232
238
  const dash = explain.singleDashHint(argv);
@@ -251,7 +257,8 @@ function runnableNext(manifest, spec, json) {
251
257
  return (spec.next ?? []).map((n) => ({ command: `${program} ${n.command}${json && !n.command.includes('--json') ? ' --json' : ''}`, when: n.when }));
252
258
  }
253
259
  const HELP_FLAGS = new Set(['--help', '-h']);
254
- function helpDocumentOf(manifest, node) {
260
+ async function helpDocumentOf(manifest, node) {
261
+ const { commandSchemaOf, typedName } = await import('./schema.js');
255
262
  const root = manifest.rootPath;
256
263
  const children = manifest.commands
257
264
  .filter((c) => c.path.length === node.path.length + 1 && c.path.slice(0, node.path.length).join(' ') === node.path.join(' '))
@@ -275,19 +282,24 @@ export function beforeTerminator(argv) {
275
282
  function rootNode(manifest, root) {
276
283
  return manifest.find(root) ?? { path: root, options: {} };
277
284
  }
278
- function unresolved({ manifest, root, io }, argv, at) {
285
+ const renderHelp = async (manifest, node, io) => {
286
+ const help = await import('./help.js');
287
+ return help.renderHelp(manifest, node, { width: io.width, color: help.colorFor(io.env, detectAgent(io.env, io.tty).interactive) });
288
+ };
289
+ const machineJson = async (...args) => (await import('./schema.js')).machineJson(...args);
290
+ async function unresolved({ manifest, root, io }, argv, at) {
279
291
  const node = at ?? rootNode(manifest, root);
280
292
  const typed = argv.slice(node.path.length - root.length);
281
293
  const first = typed[0] ?? '';
282
294
  if (typed.length > 0 && HELP_FLAGS.has(first)) {
283
295
  if (beforeTerminator(typed).includes('--json'))
284
- return { text: `${machineJson(helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
285
- return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.OK };
296
+ return { text: `${await machineJson(await helpDocumentOf(manifest, node), beforeTerminator(argv))}\n`, code: ExitCode.OK };
297
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.OK };
286
298
  }
287
299
  if (first === '--version' || first === '-V')
288
300
  return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
289
301
  if (typed.length === 0)
290
- return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.USAGE };
302
+ return { text: await renderHelp(manifest, node, io), code: ExitCode.USAGE };
291
303
  throw new UsageError(`unknown command "${typed[0] ?? ''}"`, 'run --help to see the available commands');
292
304
  }
293
305
  async function completion(manifest, argv, io) {
@@ -310,11 +322,11 @@ async function surface(manifest, argv, io) {
310
322
  if (await completion(manifest, argv, io))
311
323
  return true;
312
324
  if (argv[0] === 'help') {
313
- io.out.write(helpCommand(manifest, argv.slice(1), manifest.rootPath, io.width));
325
+ io.out.write(await helpCommand(manifest, argv.slice(1), manifest.rootPath, io));
314
326
  return true;
315
327
  }
316
328
  if (head.includes('--schema')) {
317
- io.out.write(`${machineJson(schemaSurface(manifest, argv), head)}\n`);
329
+ io.out.write(`${await machineJson(await schemaSurface(manifest, argv), head)}\n`);
318
330
  return true;
319
331
  }
320
332
  if (head[0] === '--mcp') {
@@ -333,13 +345,15 @@ async function surface(manifest, argv, io) {
333
345
  });
334
346
  return { stdout: out.join(''), stderr: err.join(''), code };
335
347
  };
348
+ const { serveMcp } = await import('./mcp.js');
336
349
  await serveMcp(manifest, { input: io.stdin, output: io.out, invoke });
337
350
  return true;
338
351
  }
339
352
  return false;
340
353
  }
341
354
  const SCHEMA_BUDGET = 48_000;
342
- function schemaSurface(manifest, argv) {
355
+ async function schemaSurface(manifest, argv) {
356
+ const { commandSchemaOf, schemaOf, summaryOf } = await import('./schema.js');
343
357
  const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema' && !a.startsWith('--format=')), manifest.rootPath);
344
358
  if (node?.run !== undefined)
345
359
  return commandSchemaOf(node, manifest.rootPath);
@@ -347,9 +361,9 @@ function schemaSurface(manifest, argv) {
347
361
  const budget = manifest.schemaBudget ?? SCHEMA_BUDGET;
348
362
  return JSON.stringify(full).length <= budget ? full : summaryOf(manifest, budget);
349
363
  }
350
- function helpCommand(manifest, argv, root, width) {
364
+ async function helpCommand(manifest, argv, root, io) {
351
365
  const { node } = manifest.resolve(argv, root);
352
- return renderHelp(manifest, node ?? rootNode(manifest, root), { width });
366
+ return renderHelp(manifest, node ?? rootNode(manifest, root), io);
353
367
  }
354
368
  function changedOf(node, data) {
355
369
  const value = isPlainObject(data) ? data['changed'] : undefined;
@@ -360,6 +374,10 @@ function changedOf(node, data) {
360
374
  }
361
375
  return undefined;
362
376
  }
377
+ function exitCodeOf(data) {
378
+ const code = isPlainObject(data) ? data['exitCode'] : undefined;
379
+ return isExitCode(code) ? code : ExitCode.OK;
380
+ }
363
381
  function versionOf(manifest, io) {
364
382
  const declared = manifest.version ?? (typeof io.pkg?.data['version'] === 'string' ? io.pkg.data['version'] : undefined);
365
383
  if (declared === undefined)
@@ -371,9 +389,9 @@ async function dispatch(manifest, { node, rest, name }, io) {
371
389
  const flags = canonical(parsed.values, node.options, parsed.tokens);
372
390
  const json = flags.json === true;
373
391
  if (flags.help === true && json)
374
- return { json, text: `${machineJson(helpDocumentOf(manifest, node), json ? ['--json'] : [])}\n` };
392
+ return { json, text: `${await machineJson(await helpDocumentOf(manifest, node), json ? ['--json'] : [])}\n` };
375
393
  if (flags.help === true)
376
- return { json, text: renderHelp(manifest, node, { width: io.width }) };
394
+ return { json, text: await renderHelp(manifest, node, io) };
377
395
  if (flags.version === true)
378
396
  return { json, text: `${versionOf(manifest, io)}\n` };
379
397
  const resolved = await resolveValues(manifest, node.options, flags, io);
@@ -420,7 +438,7 @@ async function emit(io, outcome) {
420
438
  const meta = { provenance: outcome.provenance ?? {}, ...(outcome.changed === undefined ? {} : { changed: outcome.changed }) };
421
439
  const envelope = { ok: true, data: outcome.data, meta };
422
440
  io.out.write(outcome.json ? `${JSON.stringify(envelope)}\n` : `${render(outcome.data)}\n`);
423
- return await leave(io, ExitCode.OK);
441
+ return await leave(io, exitCodeOf(outcome.data));
424
442
  }
425
443
  async function report(cause, { manifest, io, argv, json, name }) {
426
444
  const failure = await describeFailure(cause, argv, resolveCommand(manifest, argv) ?? undefined);
@@ -465,7 +483,7 @@ export async function execute(manifest, opts = {}) {
465
483
  return await leave(io, ExitCode.OK);
466
484
  const { node, rest } = manifest.resolve(argv, root);
467
485
  if (node?.run === undefined) {
468
- const { text, code } = unresolved({ manifest, root, io }, argv, node);
486
+ const { text, code } = await unresolved({ manifest, root, io }, argv, node);
469
487
  (code === ExitCode.OK ? io.out : io.err).write(text);
470
488
  return await leave(io, code);
471
489
  }
@@ -10,9 +10,25 @@ export declare const ExitCode: {
10
10
  readonly CONFIG: 3;
11
11
  /** The user or caller cancelled. */
12
12
  readonly CANCELLED: 4;
13
+ /**
14
+ * E6 — the far side said no: a credential is missing, expired, or refused.
15
+ *
16
+ * Its own code because it is the most actionable one in the survey. `RUNTIME` means *it
17
+ * failed, read the message*; `AUTH` means *log in and run it again*, and a script or an
18
+ * agent can branch on that without parsing prose. `USAGE` says fix the script, `CONFIG`
19
+ * says fix the runner, and this says fix the credential — three different responses that
20
+ * collapsed into one code before it existed.
21
+ *
22
+ * **5, where `gh` uses 4.** Four is `CANCELLED` here and has been since the contract was
23
+ * written, and moving a published code to match another tool's is a breaking change for
24
+ * every consumer that already branches on it. The survey's other citation, `aws` v2, uses
25
+ * 252/253/254 and agrees with nobody either; what matters is that the code is stable and
26
+ * documented, not that it matches a particular neighbour.
27
+ */
28
+ readonly AUTH: 5;
13
29
  /** SIGINT after the terminal was restored (E5). */
14
30
  readonly SIGINT: 130;
15
31
  };
16
32
  export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
17
- /** True for the six codes in the contract and nothing else. */
33
+ /** True for the seven codes in the contract and nothing else. */
18
34
  export declare function isExitCode(n: unknown): n is ExitCode;
package/dist/exit-code.js CHANGED
@@ -4,6 +4,7 @@ export const ExitCode = {
4
4
  USAGE: 2,
5
5
  CONFIG: 3,
6
6
  CANCELLED: 4,
7
+ AUTH: 5,
7
8
  SIGINT: 130,
8
9
  };
9
10
  const CODES = new Set(Object.values(ExitCode));
@@ -0,0 +1,2 @@
1
+ /** `burgee/help` — the help renderer, by itself. See `index.ts` for why it is not in the barrel. */
2
+ export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
@@ -0,0 +1 @@
1
+ export { renderHelp } from './help.js';
package/dist/help.d.ts CHANGED
@@ -24,6 +24,18 @@ export interface HelpOptions {
24
24
  */
25
25
  theme?: HelpTheme;
26
26
  }
27
+ /**
28
+ * Whether the engine colours help (O2). `FORCE_COLOR` decides when set — `0` and `false`
29
+ * off, anything else, the empty string included, on — so it overrides a pipe and
30
+ * `NO_COLOR` both, as Node's own `getColorDepth` does. Otherwise colour needs someone to
31
+ * see it: an interactive terminal (the caller's answer, in which a detected agent is not
32
+ * one, N12), no non-empty `NO_COLOR`, and a `TERM` other than `dumb`.
33
+ *
34
+ * It lives here, not in the engine, so the startup path pays for none of it (W4).
35
+ * Not `tty.WriteStream.prototype.hasColors(env)`, which gives the same answers: with
36
+ * both variables set it calls `process.emitWarning`, and this runs under an injected env.
37
+ */
38
+ export declare function colorFor(env: Record<string, string | undefined>, interactive: boolean): boolean;
27
39
  /**
28
40
  * Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120).
29
41
  *
package/dist/help.js CHANGED
@@ -3,6 +3,12 @@ import { width as displayWidth, widest } from 'linegauge';
3
3
  import { flagsOf, kebab } from './names.js';
4
4
  const identity = (s) => s;
5
5
  const PLAIN = { heading: identity, command: identity, flag: identity, value: identity };
6
+ export function colorFor(env, interactive) {
7
+ const force = env['FORCE_COLOR'];
8
+ if (force !== undefined)
9
+ return force !== '0' && force !== 'false';
10
+ return interactive && !env['NO_COLOR'] && env['TERM'] !== 'dumb';
11
+ }
6
12
  const DEFAULTS = {
7
13
  heading: (s) => styleText('bold', s, { validateStream: false }),
8
14
  command: (s) => styleText('bold', s, { validateStream: false }),