burgee 0.0.0 → 0.3.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 (116) hide show
  1. package/README.md +28 -1
  2. package/dist/agent.d.ts +19 -0
  3. package/dist/agent.js +25 -0
  4. package/dist/brand.d.ts +186 -0
  5. package/dist/brand.js +232 -0
  6. package/dist/cli.d.ts +62 -0
  7. package/dist/cli.js +121 -0
  8. package/dist/commander-argument.d.ts +22 -0
  9. package/dist/commander-argument.js +72 -0
  10. package/dist/commander-command.d.ts +355 -0
  11. package/dist/commander-command.js +1621 -0
  12. package/dist/commander-error.d.ts +10 -0
  13. package/dist/commander-error.js +20 -0
  14. package/dist/commander-help.d.ts +67 -0
  15. package/dist/commander-help.js +319 -0
  16. package/dist/commander-option.d.ts +58 -0
  17. package/dist/commander-option.js +164 -0
  18. package/dist/commander-suggest.d.ts +2 -0
  19. package/dist/commander-suggest.js +59 -0
  20. package/dist/commander.d.ts +18 -0
  21. package/dist/commander.js +12 -0
  22. package/dist/completions.d.ts +39 -0
  23. package/dist/completions.js +224 -0
  24. package/dist/config.d.ts +28 -0
  25. package/dist/config.js +98 -0
  26. package/dist/contrast.d.ts +70 -0
  27. package/dist/contrast.js +93 -0
  28. package/dist/dev.d.ts +47 -0
  29. package/dist/dev.js +132 -0
  30. package/dist/execute.d.ts +118 -0
  31. package/dist/execute.js +469 -0
  32. package/dist/exit-code.d.ts +18 -0
  33. package/dist/exit-code.js +12 -0
  34. package/dist/help.d.ts +34 -0
  35. package/dist/help.js +187 -0
  36. package/dist/index.d.ts +16 -1
  37. package/dist/index.js +9 -2
  38. package/dist/manifest.d.ts +207 -0
  39. package/dist/manifest.js +66 -0
  40. package/dist/mcp.d.ts +52 -0
  41. package/dist/mcp.js +124 -0
  42. package/dist/names.d.ts +5 -0
  43. package/dist/names.js +6 -0
  44. package/dist/pkg.d.ts +5 -0
  45. package/dist/pkg.js +21 -0
  46. package/dist/precedence.d.ts +55 -0
  47. package/dist/precedence.js +100 -0
  48. package/dist/runtime.d.ts +39 -0
  49. package/dist/runtime.js +24 -0
  50. package/dist/schema.d.ts +79 -0
  51. package/dist/schema.js +116 -0
  52. package/dist/testing-helpers.d.ts +79 -0
  53. package/dist/testing-helpers.js +145 -0
  54. package/dist/testing.d.ts +10 -0
  55. package/dist/testing.js +3 -0
  56. package/dist/validate.d.ts +27 -0
  57. package/dist/validate.js +133 -0
  58. package/dist/yargs-burgee.d.ts +50 -0
  59. package/dist/yargs-burgee.js +104 -0
  60. package/dist/yargs-cliui.d.ts +56 -0
  61. package/dist/yargs-cliui.js +421 -0
  62. package/dist/yargs-command.d.ts +82 -0
  63. package/dist/yargs-command.js +414 -0
  64. package/dist/yargs-completion.d.ts +41 -0
  65. package/dist/yargs-completion.js +271 -0
  66. package/dist/yargs-factory.d.ts +193 -0
  67. package/dist/yargs-factory.js +1606 -0
  68. package/dist/yargs-helpers.d.ts +6 -0
  69. package/dist/yargs-helpers.js +2 -0
  70. package/dist/yargs-middleware.d.ts +32 -0
  71. package/dist/yargs-middleware.js +81 -0
  72. package/dist/yargs-parser.d.ts +41 -0
  73. package/dist/yargs-parser.js +929 -0
  74. package/dist/yargs-shim.d.ts +54 -0
  75. package/dist/yargs-shim.js +84 -0
  76. package/dist/yargs-usage.d.ts +42 -0
  77. package/dist/yargs-usage.js +479 -0
  78. package/dist/yargs-utils.d.ts +33 -0
  79. package/dist/yargs-utils.js +209 -0
  80. package/dist/yargs-validation.d.ts +26 -0
  81. package/dist/yargs-validation.js +261 -0
  82. package/dist/yargs-y18n.d.ts +21 -0
  83. package/dist/yargs-y18n.js +117 -0
  84. package/dist/yargs.d.ts +6 -0
  85. package/dist/yargs.js +8 -0
  86. package/locales/be.json +46 -0
  87. package/locales/cs.json +51 -0
  88. package/locales/de.json +46 -0
  89. package/locales/en.json +55 -0
  90. package/locales/es.json +46 -0
  91. package/locales/fi.json +49 -0
  92. package/locales/fr.json +53 -0
  93. package/locales/he.json +55 -0
  94. package/locales/hi.json +49 -0
  95. package/locales/hu.json +46 -0
  96. package/locales/id.json +50 -0
  97. package/locales/it.json +46 -0
  98. package/locales/ja.json +51 -0
  99. package/locales/ka.json +55 -0
  100. package/locales/ko.json +49 -0
  101. package/locales/nb.json +44 -0
  102. package/locales/nl.json +49 -0
  103. package/locales/nn.json +44 -0
  104. package/locales/pirate.json +13 -0
  105. package/locales/pl.json +49 -0
  106. package/locales/pt.json +45 -0
  107. package/locales/pt_BR.json +48 -0
  108. package/locales/ru.json +51 -0
  109. package/locales/th.json +46 -0
  110. package/locales/tr.json +48 -0
  111. package/locales/uk_UA.json +51 -0
  112. package/locales/uz.json +52 -0
  113. package/locales/zh_CN.json +48 -0
  114. package/locales/zh_TW.json +51 -0
  115. package/package.json +62 -9
  116. package/dist/index.js.map +0 -1
