burgee 0.3.0 → 0.5.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -9
  3. package/dist/brand.d.ts +62 -1
  4. package/dist/brand.js +73 -7
  5. package/dist/{commander-argument.js → commander/argument.js} +4 -1
  6. package/dist/{commander-command.d.ts → commander/command.d.ts} +5 -5
  7. package/dist/{commander-command.js → commander/command.js} +159 -22
  8. package/dist/{commander-error.js → commander/error.js} +2 -0
  9. package/dist/{commander-help.d.ts → commander/help.d.ts} +3 -3
  10. package/dist/{commander-help.js → commander/help.js} +18 -1
  11. package/dist/{commander-option.d.ts → commander/option.d.ts} +1 -1
  12. package/dist/{commander-option.js → commander/option.js} +15 -1
  13. package/dist/commander.d.ts +8 -8
  14. package/dist/commander.js +8 -8
  15. package/dist/completions.js +15 -4
  16. package/dist/contrast.d.ts +9 -11
  17. package/dist/contrast.js +2 -33
  18. package/dist/execute.js +39 -24
  19. package/dist/schema.d.ts +34 -0
  20. package/dist/schema.js +12 -0
  21. package/dist/unknown-option.d.ts +9 -0
  22. package/dist/unknown-option.js +27 -0
  23. package/dist/{yargs-burgee.d.ts → yargs/burgee.d.ts} +2 -2
  24. package/dist/{yargs-burgee.js → yargs/burgee.js} +15 -1
  25. package/dist/{yargs-cliui.js → yargs/cliui.js} +8 -0
  26. package/dist/{yargs-command.d.ts → yargs/command.d.ts} +5 -5
  27. package/dist/{yargs-command.js → yargs/command.js} +9 -2
  28. package/dist/{yargs-completion.d.ts → yargs/completion.d.ts} +3 -3
  29. package/dist/{yargs-completion.js → yargs/completion.js} +7 -2
  30. package/dist/{yargs-factory.d.ts → yargs/factory.d.ts} +8 -8
  31. package/dist/{yargs-factory.js → yargs/factory.js} +63 -13
  32. package/dist/{yargs-middleware.js → yargs/middleware.js} +6 -1
  33. package/dist/{yargs-shim.d.ts → yargs/shim.d.ts} +3 -3
  34. package/dist/{yargs-shim.js → yargs/shim.js} +24 -5
  35. package/dist/{yargs-usage.d.ts → yargs/usage.d.ts} +1 -1
  36. package/dist/{yargs-usage.js → yargs/usage.js} +9 -1
  37. package/dist/{yargs-utils.js → yargs/utils.js} +11 -0
  38. package/dist/{yargs-validation.d.ts → yargs/validation.d.ts} +1 -1
  39. package/dist/{yargs-validation.js → yargs/validation.js} +7 -1
  40. package/dist/{yargs-y18n.js → yargs/y18n.js} +6 -0
  41. package/dist/yargs-helpers.d.ts +1 -1
  42. package/dist/yargs-helpers.js +1 -1
  43. package/dist/yargs.d.ts +4 -4
  44. package/dist/yargs.js +5 -5
  45. package/package.json +5 -1
  46. /package/dist/{commander-argument.d.ts → commander/argument.d.ts} +0 -0
  47. /package/dist/{commander-error.d.ts → commander/error.d.ts} +0 -0
  48. /package/dist/{commander-suggest.d.ts → suggest.d.ts} +0 -0
  49. /package/dist/{commander-suggest.js → suggest.js} +0 -0
  50. /package/dist/{yargs-cliui.d.ts → yargs/cliui.d.ts} +0 -0
  51. /package/dist/{yargs-middleware.d.ts → yargs/middleware.d.ts} +0 -0
  52. /package/dist/{yargs-utils.d.ts → yargs/utils.d.ts} +0 -0
  53. /package/dist/{yargs-y18n.d.ts → yargs/y18n.d.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ /** commander's error classes, byte-for-byte in shape: `code`, `exitCode`, `nestedError`. */
1
2
  export class CommanderError extends Error {
2
3
  code;
3
4
  exitCode;
@@ -18,3 +19,4 @@ export class InvalidArgumentError extends CommanderError {
18
19
  this.name = this.constructor.name;
19
20
  }
20
21
  }
22
+ //# sourceMappingURL=error.js.map
@@ -1,6 +1,6 @@
1
- import { type Argument } from './commander-argument.js';
2
- import type { Command } from './commander-command.js';
3
- import type { Option } from './commander-option.js';
1
+ import { type Argument } from './argument.js';
2
+ import type { Command } from './command.js';
3
+ import type { Option } from './option.js';
4
4
  export interface HelpContext {
5
5
  error?: boolean;
6
6
  helpWidth?: number;
@@ -1,11 +1,13 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
- import { humanReadableArgName } from './commander-argument.js';
2
+ import { humanReadableArgName } from './argument.js';
3
+ /** Methods are static in style so a subclass, or plain functions via `configureHelp`, can override them. */
3
4
  export class Help {
4
5
  helpWidth = undefined;
5
6
  minWidthToWrap = 40;
6
7
  sortSubcommands = false;
7
8
  sortOptions = false;
8
9
  showGlobalOptions = false;
10
+ /** Called after `configureHelp` overrides are applied and just before `formatHelp`. */
9
11
  prepareContext(contextOptions) {
10
12
  this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
11
13
  }
@@ -26,6 +28,7 @@ export class Help {
26
28
  const visibleOptions = cmd.options.filter((option) => !option.hidden);
27
29
  const helpOption = cmd._getHelpOption();
28
30
  if (helpOption && !helpOption.hidden) {
31
+ // Historical: hide the built-in help flags a user option already took.
29
32
  const removeShort = helpOption.short && cmd._findOption(helpOption.short);
30
33
  const removeLong = helpOption.long && cmd._findOption(helpOption.long);
31
34
  if (!removeShort && !removeLong)
@@ -51,6 +54,7 @@ export class Help {
51
54
  return globalOptions;
52
55
  }
53
56
  visibleArguments(cmd) {
57
+ // Side effect: apply the legacy descriptions before the arguments are displayed.
54
58
  if (cmd._argsDescription) {
55
59
  for (const argument of cmd.registeredArguments) {
56
60
  argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
@@ -106,6 +110,7 @@ export class Help {
106
110
  commandDescription(cmd) {
107
111
  return cmd.description();
108
112
  }
113
+ /** Summary, falling back to description for backwards compatibility. */
109
114
  subcommandDescription(cmd) {
110
115
  return cmd.summary() || cmd.description();
111
116
  }
@@ -115,6 +120,7 @@ export class Help {
115
120
  extraInfo.push(`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
116
121
  }
117
122
  if (option.defaultValue !== undefined) {
123
+ // Defaults for boolean and negated are more for the programmer than the end user.
118
124
  const showDefault = option.required || option.optional || (option.isBoolean() && typeof option.defaultValue === 'boolean');
119
125
  if (showDefault)
120
126
  extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
@@ -148,6 +154,7 @@ export class Help {
148
154
  return [];
149
155
  return [helper.styleTitle(heading), ...items, ''];
150
156
  }
157
+ /** Groups in order of first appearance among all items; members in order of the visible items. */
151
158
  groupItems(unsortedItems, visibleItems, getGroup) {
152
159
  const result = new Map();
153
160
  for (const item of unsortedItems) {
@@ -196,6 +203,7 @@ export class Help {
196
203
  }
197
204
  return output.join('\n');
198
205
  }
206
+ /** Width ignoring ANSI escapes, for padding and wrapping. */
199
207
  displayWidth(str) {
200
208
  return stripVTControlCharacters(str).length;
201
209
  }
@@ -203,6 +211,7 @@ export class Help {
203
211
  return str;
204
212
  }
205
213
  styleUsage(str) {
214
+ // Assume the default usage shape: command subcommand [options] [command] <foo> [bar]
206
215
  return str
207
216
  .split(' ')
208
217
  .map((word) => {
@@ -264,9 +273,15 @@ export class Help {
264
273
  padWidth(cmd, helper) {
265
274
  return Math.max(helper.longestOptionTermLength(cmd, helper), helper.longestGlobalOptionTermLength(cmd, helper), helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper));
266
275
  }
276
+ /** Manually wrapped text: a line break followed by whitespace. */
267
277
  preformatted(str) {
268
278
  return /\n[^\S\r\n]/.test(str);
269
279
  }
280
+ /**
281
+ * Pad the term and wrap the description, indenting the following lines:
282
+ * TTT DDD DDDD
283
+ * DD DDD
284
+ */
270
285
  formatItem(term, termWidth, description, helper) {
271
286
  const itemIndent = 2;
272
287
  const itemIndentStr = ' '.repeat(itemIndent);
@@ -286,6 +301,7 @@ export class Help {
286
301
  }
287
302
  return itemIndentStr + paddedTerm + ' '.repeat(spacerWidth) + formattedDescription.replace(/\n/g, `\n${itemIndentStr}`);
288
303
  }
304
+ /** Wrap at whitespace, preserving existing line breaks; skipped below `minWidthToWrap`. */
289
305
  boxWrap(str, width) {
290
306
  if (width < this.minWidthToWrap)
291
307
  return str;
@@ -317,3 +333,4 @@ export class Help {
317
333
  return wrappedLines.join('\n');
318
334
  }
319
335
  }
336
+ //# sourceMappingURL=help.js.map
@@ -1,4 +1,4 @@
1
- import { type ParseArg } from './commander-argument.js';
1
+ import { type ParseArg } from './argument.js';
2
2
  export declare class Option {
3
3
  flags: string;
4
4
  description: string;
@@ -1,4 +1,5 @@
1
- import { InvalidArgumentError } from './commander-error.js';
1
+ import {} from './argument.js';
2
+ import { InvalidArgumentError } from './error.js';
2
3
  export class Option {
3
4
  flags;
4
5
  description;
@@ -24,6 +25,7 @@ export class Option {
24
25
  this.description = description || '';
25
26
  this.required = flags.includes('<');
26
27
  this.optional = flags.includes('[');
28
+ // `<value,...>` describes custom splitting of one argument, not a variadic option.
27
29
  this.variadic = /\w\.\.\.[>\]]$/.test(flags);
28
30
  const { shortFlag, longFlag } = splitOptionFlags(flags);
29
31
  this.short = shortFlag;
@@ -36,6 +38,7 @@ export class Option {
36
38
  this.defaultValueDescription = description;
37
39
  return this;
38
40
  }
41
+ /** Value used when the option appears without an option-argument. `parseArg` still runs. */
39
42
  preset(arg) {
40
43
  this.presetArg = arg;
41
44
  return this;
@@ -44,6 +47,7 @@ export class Option {
44
47
  this.conflictsWith = this.conflictsWith.concat(names);
45
48
  return this;
46
49
  }
50
+ /** Values implied for other options when this one is set and they are not. `parseArg` does not run on them. */
47
51
  implies(impliedOptionValues) {
48
52
  const newImplied = typeof impliedOptionValues === 'string' ? { [impliedOptionValues]: true } : impliedOptionValues;
49
53
  this.implied = Object.assign(this.implied ?? {}, newImplied);
@@ -86,6 +90,7 @@ export class Option {
86
90
  return this.long.replace(/^--/, '');
87
91
  return (this.short ?? '').replace(/^-/, '');
88
92
  }
93
+ /** camelCase key used on the options object; `--no-foo` shares `foo` with `--foo`. */
89
94
  attributeName() {
90
95
  return camelcase(this.negate ? this.name().replace(/^no-/, '') : this.name());
91
96
  }
@@ -96,10 +101,15 @@ export class Option {
96
101
  is(arg) {
97
102
  return this.short === arg || this.long === arg;
98
103
  }
104
+ /** Options are one of boolean, negated, required-argument or optional-argument. */
99
105
  isBoolean() {
100
106
  return !this.required && !this.optional && !this.negate;
101
107
  }
102
108
  }
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
+ */
103
113
  export class DualOptions {
104
114
  positiveOptions = new Map();
105
115
  negativeOptions = new Map();
@@ -125,6 +135,7 @@ export class DualOptions {
125
135
  function camelcase(str) {
126
136
  return str.split('-').reduce((acc, word) => acc + (word[0] ?? '').toUpperCase() + word.slice(1));
127
137
  }
138
+ /** Split `'-m,--mixed <value>'` into its short and long flags, failing noisily on anything else. */
128
139
  export function splitOptionFlags(flags) {
129
140
  let shortFlag;
130
141
  let longFlag;
@@ -136,8 +147,10 @@ export function splitOptionFlags(flags) {
136
147
  shortFlag = flagParts.shift();
137
148
  if (longFlagExp.test(head()))
138
149
  longFlag = flagParts.shift();
150
+ // Long then short. Rarely used but fine.
139
151
  if (!shortFlag && shortFlagExp.test(head()))
140
152
  shortFlag = flagParts.shift();
153
+ // Two long flags, like '--ws, --workspace': the supported way to have a shortish flag.
141
154
  if (!shortFlag && longFlagExp.test(head())) {
142
155
  shortFlag = longFlag;
143
156
  longFlag = flagParts.shift();
@@ -162,3 +175,4 @@ export function splitOptionFlags(flags) {
162
175
  }
163
176
  return { shortFlag, longFlag };
164
177
  }
178
+ //# sourceMappingURL=option.js.map
@@ -3,14 +3,14 @@
3
3
  * dependency on commander itself). Graded by commander's own suite; see
4
4
  * `.sdlc/intents/commander-compat/design.md` and `npm run compat`.
5
5
  */
6
- import { Argument } from './commander-argument.js';
7
- import { Command } from './commander-command.js';
8
- import { Option } from './commander-option.js';
9
- export { Argument, humanReadableArgName } from './commander-argument.js';
10
- export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HookEvent, type HookListener, type OutputConfiguration, type OutputContext, type ParseOptions, } from './commander-command.js';
11
- export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander-error.js';
12
- export { Help, type HelpContext } from './commander-help.js';
13
- export { DualOptions, Option } from './commander-option.js';
6
+ import { Argument } from './commander/argument.js';
7
+ import { Command } from './commander/command.js';
8
+ import { Option } from './commander/option.js';
9
+ export { Argument, humanReadableArgName } from './commander/argument.js';
10
+ export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HookEvent, type HookListener, type OutputConfiguration, type OutputContext, type ParseOptions, } from './commander/command.js';
11
+ export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander/error.js';
12
+ export { Help, type HelpContext } from './commander/help.js';
13
+ export { DualOptions, Option } from './commander/option.js';
14
14
  /** The root command, for programs that never construct their own. */
15
15
  export declare const program: Command;
16
16
  export declare const createCommand: (name?: string) => Command;
package/dist/commander.js CHANGED
@@ -1,11 +1,11 @@
1
- import { Argument } from './commander-argument.js';
2
- import { Command } from './commander-command.js';
3
- import { Option } from './commander-option.js';
4
- export { Argument, humanReadableArgName } from './commander-argument.js';
5
- export { Command, useColor, } from './commander-command.js';
6
- export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander-error.js';
7
- export { Help } from './commander-help.js';
8
- export { DualOptions, Option } from './commander-option.js';
1
+ import { Argument } from './commander/argument.js';
2
+ import { Command } from './commander/command.js';
3
+ import { Option } from './commander/option.js';
4
+ export { Argument, humanReadableArgName } from './commander/argument.js';
5
+ export { Command, useColor, } from './commander/command.js';
6
+ export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander/error.js';
7
+ export { Help } from './commander/help.js';
8
+ export { DualOptions, Option } from './commander/option.js';
9
9
  export const program = new Command();
10
10
  export const createCommand = (name) => new Command(name);
11
11
  export const createOption = (flags, description) => new Option(flags, description);
@@ -14,7 +14,9 @@ function children(manifest, at) {
14
14
  .map((c) => nodeOf(manifest, c));
15
15
  }
16
16
  function nodeOf(manifest, c) {
17
- const visible = Object.fromEntries(Object.entries(c.options).filter(([, spec]) => spec.hidden !== true));
17
+ const visible = Object.fromEntries(Object.entries(c.options)
18
+ .filter(([, spec]) => spec.hidden !== true)
19
+ .map(([name, spec]) => [name, spec.type === 'boolean' ? { ...spec, negatable: true } : spec]));
18
20
  const isRoot = c.path.length === manifest.rootPath.length;
19
21
  return {
20
22
  path: c.path.slice(manifest.rootPath.length),
@@ -40,7 +42,11 @@ function walk(root) {
40
42
  return out;
41
43
  }
42
44
  const sq = (s) => `'${s.replaceAll("'", "'\\''")}'`;
43
- const flags = (name, spec) => (spec.short === undefined ? [`--${kebab(name)}`] : [`-${spec.short}`, `--${kebab(name)}`]);
45
+ const flags = (name, spec) => {
46
+ const long = `--${kebab(name)}`;
47
+ const spellings = spec.short === undefined ? [long] : [`-${spec.short}`, long];
48
+ return spec.negatable === true ? [...spellings, `--no-${kebab(name)}`] : spellings;
49
+ };
44
50
  const takesValue = (spec) => spec.type !== 'boolean';
45
51
  const fname = (program, path) => `_${[program, ...path].join('_').replaceAll(/[^A-Za-z0-9_]/g, '_')}`;
46
52
  function bashCase(program, node) {
@@ -141,8 +147,13 @@ function fish(program, root) {
141
147
  const cond = sq(fishCondition(node));
142
148
  for (const c of node.children)
143
149
  lines.push(`complete -c ${program} -n ${cond} -f -a ${c.path[c.path.length - 1] ?? ''} -d ${sq(c.description)}`);
144
- for (const [name, spec] of Object.entries(node.options))
145
- lines.push(fishOption(program, cond, name, spec));
150
+ for (const [name, spec] of Object.entries(node.options)) {
151
+ lines.push(fishOption(program, cond, kebab(name), spec));
152
+ if (spec.negatable === true) {
153
+ const { short: _short, ...unshort } = spec;
154
+ lines.push(fishOption(program, cond, `no-${kebab(name)}`, unshort));
155
+ }
156
+ }
146
157
  }
147
158
  return `# ${program} completion for fish, generated by burgee — never runs ${program} on TAB.
148
159
  # Install: ${program} completion fish > ~/.config/fish/completions/${program}.fish
@@ -10,18 +10,14 @@
10
10
  * WCAG 2.2 sets 4.5:1 for body text and 3:1 for large text and for the parts of
11
11
  * a graphic you need in order to understand it. A logo's bars are the latter, so
12
12
  * 3:1 is the floor used here — see `AA`.
13
+ *
14
+ * The measuring is `roundel`'s, not ours. Colour is the layer below this one, and until
15
+ * 2026-09-09 both packages carried the same forty lines of WCAG luminance — identical
16
+ * constants, identical maths, differing only in which package name the hex error says.
17
+ * What is left here is the part that is actually about a burgee: which pairs of a flag's
18
+ * own colours have to clear the floor, and how to say so to a person running `burgee brand`.
13
19
  */
14
- /** The floors WCAG 2.2 sets, as ratios. */
15
- export declare const AA: {
16
- /** Body text against its background. */
17
- readonly TEXT: 4.5;
18
- /** Large text, UI components, and meaningful parts of a graphic. */
19
- readonly GRAPHIC: 3;
20
- };
21
- /** WCAG relative luminance. */
22
- export declare function luminance(hex: string): number;
23
- /** The WCAG contrast ratio between two colours. Order does not matter. */
24
- export declare function contrast(a: string, b: string): number;
20
+ import { AA, contrast, luminance } from 'roundel/contrast';
25
21
  /** Mix two colours in sRGB. Enough for reading a gradient stop, not for colour science. */
26
22
  export declare function mix(a: string, b: string, t: number): string;
27
23
  export interface ContrastFinding {
@@ -68,3 +64,5 @@ export interface AuditInput {
68
64
  * intrinsic pair and fails a ground has a page problem, not a logo problem.
69
65
  */
70
66
  export declare function auditBurgee(brand: AuditInput, grounds?: readonly string[]): ContrastFinding[];
67
+ /** Re-exported so a caller reading a burgee's contrast needs one import, not two. */
68
+ export { AA, contrast, luminance };
package/dist/contrast.js CHANGED
@@ -1,38 +1,6 @@
1
- export const AA = {
2
- TEXT: 4.5,
3
- GRAPHIC: 3,
4
- };
1
+ import { AA, channels, contrast, luminance } from 'roundel/contrast';
5
2
  const SRGB_MAX = 255;
6
- const LINEAR_THRESHOLD = 0.03928;
7
- const LINEAR_DIVISOR = 12.92;
8
- const GAMMA_OFFSET = 0.055;
9
- const GAMMA_SCALE = 1.055;
10
- const GAMMA_EXPONENT = 2.4;
11
- const LUMA = { r: 0.2126, g: 0.7152, b: 0.0722 };
12
- const CONTRAST_OFFSET = 0.05;
13
- const RED_AT = 1;
14
- const GREEN_AT = 3;
15
- const BLUE_AT = 5;
16
- const HEX_PAIRS = [RED_AT, GREEN_AT, BLUE_AT];
17
3
  const HEX_RADIX = 16;
18
- const SHORT_HEX_LENGTH = 4;
19
- function channels(hex) {
20
- const full = hex.length === SHORT_HEX_LENGTH
21
- ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`
22
- : hex;
23
- if (!/^#[0-9a-fA-F]{6}$/.test(full))
24
- throw new Error(`burgee: "${hex}" is not a hex colour`);
25
- const parsed = HEX_PAIRS.map((i) => Number.parseInt(full.slice(i, i + 2), HEX_RADIX) / SRGB_MAX);
26
- return parsed;
27
- }
28
- export function luminance(hex) {
29
- const [r, g, b] = channels(hex).map((v) => v <= LINEAR_THRESHOLD ? v / LINEAR_DIVISOR : ((v + GAMMA_OFFSET) / GAMMA_SCALE) ** GAMMA_EXPONENT);
30
- return LUMA.r * r + LUMA.g * g + LUMA.b * b;
31
- }
32
- export function contrast(a, b) {
33
- const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
34
- return (hi + CONTRAST_OFFSET) / (lo + CONTRAST_OFFSET);
35
- }
36
4
  export function mix(a, b, t) {
37
5
  const [ca, cb] = [channels(a), channels(b)];
38
6
  const hex = ca
@@ -91,3 +59,4 @@ export function auditBurgee(brand, grounds = []) {
91
59
  }
92
60
  return findings;
93
61
  }
62
+ export { AA, contrast, luminance };
package/dist/execute.js CHANGED
@@ -8,7 +8,7 @@ import { serveMcp } from './mcp.js';
8
8
  import { camel, kebab } from './names.js';
9
9
  import { nearestPackage } from './pkg.js';
10
10
  import { ConfigError, explain, resolve as resolveLayers } from './precedence.js';
11
- import { commandSchemaOf, schemaOf, summaryOf } from './schema.js';
11
+ import { commandSchemaOf, machineJson, schemaOf, summaryOf } from './schema.js';
12
12
  import { checkDefinition, checkRelations, coerce, UsageError } from './validate.js';
13
13
  function helpFields(c) {
14
14
  const node = {};
@@ -116,6 +116,7 @@ function render(value) {
116
116
  }
117
117
  return String(value);
118
118
  }
119
+ const NO = 'no-';
119
120
  function toParseConfig(specs, withConfig) {
120
121
  const config = { json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' }, explain: { type: 'string' } };
121
122
  if (withConfig) {
@@ -128,11 +129,28 @@ function toParseConfig(specs, withConfig) {
128
129
  ...(spec.short === undefined ? {} : { short: spec.short }),
129
130
  ...(spec.multiple === true ? { multiple: true } : {}),
130
131
  };
132
+ if (spec.type === 'boolean')
133
+ config[`${NO}${kebab(name)}`] = { type: 'boolean' };
131
134
  }
132
135
  return config;
133
136
  }
134
- function canonical(values) {
135
- return Object.fromEntries(Object.entries(values).map(([k, v]) => [camel(k), v]));
137
+ function canonical(values, specs, tokens) {
138
+ const out = {};
139
+ const negatable = (key) => {
140
+ const bare = camel(key.startsWith(NO) ? key.slice(NO.length) : key);
141
+ return specs[bare]?.type === 'boolean' ? bare : '';
142
+ };
143
+ for (const [k, v] of Object.entries(values))
144
+ if (!k.startsWith(NO) || negatable(k) === '')
145
+ out[camel(k)] = v;
146
+ for (const token of tokens) {
147
+ if (token.kind !== 'option')
148
+ continue;
149
+ const bare = negatable(token.name);
150
+ if (bare !== '')
151
+ out[bare] = !token.name.startsWith(NO);
152
+ }
153
+ return out;
136
154
  }
137
155
  function packageLayer(pkg, name) {
138
156
  if (pkg === undefined || name === undefined)
@@ -195,18 +213,7 @@ function exitSignal(cause) {
195
213
  const code = cause?.code;
196
214
  return typeof code === 'number' && isExitCode(code) ? code : undefined;
197
215
  }
198
- const SINGLE_DASH_WORD = /^-([a-zA-Z][\w-]+)(?:=.*)?$/;
199
- function singleDashHint(argv) {
200
- for (const token of argv) {
201
- if (token === '--')
202
- return undefined;
203
- const found = SINGLE_DASH_WORD.exec(token);
204
- if (found?.[1] !== undefined)
205
- return `did you mean --${found[1]}? a single dash introduces one-letter options`;
206
- }
207
- return undefined;
208
- }
209
- function describeFailure(cause, argv) {
216
+ async function describeFailure(cause, argv, node) {
210
217
  const signal = exitSignal(cause);
211
218
  if (signal !== undefined)
212
219
  return { code: signal, message: '', silent: true };
@@ -220,7 +227,12 @@ function describeFailure(cause, argv) {
220
227
  return { code: ExitCode.CONFIG, message, ...(cause.hint === undefined ? {} : { hint: cause.hint }) };
221
228
  }
222
229
  if (isParseArgsFailure(cause)) {
223
- return { code: ExitCode.USAGE, message, hint: singleDashHint(argv) ?? 'run --help to see the available options' };
230
+ const explain = await import('./unknown-option.js');
231
+ const dash = explain.singleDashHint(argv);
232
+ if (dash !== undefined)
233
+ return { code: ExitCode.USAGE, message, hint: dash };
234
+ const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}));
235
+ return { code: ExitCode.USAGE, message, hint: 'run --help to see the available options', ...better };
224
236
  }
225
237
  return { code: ExitCode.RUNTIME, message };
226
238
  }
@@ -246,13 +258,16 @@ export function beforeTerminator(argv) {
246
258
  function rootNode(manifest, root) {
247
259
  return manifest.find(root) ?? { path: root, options: {} };
248
260
  }
249
- function unresolved({ manifest, root, io: { width } }, argv, at) {
261
+ function unresolved({ manifest, root, io }, argv, at) {
250
262
  const node = at ?? rootNode(manifest, root);
251
263
  const typed = argv.slice(node.path.length - root.length);
252
- if (typed.length > 0 && HELP_FLAGS.has(typed[0] ?? ''))
253
- return { text: renderHelp(manifest, node, { width }), code: ExitCode.OK };
264
+ const first = typed[0] ?? '';
265
+ if (typed.length > 0 && HELP_FLAGS.has(first))
266
+ return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.OK };
267
+ if (first === '--version' || first === '-V')
268
+ return { text: `${versionOf(manifest, io)}\n`, code: ExitCode.OK };
254
269
  if (typed.length === 0)
255
- return { text: renderHelp(manifest, node, { width }), code: ExitCode.USAGE };
270
+ return { text: renderHelp(manifest, node, { width: io.width }), code: ExitCode.USAGE };
256
271
  throw new UsageError(`unknown command "${typed[0] ?? ''}"`, 'run --help to see the available commands');
257
272
  }
258
273
  async function completion(manifest, argv, io) {
@@ -279,7 +294,7 @@ async function surface(manifest, argv, io) {
279
294
  return true;
280
295
  }
281
296
  if (head.includes('--schema')) {
282
- io.out.write(`${JSON.stringify(schemaSurface(manifest, argv), null, 2)}\n`);
297
+ io.out.write(`${machineJson(schemaSurface(manifest, argv), head)}\n`);
283
298
  return true;
284
299
  }
285
300
  if (head[0] === '--mcp') {
@@ -305,7 +320,7 @@ async function surface(manifest, argv, io) {
305
320
  }
306
321
  const SCHEMA_BUDGET = 48_000;
307
322
  function schemaSurface(manifest, argv) {
308
- const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema'), manifest.rootPath);
323
+ const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema' && !a.startsWith('--format=')), manifest.rootPath);
309
324
  if (node?.run !== undefined)
310
325
  return commandSchemaOf(node, manifest.rootPath);
311
326
  const full = schemaOf(manifest);
@@ -333,7 +348,7 @@ function versionOf(manifest, io) {
333
348
  }
334
349
  async function dispatch(manifest, { node, rest, name }, io) {
335
350
  const parsed = parseArgs({ args: rest, options: toParseConfig(node.options, manifest.config !== undefined), allowPositionals: true, strict: true, tokens: true });
336
- const flags = canonical(parsed.values);
351
+ const flags = canonical(parsed.values, node.options, parsed.tokens);
337
352
  const json = flags.json === true;
338
353
  if (flags.help === true)
339
354
  return { json, text: renderHelp(manifest, node, { width: io.width }) };
@@ -389,7 +404,7 @@ function emit(io, outcome) {
389
404
  return io.exit(ExitCode.OK);
390
405
  }
391
406
  async function report(cause, { manifest, io, argv, json, name }) {
392
- const failure = describeFailure(cause, argv);
407
+ const failure = await describeFailure(cause, argv, resolveCommand(manifest, argv) ?? undefined);
393
408
  if (failure.silent === true)
394
409
  return io.exit(failure.code);
395
410
  await manifest.fire('onError', name, {});
package/dist/schema.d.ts CHANGED
@@ -42,10 +42,42 @@ export interface CommandSchema {
42
42
  plugin?: string;
43
43
  arguments: ArgumentSpec[];
44
44
  options: Record<string, OptionSpec>;
45
+ /**
46
+ * The constraints between options (S2/S6), which `validate.ts` already enforces and the
47
+ * schema did not publish. Without them an agent can only discover that `--a` conflicts
48
+ * with `--b` by sending both and reading exit 2 — a round trip per constraint, and under
49
+ * E1 an exit 2 means *rewrite the command*, so it may well send the same pair again.
50
+ *
51
+ * Omitted entirely when a command declares none, so a reader can tell "no constraints"
52
+ * from "constraints not published".
53
+ */
54
+ relations?: PublishedRelation[];
45
55
  examples: Example[];
46
56
  /** The arguments and options as one JSON Schema object — what an MCP tool call takes. */
47
57
  inputSchema: JsonSchema;
48
58
  }
59
+ /**
60
+ * A relation as JSON can carry it.
61
+ *
62
+ * `implies` takes either another option's name or a **predicate over the values**, and a
63
+ * function cannot be published. `JSON.stringify` turns it into `null` without a word, which
64
+ * would hand an agent `["force", null]` and let it conclude the constraint is malformed
65
+ * rather than unevaluable. So a predicate becomes the string `"(predicate)"`: the pair is
66
+ * still visible, and what is missing says so.
67
+ */
68
+ export type PublishedRelation = {
69
+ exactlyOneOf: readonly string[];
70
+ } | {
71
+ atLeastOneOf: readonly string[];
72
+ } | {
73
+ atMostOneOf: readonly string[];
74
+ } | {
75
+ conflicts: readonly string[];
76
+ } | {
77
+ implies: readonly [string, string];
78
+ };
79
+ /** The marker a predicate leaves behind. Not a name any option can have — it has parentheses. */
80
+ export declare const PREDICATE = "(predicate)";
49
81
  export interface ProgramSchema {
50
82
  schemaVersion: 1;
51
83
  name: string;
@@ -77,3 +109,5 @@ export interface SchemaSummary {
77
109
  /** Above the budget (N13): every command by name and summary, and the drilling command for one in full. */
78
110
  export declare function summaryOf(manifest: Manifest, budget: number): SchemaSummary;
79
111
  export declare function schemaOf(manifest: Manifest): ProgramSchema;
112
+ /** The machine document, compact unless a person asked for the readable one. */
113
+ export declare const machineJson: (value: unknown, head: readonly string[]) => string;
package/dist/schema.js CHANGED
@@ -1,4 +1,11 @@
1
1
  import { kebab } from './names.js';
2
+ export const PREDICATE = '(predicate)';
3
+ function publishable(relation) {
4
+ if (!('implies' in relation))
5
+ return relation;
6
+ const [option, consequent] = relation.implies;
7
+ return { implies: [option, typeof consequent === 'function' ? PREDICATE : consequent] };
8
+ }
2
9
  function argumentProperty(a) {
3
10
  const p = a.variadic === true ? { type: 'array', items: { type: 'string' } } : { type: 'string' };
4
11
  if (a.description !== undefined)
@@ -76,6 +83,8 @@ export function commandSchemaOf(node, root) {
76
83
  out.lazy = true;
77
84
  if (node.plugin !== undefined)
78
85
  out.plugin = node.plugin;
86
+ if (node.relations !== undefined && node.relations.length > 0)
87
+ out.relations = node.relations.map(publishable);
79
88
  return out;
80
89
  }
81
90
  export function runnable(manifest) {
@@ -114,3 +123,6 @@ export function schemaOf(manifest) {
114
123
  out.description = description;
115
124
  return out;
116
125
  }
126
+ const JSON_PRETTY = '--format=json-pretty';
127
+ const INDENT = 2;
128
+ export const machineJson = (value, head) => JSON.stringify(value, null, head.includes(JSON_PRETTY) ? INDENT : 0);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `-foo=bar` means three short flags to a parser and one long flag to a person
3
+ * (citty #237). The more specific reading of the same argv, so it is offered first.
4
+ */
5
+ export declare function singleDashHint(argv: readonly string[]): string | undefined;
6
+ export declare function unknownOption(cause: unknown, declared: readonly string[]): {
7
+ message: string;
8
+ hint: string;
9
+ } | undefined;
@@ -0,0 +1,27 @@
1
+ import { suggestSimilar } from './suggest.js';
2
+ const UNKNOWN_OPTION = /^Unknown option '(?<flag>[^']+)'/u;
3
+ const NEAREST = /--[\w-]+/u;
4
+ const SINGLE_DASH_WORD = /^-(?<word>[a-zA-Z][\w-]+)(?:=.*)?$/u;
5
+ export function singleDashHint(argv) {
6
+ for (const token of argv) {
7
+ if (token === '--')
8
+ return undefined;
9
+ const word = SINGLE_DASH_WORD.exec(token)?.groups?.['word'];
10
+ if (word !== undefined)
11
+ return `did you mean --${word}? a single dash introduces one-letter options`;
12
+ }
13
+ return undefined;
14
+ }
15
+ export function unknownOption(cause, declared) {
16
+ if (!(cause instanceof Error))
17
+ return undefined;
18
+ const flag = UNKNOWN_OPTION.exec(cause.message)?.groups?.['flag'];
19
+ if (flag === undefined)
20
+ return undefined;
21
+ const dashed = declared.map((option) => `--${option}`);
22
+ const near = NEAREST.exec(suggestSimilar(flag, dashed))?.[0];
23
+ return {
24
+ message: `unknown option ${flag}`,
25
+ hint: near === undefined ? 'run --help to see the available options' : `did you mean ${near}?`,
26
+ };
27
+ }
@@ -4,8 +4,8 @@
4
4
  * this module projects that snapshot into the manifest every surface reads (J7, J8).
5
5
  * Nothing here runs at parse time unless a burgee surface was asked for.
6
6
  */
7
- import { type Effects, type Manifest, type OptionSpec } from './manifest.js';
8
- import type { Positional } from './yargs-utils.js';
7
+ import { type Effects, type Manifest, type OptionSpec } from '../manifest.js';
8
+ import type { Positional } from './utils.js';
9
9
  /** What one yargs instance (the root, or a command's builder run on a scratch) registered. */
10
10
  export interface Snapshot {
11
11
  name: string;