@@ -0,0 +1,59 @@
1
+ const maxDistance = 3;
2
+ function editDistance(a, b) {
3
+ if (Math.abs(a.length - b.length) > maxDistance)
4
+ return Math.max(a.length, b.length);
5
+ const d = [];
6
+ for (let i = 0; i <= a.length; i++)
7
+ d[i] = [i];
8
+ for (let j = 0; j <= b.length; j++)
9
+ (d[0] ??= [])[j] = j;
10
+ for (let j = 1; j <= b.length; j++) {
11
+ for (let i = 1; i <= a.length; i++) {
12
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
13
+ const row = d[i] ?? [];
14
+ const prev = d[i - 1] ?? [];
15
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
16
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
17
+ row[j] = Math.min(row[j] ?? 0, (d[i - 2]?.[j - 2] ?? 0) + 1);
18
+ }
19
+ }
20
+ }
21
+ return d[a.length]?.[b.length] ?? 0;
22
+ }
23
+ export function suggestSimilar(word, candidates) {
24
+ if (!candidates || candidates.length === 0)
25
+ return '';
26
+ candidates = Array.from(new Set(candidates));
27
+ const searchingOptions = word.startsWith('--');
28
+ if (searchingOptions) {
29
+ word = word.slice(2);
30
+ candidates = candidates.map((candidate) => candidate.slice(2));
31
+ }
32
+ let similar = [];
33
+ let bestDistance = maxDistance;
34
+ const minSimilarity = 0.4;
35
+ for (const candidate of candidates) {
36
+ if (candidate.length <= 1)
37
+ continue;
38
+ const distance = editDistance(word, candidate);
39
+ const length = Math.max(word.length, candidate.length);
40
+ const similarity = (length - distance) / length;
41
+ if (similarity > minSimilarity) {
42
+ if (distance < bestDistance) {
43
+ bestDistance = distance;
44
+ similar = [candidate];
45
+ }
46
+ else if (distance === bestDistance) {
47
+ similar.push(candidate);
48
+ }
49
+ }
50
+ }
51
+ similar.sort((a, b) => a.localeCompare(b));
52
+ if (searchingOptions)
53
+ similar = similar.map((candidate) => `--${candidate}`);
54
+ if (similar.length > 1)
55
+ return `\n(Did you mean one of ${similar.join(', ')}?)`;
56
+ if (similar.length === 1)
57
+ return `\n(Did you mean ${similar[0]}?)`;
58
+ return '';
59
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `burgee/commander` — commander's public surface, implemented over burgee (J9: no
3
+ * dependency on commander itself). Graded by commander's own suite; see
4
+ * `.sdlc/intents/commander-compat/design.md` and `npm run compat`.
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';
14
+ /** The root command, for programs that never construct their own. */
15
+ export declare const program: Command;
16
+ export declare const createCommand: (name?: string) => Command;
17
+ export declare const createOption: (flags: string, description?: string) => Option;
18
+ export declare const createArgument: (name: string, description?: string) => Argument;
@@ -0,0 +1,12 @@
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
+ export const program = new Command();
10
+ export const createCommand = (name) => new Command(name);
11
+ export const createOption = (flags, description) => new Option(flags, description);
12
+ export const createArgument = (name, description) => new Argument(name, description);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Completions, generated statically from the manifest (D2): bash, zsh, fish and
3
+ * PowerShell scripts that never run Node on TAB (D3), plus a Fig spec from the same walk
4
+ * (D5). Every script is deterministic for a manifest, so a snapshot pins it and the shell
5
+ * itself exercises it in CI (D4).
6
+ */
7
+ import { type Manifest, type OptionSpec } from './manifest.js';
8
+ export type Shell = 'bash' | 'zsh' | 'fish' | 'pwsh';
9
+ export declare const SHELLS: readonly Shell[];
10
+ interface Node {
11
+ /** Typed path, e.g. `['config', 'get']`; empty for the root. */
12
+ path: string[];
13
+ description: string;
14
+ options: Record<string, OptionSpec>;
15
+ children: Node[];
16
+ }
17
+ /** The tree the templates walk; the root's own name is the program name. */
18
+ export declare function completionTree(manifest: Manifest): {
19
+ program: string;
20
+ root: Node;
21
+ };
22
+ interface FigOption {
23
+ name: string[];
24
+ description?: string;
25
+ args?: {
26
+ name: string;
27
+ suggestions?: string[];
28
+ };
29
+ }
30
+ interface FigSubcommand {
31
+ name: string;
32
+ description?: string;
33
+ subcommands?: FigSubcommand[];
34
+ options?: FigOption[];
35
+ }
36
+ /** A Fig spec (`Fig.Spec`) from the same walk (D5): one entry per node, children attached by path. */
37
+ export declare function renderFigSpec(manifest: Manifest): FigSubcommand;
38
+ export declare function renderCompletion(manifest: Manifest, shell: Shell): string;
39
+ export {};
@@ -0,0 +1,224 @@
1
+ import { kebab } from './names.js';
2
+ export const SHELLS = ['bash', 'zsh', 'fish', 'pwsh'];
3
+ const GLOBAL = {
4
+ json: { type: 'boolean', description: 'machine-readable output' },
5
+ help: { type: 'boolean', description: 'show this help' },
6
+ };
7
+ const ROOT_ONLY = {
8
+ schema: { type: 'boolean', description: 'the program as data' },
9
+ mcp: { type: 'boolean', description: 'serve the program as MCP tools' },
10
+ };
11
+ function children(manifest, at) {
12
+ return manifest.commands
13
+ .filter((c) => c.hidden !== true && c.path.length === at.length + 1 && at.every((seg, i) => c.path[i] === seg))
14
+ .map((c) => nodeOf(manifest, c));
15
+ }
16
+ function nodeOf(manifest, c) {
17
+ const visible = Object.fromEntries(Object.entries(c.options).filter(([, spec]) => spec.hidden !== true));
18
+ const isRoot = c.path.length === manifest.rootPath.length;
19
+ return {
20
+ path: c.path.slice(manifest.rootPath.length),
21
+ description: c.summary ?? c.description ?? '',
22
+ options: { ...visible, ...GLOBAL, ...(isRoot ? ROOT_ONLY : {}) },
23
+ children: children(manifest, c.path),
24
+ };
25
+ }
26
+ export function completionTree(manifest) {
27
+ const root = manifest.find(manifest.rootPath) ?? { path: manifest.rootPath, options: {} };
28
+ return { program: manifest.rootPath.join('-') || 'program', root: nodeOf(manifest, root) };
29
+ }
30
+ function walk(root) {
31
+ const out = [];
32
+ const stack = [root];
33
+ while (stack.length > 0) {
34
+ const node = stack.pop();
35
+ if (node !== undefined) {
36
+ out.push(node);
37
+ stack.push(...[...node.children].reverse());
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ const sq = (s) => `'${s.replaceAll("'", "'\\''")}'`;
43
+ const flags = (name, spec) => (spec.short === undefined ? [`--${kebab(name)}`] : [`-${spec.short}`, `--${kebab(name)}`]);
44
+ const takesValue = (spec) => spec.type !== 'boolean';
45
+ const fname = (program, path) => `_${[program, ...path].join('_').replaceAll(/[^A-Za-z0-9_]/g, '_')}`;
46
+ function bashCase(program, node) {
47
+ const opts = Object.entries(node.options).flatMap(([n, s]) => flags(n, s));
48
+ const subs = node.children.map((c) => c.path[c.path.length - 1] ?? '');
49
+ const valued = Object.entries(node.options)
50
+ .filter(([, s]) => takesValue(s))
51
+ .map(([n, s]) => ` ${flags(n, s).join('|')}) COMPREPLY=($(compgen -W ${sq((s.choices ?? []).join(' '))} -- "$cur")); return ;;`)
52
+ .join('\n');
53
+ return ` ${fname(program, node.path)}() {
54
+ case "$prev" in
55
+ ${valued === '' ? ' *) ;;' : `${valued}\n *) ;;`}
56
+ esac
57
+ if [[ "$cur" == -* ]]; then COMPREPLY=($(compgen -W ${sq(opts.join(' '))} -- "$cur")); return; fi
58
+ COMPREPLY=($(compgen -W ${sq(subs.join(' '))} -- "$cur"))
59
+ }`;
60
+ }
61
+ function bash(program, root) {
62
+ const nodes = walk(root);
63
+ const dispatch = nodes
64
+ .filter((n) => n.path.length > 0)
65
+ .map((n) => ` ${sq(n.path.join(' '))}) ${fname(program, n.path)}; return ;;`)
66
+ .join('\n');
67
+ return `# ${program} completion for bash, generated by burgee — never runs ${program} on TAB.
68
+ # Install: ${program} completion bash > ~/.local/share/bash-completion/completions/${program}
69
+ _${program}_main() {
70
+ local cur prev words cword
71
+ _get_comp_words_by_ref -n : cur prev words cword 2>/dev/null || { cur="\${COMP_WORDS[COMP_CWORD]}"; prev="\${COMP_WORDS[COMP_CWORD-1]}"; words=("\${COMP_WORDS[@]}"); cword=$COMP_CWORD; }
72
+ ${nodes.map((n) => bashCase(program, n)).join('\n')}
73
+ # The deepest command named by the words before the cursor decides what to offer.
74
+ local path="" i
75
+ for ((i = 1; i < cword; i++)); do
76
+ case "\${words[i]}" in -*) continue ;; esac
77
+ case "\${path:+$path }\${words[i]}" in
78
+ ${nodes
79
+ .filter((n) => n.path.length > 0)
80
+ .map((n) => ` ${sq(n.path.join(' '))}) path="\${path:+$path }\${words[i]}" ;;`)
81
+ .join('\n')}
82
+ esac
83
+ done
84
+ case "$path" in
85
+ ${dispatch}
86
+ *) ${fname(program, [])} ;;
87
+ esac
88
+ }
89
+ complete -F _${program}_main ${program}
90
+ `;
91
+ }
92
+ function zshOptionSpec(name, spec) {
93
+ const desc = (spec.description ?? '').replaceAll(/[[\]:]/g, ' ');
94
+ const action = spec.choices === undefined ? ' ' : `(${spec.choices.join(' ')})`;
95
+ const value = takesValue(spec) ? `:${name}:${action}` : '';
96
+ return flags(name, spec)
97
+ .map((f) => sq(`${f}[${desc}]${value}`))
98
+ .join(' ');
99
+ }
100
+ function zshFunction(program, node) {
101
+ const options = Object.entries(node.options).map(([n, s]) => zshOptionSpec(n, s));
102
+ if (node.children.length === 0) {
103
+ return `${fname(program, node.path)}() {\n _arguments -s ${options.join(' ')} '*: :_files'\n}`;
104
+ }
105
+ const subs = node.children.map((c) => sq(`${c.path[c.path.length - 1] ?? ''}[${c.description.replaceAll(/[[\]:]/g, ' ')}]`)).join(' ');
106
+ const dispatch = node.children.map((c) => ` ${c.path[c.path.length - 1] ?? ''}) ${fname(program, c.path)} ;;`).join('\n');
107
+ return `${fname(program, node.path)}() {
108
+ local context state state_descr line
109
+ typeset -A opt_args
110
+ _arguments -s -C ${options.join(' ')} '1: :->command' '*:: :->args'
111
+ case $state in
112
+ command) _values 'command' ${subs} ;;
113
+ args) case $line[1] in
114
+ ${dispatch}
115
+ esac ;;
116
+ esac
117
+ }`;
118
+ }
119
+ function zsh(program, root) {
120
+ return `#compdef ${program}
121
+ # ${program} completion for zsh, generated by burgee — never runs ${program} on TAB.
122
+ # Install: ${program} completion zsh > "\${fpath[1]}/_${program}" && compinit
123
+ ${walk(root).map((n) => zshFunction(program, n)).join('\n')}
124
+ if [[ $zsh_eval_context[-1] == loadautofunc ]]; then ${fname(program, [])} "$@"; else compdef ${fname(program, [])} ${program}; fi
125
+ `;
126
+ }
127
+ function fishCondition(node) {
128
+ if (node.path.length === 0)
129
+ return '__fish_use_subcommand';
130
+ return node.path.map((seg) => `__fish_seen_subcommand_from ${seg}`).join('; and ');
131
+ }
132
+ function fishOption(program, cond, name, spec) {
133
+ const short = spec.short === undefined ? '' : ` -s ${spec.short}`;
134
+ const choices = spec.choices === undefined ? '' : ` -f -a ${sq(spec.choices.join(' '))}`;
135
+ const value = takesValue(spec) ? ` -r${choices}` : '';
136
+ return `complete -c ${program} -n ${cond}${short} -l ${name}${value} -d ${sq(spec.description ?? '')}`;
137
+ }
138
+ function fish(program, root) {
139
+ const lines = [];
140
+ for (const node of walk(root)) {
141
+ const cond = sq(fishCondition(node));
142
+ for (const c of node.children)
143
+ 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));
146
+ }
147
+ return `# ${program} completion for fish, generated by burgee — never runs ${program} on TAB.
148
+ # Install: ${program} completion fish > ~/.config/fish/completions/${program}.fish
149
+ ${lines.join('\n')}
150
+ `;
151
+ }
152
+ function pwshEntry(node) {
153
+ const subs = node.children.map((c) => `@('${c.path[c.path.length - 1] ?? ''}', 'Command', '${c.description.replaceAll("'", "''")}')`);
154
+ const opts = Object.entries(node.options).flatMap(([n, s]) => flags(n, s).map((f) => `@('${f}', 'ParameterName', '${(s.description ?? '').replaceAll("'", "''")}')`));
155
+ const values = Object.entries(node.options)
156
+ .filter(([, s]) => s.choices !== undefined)
157
+ .map(([n, s]) => `'--${n}' = @(${(s.choices ?? []).map((c) => `'${c}'`).join(', ')})`);
158
+ return ` '${node.path.join(' ')}' = @{ Items = @(${[...subs, ...opts].join(', ')}); Values = @{ ${values.join('; ')} } }`;
159
+ }
160
+ function pwsh(program, root) {
161
+ return `# ${program} completion for PowerShell, generated by burgee — never runs ${program} on TAB.
162
+ # Install: ${program} completion pwsh > ${program}-completion.ps1, then dot-source it from $PROFILE
163
+ Register-ArgumentCompleter -Native -CommandName '${program}' -ScriptBlock {
164
+ param($wordToComplete, $commandAst, $cursorPosition)
165
+ $table = @{
166
+ ${walk(root).map(pwshEntry).join('\n')}
167
+ }
168
+ $words = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text } | Select-Object -Skip 1)
169
+ if ($wordToComplete -ne '' -and $words.Count -gt 0) { $words = $words[0..($words.Count - 2)] }
170
+ $path = ''
171
+ foreach ($w in $words) {
172
+ if ($w -like '-*') { continue }
173
+ $candidate = if ($path -eq '') { $w } else { "$path $w" }
174
+ if ($table.ContainsKey($candidate)) { $path = $candidate }
175
+ }
176
+ $entry = $table[$path]
177
+ $prev = if ($words.Count -gt 0) { $words[$words.Count - 1] } else { '' }
178
+ if ($entry.Values.ContainsKey($prev)) {
179
+ $entry.Values[$prev] | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
180
+ return
181
+ }
182
+ $entry.Items | Where-Object { $_[0] -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_[0], $_[0], $_[1], $(if ($_[2] -eq '') { $_[0] } else { $_[2] })) }
183
+ }
184
+ `;
185
+ }
186
+ function figOptions(node) {
187
+ return Object.entries(node.options).map(([n, s]) => {
188
+ const o = { name: flags(n, s) };
189
+ if (s.description !== undefined)
190
+ o.description = s.description;
191
+ if (takesValue(s))
192
+ o.args = { name: s.placeholder ?? 'value', ...(s.choices === undefined ? {} : { suggestions: [...s.choices] }) };
193
+ return o;
194
+ });
195
+ }
196
+ export function renderFigSpec(manifest) {
197
+ const { program, root } = completionTree(manifest);
198
+ const entries = new Map();
199
+ for (const node of walk(root)) {
200
+ const entry = { name: node.path[node.path.length - 1] ?? program, options: figOptions(node) };
201
+ if (node.description !== '')
202
+ entry.description = node.description;
203
+ entries.set(node.path.join(' '), entry);
204
+ if (node.path.length === 0)
205
+ continue;
206
+ const parent = entries.get(node.path.slice(0, -1).join(' '));
207
+ if (parent !== undefined)
208
+ (parent.subcommands ??= []).push(entry);
209
+ }
210
+ return entries.get('') ?? { name: program, options: [] };
211
+ }
212
+ export function renderCompletion(manifest, shell) {
213
+ const { program, root } = completionTree(manifest);
214
+ switch (shell) {
215
+ case 'bash':
216
+ return bash(program, root);
217
+ case 'zsh':
218
+ return zsh(program, root);
219
+ case 'fish':
220
+ return fish(program, root);
221
+ case 'pwsh':
222
+ return pwsh(program, root);
223
+ }
224
+ }
@@ -0,0 +1,28 @@
1
+ import { type Layer } from './precedence.js';
2
+ export interface Discovery {
3
+ name: string;
4
+ cwd: string;
5
+ env: Record<string, string | undefined>;
6
+ /** `--config <path>`. */
7
+ explicit?: string;
8
+ /** `--no-config`. */
9
+ disabled?: boolean;
10
+ }
11
+ export interface Loaded extends Layer {
12
+ /** The files merged, outermost first — what `--explain` shows. */
13
+ chain: string[];
14
+ }
15
+ /** Objects merge recursively; anything else, the later value wins. */
16
+ export declare function deepMerge(base: Record<string, unknown>, over: Record<string, unknown>): Record<string, unknown>;
17
+ /** Load a file and everything it extends, outermost first, the file's own keys winning. */
18
+ export declare function loadWithExtends(path: string, seen?: string[]): Promise<Loaded>;
19
+ /** The discovery order as candidate paths, first hit wins; each entry says why it was tried. */
20
+ export declare function candidates(d: Discovery): {
21
+ path: string;
22
+ reason: string;
23
+ }[];
24
+ /**
25
+ * Discover and load. `package.json#<name>` is not a file here — the engine reads the
26
+ * owning package.json itself and treats the field as its own layer, below config.
27
+ */
28
+ export declare function discover(d: Discovery): Promise<Loaded | undefined>;
package/dist/config.js ADDED
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { ConfigError } from './precedence.js';
6
+ const EXTENSIONS = ['.json', '.mjs', '.js', '.cjs'];
7
+ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
8
+ export function deepMerge(base, over) {
9
+ const out = new Map(Object.entries(base));
10
+ for (const [k, v] of Object.entries(over)) {
11
+ const prev = out.get(k);
12
+ out.set(k, isObject(prev) && isObject(v) ? deepMerge(prev, v) : v);
13
+ }
14
+ return Object.fromEntries(out);
15
+ }
16
+ async function loadFile(path) {
17
+ if (path.endsWith('.json')) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
20
+ if (!isObject(parsed))
21
+ throw new ConfigError(`${path} must contain an object`);
22
+ return parsed;
23
+ }
24
+ catch (cause) {
25
+ if (cause instanceof ConfigError)
26
+ throw cause;
27
+ throw new ConfigError(`${path} is not valid JSON`, cause instanceof Error ? cause.message : undefined);
28
+ }
29
+ }
30
+ const mod = (await import(pathToFileURL(path).href));
31
+ const value = typeof mod.default === 'function' ? await mod.default() : mod.default;
32
+ if (!isObject(value))
33
+ throw new ConfigError(`${path} must export an object (or a function returning one)`);
34
+ return value;
35
+ }
36
+ function resolveExtends(spec, from) {
37
+ if (spec.startsWith('.') || isAbsolute(spec))
38
+ return resolve(dirname(from), spec);
39
+ try {
40
+ return createRequire(from).resolve(spec);
41
+ }
42
+ catch {
43
+ throw new ConfigError(`${from} extends "${spec}", which cannot be resolved`, 'use a relative path or an installed package');
44
+ }
45
+ }
46
+ function extendsList(value) {
47
+ if (typeof value === 'string')
48
+ return [value];
49
+ return Array.isArray(value) ? value.map(String) : [];
50
+ }
51
+ export async function loadWithExtends(path, seen = []) {
52
+ if (seen.includes(path))
53
+ throw new ConfigError(`config extends itself: ${[...seen, path].join(' → ')}`);
54
+ const own = await loadFile(path);
55
+ const parents = own['extends'];
56
+ const specs = extendsList(parents);
57
+ let data = {};
58
+ const chain = [];
59
+ for (const spec of specs) {
60
+ const parent = await loadWithExtends(resolveExtends(spec, path), [...seen, path]);
61
+ data = deepMerge(data, parent.data);
62
+ chain.push(...parent.chain);
63
+ }
64
+ const { extends: _ignored, ...rest } = own;
65
+ return { path, data: deepMerge(data, rest), chain: [...chain, path] };
66
+ }
67
+ const userConfigDir = (env) => {
68
+ const base = env['XDG_CONFIG_HOME'] ?? (env['HOME'] === undefined ? undefined : join(env['HOME'], '.config'));
69
+ return base;
70
+ };
71
+ export function candidates(d) {
72
+ const out = [];
73
+ const fromEnv = d.env[`${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG`];
74
+ if (fromEnv !== undefined)
75
+ out.push({ path: resolve(d.cwd, fromEnv), reason: `${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG` });
76
+ for (const ext of EXTENSIONS)
77
+ out.push({ path: join(d.cwd, `${d.name}.config${ext}`), reason: 'current directory' });
78
+ const user = userConfigDir(d.env);
79
+ if (user !== undefined)
80
+ out.push({ path: join(user, d.name, 'config.json'), reason: 'user config directory' });
81
+ return out;
82
+ }
83
+ export async function discover(d) {
84
+ if (d.disabled === true)
85
+ return undefined;
86
+ if (d.explicit !== undefined) {
87
+ const path = resolve(d.cwd, d.explicit);
88
+ if (!existsSync(path))
89
+ throw new ConfigError(`config file not found: ${path}`, 'check --config, or drop it to use discovery');
90
+ return await loadWithExtends(path);
91
+ }
92
+ const found = candidates(d)
93
+ .map((c) => c.path)
94
+ .find((path) => existsSync(path));
95
+ if (found === undefined)
96
+ return undefined;
97
+ return await loadWithExtends(found);
98
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Contrast, as a thing the package checks rather than a thing someone remembers.
3
+ *
4
+ * A burgee is a mark whose charge sits ON its own field, and whose field sits on
5
+ * a page nobody here controls. Both of those are contrast relationships, and
6
+ * both are easy to get wrong in a way that looks fine at 512px and fails at 24.
7
+ * This module measures them, so `burgee brand` can refuse to emit a flag that
8
+ * would not clear the floor and CI can hold ours to the same line.
9
+ *
10
+ * WCAG 2.2 sets 4.5:1 for body text and 3:1 for large text and for the parts of
11
+ * a graphic you need in order to understand it. A logo's bars are the latter, so
12
+ * 3:1 is the floor used here — see `AA`.
13
+ */
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;
25
+ /** Mix two colours in sRGB. Enough for reading a gradient stop, not for colour science. */
26
+ export declare function mix(a: string, b: string, t: number): string;
27
+ export interface ContrastFinding {
28
+ /** What was compared, in the words someone fixing it would use. */
29
+ what: string;
30
+ a: string;
31
+ b: string;
32
+ ratio: number;
33
+ required: number;
34
+ passes: boolean;
35
+ }
36
+ export declare function ratio(a: string, b: string): number;
37
+ export declare function check(what: string, a: string, b: string, required?: 3): ContrastFinding;
38
+ /** One line per finding, aligned, for a terminal or a failing test. */
39
+ export declare function report(findings: readonly ContrastFinding[]): string;
40
+ /** The field colour at a point along the gradient axis, for stops given in order. */
41
+ export declare function fieldColorAt(stops: ReadonlyArray<{
42
+ offset: number;
43
+ color: string;
44
+ }>, at: number): string;
45
+ export interface AuditInput {
46
+ mark: {
47
+ lead: string;
48
+ follow: string;
49
+ };
50
+ field: ReadonlyArray<{
51
+ offset: number;
52
+ color: string;
53
+ }>;
54
+ bordure?: {
55
+ color: string;
56
+ width: number;
57
+ } | ReadonlyArray<{
58
+ color: string;
59
+ width: number;
60
+ }>;
61
+ }
62
+ /**
63
+ * Every contrast relationship a burgee has to survive.
64
+ *
65
+ * Two are intrinsic — each bar against the field beneath it — and hold wherever
66
+ * the flag is used. The rest depend on where it is placed, so pass the grounds
67
+ * the flag will actually fly on and they are checked too. A flag that passes the
68
+ * intrinsic pair and fails a ground has a page problem, not a logo problem.
69
+ */
70
+ export declare function auditBurgee(brand: AuditInput, grounds?: readonly string[]): ContrastFinding[];
@@ -0,0 +1,93 @@
1
+ export const AA = {
2
+ TEXT: 4.5,
3
+ GRAPHIC: 3,
4
+ };
5
+ 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
+ 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
+ export function mix(a, b, t) {
37
+ const [ca, cb] = [channels(a), channels(b)];
38
+ const hex = ca
39
+ .map((v, i) => Math.round((v + (cb[i] - v) * t) * SRGB_MAX))
40
+ .map((v) => v.toString(HEX_RADIX).padStart(2, '0'))
41
+ .join('');
42
+ return `#${hex}`;
43
+ }
44
+ const CENTS = 100;
45
+ export function ratio(a, b) {
46
+ return Math.round(contrast(a, b) * CENTS) / CENTS;
47
+ }
48
+ export function check(what, a, b, required = AA.GRAPHIC) {
49
+ const value = ratio(a, b);
50
+ return { what, a, b, ratio: value, required, passes: value >= required };
51
+ }
52
+ export function report(findings) {
53
+ const width = Math.max(...findings.map((f) => f.what.length));
54
+ return findings
55
+ .map((f) => `${f.passes ? 'pass' : 'FAIL'} ${f.what.padEnd(width)} ` +
56
+ `${f.a} on ${f.b} ${f.ratio.toFixed(2)}:1 (needs ${f.required}:1)`)
57
+ .join('\n');
58
+ }
59
+ export function fieldColorAt(stops, at) {
60
+ const ordered = [...stops].sort((x, y) => x.offset - y.offset);
61
+ const first = ordered[0];
62
+ const last = ordered.at(-1);
63
+ if (first === undefined || last === undefined)
64
+ throw new Error('burgee: a field needs at least one stop');
65
+ if (at <= first.offset)
66
+ return first.color;
67
+ if (at >= last.offset)
68
+ return last.color;
69
+ for (let i = 1; i < ordered.length; i++) {
70
+ const lo = ordered[i - 1];
71
+ const hi = ordered[i];
72
+ if (at <= hi.offset) {
73
+ const span = hi.offset - lo.offset;
74
+ return span === 0 ? hi.color : mix(lo.color, hi.color, (at - lo.offset) / span);
75
+ }
76
+ }
77
+ return last.color;
78
+ }
79
+ const CHARGE_AT = 0.5;
80
+ export function auditBurgee(brand, grounds = []) {
81
+ const under = fieldColorAt(brand.field, CHARGE_AT);
82
+ const findings = [
83
+ check('leading bar on its field', brand.mark.lead, under),
84
+ check('following bar on its field', brand.mark.follow, under),
85
+ ];
86
+ const edges = [fieldColorAt(brand.field, 0), fieldColorAt(brand.field, 1)];
87
+ for (const ground of grounds) {
88
+ for (const edge of edges) {
89
+ findings.push(check(`flag edge on ${ground}`, edge, ground));
90
+ }
91
+ }
92
+ return findings;
93
+ }