burgee 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.d.ts +1 -90
  3. package/dist/cli.js +3 -151
  4. package/dist/commander/command.d.ts +22 -0
  5. package/dist/commander/command.js +41 -10
  6. package/dist/compat.d.ts +39 -2
  7. package/dist/compat.js +90 -2
  8. package/dist/complete-dynamic.d.ts +18 -0
  9. package/dist/complete-dynamic.js +19 -0
  10. package/dist/completions.js +32 -14
  11. package/dist/config-explain.d.ts +22 -0
  12. package/dist/config-explain.js +33 -0
  13. package/dist/define-error.d.ts +29 -0
  14. package/dist/define-error.js +29 -0
  15. package/dist/errors.d.ts +29 -0
  16. package/dist/errors.js +17 -0
  17. package/dist/execute.d.ts +6 -0
  18. package/dist/execute.js +71 -13
  19. package/dist/facade-failure.d.ts +17 -0
  20. package/dist/facade-failure.js +12 -0
  21. package/dist/fields.d.ts +32 -0
  22. package/dist/fields.js +45 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/manifest.d.ts +35 -5
  26. package/dist/manifest.js +6 -0
  27. package/dist/mcp.js +68 -4
  28. package/dist/migrate.d.ts +57 -7
  29. package/dist/migrate.js +413 -34
  30. package/dist/parse-hooks.d.ts +7 -0
  31. package/dist/parse-hooks.js +17 -0
  32. package/dist/plugin.d.ts +2 -6
  33. package/dist/plugin.js +1 -1
  34. package/dist/program-schema.json +1 -0
  35. package/dist/program.d.ts +90 -0
  36. package/dist/program.js +151 -0
  37. package/dist/runtime.d.ts +3 -1
  38. package/dist/runtime.js +3 -0
  39. package/dist/schema.d.ts +7 -0
  40. package/dist/schema.js +6 -11
  41. package/dist/schema.json +1 -1
  42. package/dist/stdin-dash.d.ts +15 -0
  43. package/dist/stdin-dash.js +12 -0
  44. package/dist/validate.d.ts +1 -27
  45. package/dist/validate.js +2 -17
  46. package/dist/yargs/factory.js +39 -13
  47. package/dist/yargs-parser.d.ts +8 -1
  48. package/dist/yargs-parser.js +1 -1
  49. package/dist/yargs.d.ts +1 -0
  50. package/dist/yargs.js +1 -0
  51. package/package.json +7 -6
@@ -0,0 +1,90 @@
1
+ export declare const brandCommand: import("./execute.js").Command<{
2
+ readonly lead: {
3
+ readonly type: "string";
4
+ readonly required: true;
5
+ readonly description: "leading colour, hex. Your primary; it leads the charge upper-left";
6
+ };
7
+ readonly follow: {
8
+ readonly type: "string";
9
+ readonly required: true;
10
+ readonly description: "following colour, hex. Your secondary; it follows lower-right";
11
+ };
12
+ readonly name: {
13
+ readonly type: "string";
14
+ readonly description: "brand name, used as the accessible label and card title";
15
+ };
16
+ readonly ground: {
17
+ readonly type: "string";
18
+ readonly default: "#0a0a0a";
19
+ readonly description: "the field’s dark midpoint, which is what keeps the charge legible";
20
+ };
21
+ readonly charge: {
22
+ readonly type: "string";
23
+ readonly description: "path to an SVG whose contents replace the bars, drawn in a 0 0 100 100 box";
24
+ };
25
+ readonly bordure: {
26
+ readonly type: "string";
27
+ readonly description: "outline colour, hex. Omit for no outline";
28
+ };
29
+ readonly bordureWidth: {
30
+ readonly type: "string";
31
+ readonly default: "1.5";
32
+ readonly description: "outline width";
33
+ };
34
+ readonly tagline: {
35
+ readonly type: "string";
36
+ readonly description: "one line under the name on the card and cover";
37
+ };
38
+ readonly out: {
39
+ readonly type: "string";
40
+ readonly description: "directory to write into. Omit to print the flag only";
41
+ };
42
+ readonly on: {
43
+ readonly type: "string";
44
+ readonly description: "page colour(s) the flag will fly on, comma separated. Checked for contrast";
45
+ };
46
+ readonly allowLowContrast: {
47
+ readonly type: "boolean";
48
+ readonly description: "emit anyway when a contrast check fails. Says so in the output";
49
+ };
50
+ }>;
51
+ /**
52
+ * `burgee dev <entry>` — watch the entry, reload it on change, serve it as MCP on stdio
53
+ * and print every surface on each save (dev-loop). Loaded only when asked for, so the
54
+ * CLI's own weight and the framework's stay what they were (W4).
55
+ */
56
+ export declare const devCommand: import("./execute.js").Command<{
57
+ readonly noWatch: {
58
+ readonly type: "boolean";
59
+ readonly description: "load once and serve; do not watch for changes";
60
+ };
61
+ }>;
62
+ /**
63
+ * `burgee migrate [dir]` — rewrite a commander or yargs project's imports to burgee's
64
+ * drop-in front-ends and report what changed, with the numbers that say why it was safe.
65
+ *
66
+ * The engine is loaded on this path only (K6), the same way `dev` is: a dynamic import, so
67
+ * `burgee`'s own start-up and the framework's weight are what they were. `weight.test.ts`
68
+ * denies `migrate.js` to the root entry by name, so that cannot drift back.
69
+ *
70
+ * `idempotent` is the honest answer rather than the conservative one, and it is earned:
71
+ * `burgee/commander` is not a key in the mapping, so a second run over a migrated tree
72
+ * rewrites nothing. N6 then requires the result to report `changed`, which it does.
73
+ */
74
+ export declare const migrateCommand: import("./execute.js").Command<{
75
+ readonly dryRun: {
76
+ readonly type: "boolean";
77
+ readonly description: "scan and report; write nothing";
78
+ };
79
+ readonly force: {
80
+ readonly type: "boolean";
81
+ readonly description: "migrate even though the git tree has uncommitted changes";
82
+ };
83
+ }>;
84
+ /**
85
+ * `burgee check <plugin-file>` — the feedback loop PRINCIPLES 7 asks every extension surface
86
+ * for, and the one burgee did not have. See `check.ts`: this one returns its report as data, so
87
+ * `--json` is the form an agent that just wrote a plugin reads.
88
+ */
89
+ export declare const pluginCheckCommand: import("./execute.js").Command<import("./execute.js").OptionSpecs>;
90
+ export declare const program: import("./manifest.js").Manifest;
@@ -0,0 +1,151 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DEFAULT_GROUND, defineBurgee, opposedField } from './brand.js';
4
+ import { auditBurgee, report } from './contrast.js';
5
+ import { defineCommand, defineProgram } from './execute.js';
6
+ const MASTER = 512;
7
+ const CONTRAST_HINT = 'hint: darken the --ground stop under the charge, or pass --allow-low-contrast';
8
+ const DEFAULT_BORDURE_WIDTH = '1.5';
9
+ function surfaces(brand, tagline) {
10
+ const burgee = defineBurgee(brand);
11
+ const subtitle = tagline === '' ? {} : { subtitle: tagline };
12
+ return [
13
+ { file: 'flag.svg', svg: burgee.flag(MASTER) },
14
+ { file: 'icon.svg', svg: burgee.favicon() },
15
+ { file: 'og.svg', svg: burgee.og(subtitle) },
16
+ { file: 'cover.svg', svg: burgee.cover(subtitle) },
17
+ { file: 'lockup.svg', svg: burgee.lockup({ theme: 'dark' }) },
18
+ { file: 'lockup-light.svg', svg: burgee.lockup({ theme: 'light' }) },
19
+ ];
20
+ }
21
+ export const brandCommand = defineCommand({
22
+ name: 'brand',
23
+ description: 'Generate a burgee — flag, favicon, social card, cover and lockup — from two colours',
24
+ effects: 'non_idempotent',
25
+ options: {
26
+ lead: {
27
+ type: 'string',
28
+ required: true,
29
+ description: 'leading colour, hex. Your primary; it leads the charge upper-left',
30
+ },
31
+ follow: {
32
+ type: 'string',
33
+ required: true,
34
+ description: 'following colour, hex. Your secondary; it follows lower-right',
35
+ },
36
+ name: { type: 'string', description: 'brand name, used as the accessible label and card title' },
37
+ ground: {
38
+ type: 'string',
39
+ default: DEFAULT_GROUND,
40
+ description: 'the field’s dark midpoint, which is what keeps the charge legible',
41
+ },
42
+ charge: {
43
+ type: 'string',
44
+ description: 'path to an SVG whose contents replace the bars, drawn in a 0 0 100 100 box',
45
+ },
46
+ bordure: { type: 'string', description: 'outline colour, hex. Omit for no outline' },
47
+ bordureWidth: { type: 'string', default: DEFAULT_BORDURE_WIDTH, description: 'outline width' },
48
+ tagline: { type: 'string', description: 'one line under the name on the card and cover' },
49
+ out: { type: 'string', description: 'directory to write into. Omit to print the flag only' },
50
+ 'on': {
51
+ type: 'string',
52
+ description: 'page colour(s) the flag will fly on, comma separated. Checked for contrast',
53
+ },
54
+ allowLowContrast: {
55
+ type: 'boolean',
56
+ description: 'emit anyway when a contrast check fails. Says so in the output',
57
+ },
58
+ },
59
+ run: ({ options }) => {
60
+ const lead = options.lead ?? '';
61
+ const follow = options.follow ?? '';
62
+ const colors = { lead, follow };
63
+ const charge = options.charge === undefined
64
+ ? {}
65
+ : { charge: readFileSync(options.charge, 'utf8').replace(/<\/?svg[^>]*>/g, '').trim() };
66
+ const bordure = options.bordure === undefined
67
+ ? {}
68
+ : {
69
+ bordure: {
70
+ color: options.bordure,
71
+ width: Number(options.bordureWidth ?? DEFAULT_BORDURE_WIDTH),
72
+ },
73
+ };
74
+ const brand = {
75
+ ...(options.name === undefined ? {} : { name: options.name }),
76
+ mark: colors,
77
+ field: opposedField(colors, options.ground ?? DEFAULT_GROUND),
78
+ ...charge,
79
+ ...bordure,
80
+ };
81
+ const grounds = (options.on ?? '')
82
+ .split(',')
83
+ .map((g) => g.trim())
84
+ .filter((g) => g !== '');
85
+ const findings = auditBurgee(brand, grounds);
86
+ const failed = findings.filter((f) => !f.passes);
87
+ if (failed.length > 0 && options.allowLowContrast !== true) {
88
+ throw new Error(`contrast below WCAG AA:\n${report(failed)}\n${CONTRAST_HINT}`);
89
+ }
90
+ const written = surfaces(brand, options.tagline ?? '');
91
+ const contrast = findings.map((f) => ({ what: f.what, ratio: f.ratio, passes: f.passes }));
92
+ if (options.out === undefined) {
93
+ return { flag: defineBurgee(brand).flag(MASTER), files: [], contrast };
94
+ }
95
+ mkdirSync(options.out, { recursive: true });
96
+ for (const s of written)
97
+ writeFileSync(join(options.out, s.file), `${s.svg}\n`);
98
+ return { out: options.out, files: written.map((s) => s.file), contrast };
99
+ },
100
+ });
101
+ export const devCommand = defineCommand({
102
+ name: 'dev',
103
+ description: 'Watch a CLI entry, reload it on change, and serve it as MCP on stdio while you write it',
104
+ arguments: [{ name: 'entry', description: 'the module that exports the program, as program or as its default export', required: true }],
105
+ options: {
106
+ noWatch: { type: 'boolean', description: 'load once and serve; do not watch for changes' },
107
+ },
108
+ effects: 'withheld',
109
+ run: async ({ positionals, options }) => {
110
+ const [entry] = positionals;
111
+ if (entry === undefined)
112
+ throw new Error('an entry file is required');
113
+ const [{ dev }, { processRuntime }] = await Promise.all([import('./dev.js'), import('./runtime.js')]);
114
+ const handle = dev({ entry, input: processRuntime.stdin, output: processRuntime.stdout, log: processRuntime.stderr, watch: options.noWatch !== true });
115
+ await handle.done;
116
+ },
117
+ });
118
+ export const migrateCommand = defineCommand({
119
+ name: 'migrate',
120
+ description: 'Rewrite imports of commander, yargs, chalk, ora and every other graded incumbent to the family’s drop-ins, and report what to install',
121
+ arguments: [{ name: 'dir', description: 'the project to migrate. Defaults to the current directory', required: false }],
122
+ options: {
123
+ dryRun: { type: 'boolean', description: 'scan and report; write nothing' },
124
+ force: { type: 'boolean', description: 'migrate even though the git tree has uncommitted changes' },
125
+ },
126
+ effects: 'idempotent',
127
+ examples: [
128
+ { command: 'burgee migrate --dry-run', description: 'what it would change, without changing it' },
129
+ { command: 'burgee migrate --json', description: 'the same report as data; exit 1 when anything was refused' },
130
+ ],
131
+ run: async ({ positionals, options }) => {
132
+ const [{ migrate }, { host }] = await Promise.all([import('./migrate.js'), import('./runtime.js')]);
133
+ return await migrate({ dir: positionals[0] ?? host.cwd(), dryRun: options.dryRun === true, force: options.force === true });
134
+ },
135
+ });
136
+ export const pluginCheckCommand = defineCommand({
137
+ name: 'check',
138
+ description: 'Validate a burgee plugin, register it into a throwaway program, and report what it contributes',
139
+ arguments: [{ name: 'file', description: 'the plugin module to check' }],
140
+ effects: 'read_only',
141
+ examples: [
142
+ { command: 'burgee check ./my-plugin.mjs', description: 'what the plugin contributes, or why it was refused' },
143
+ { command: 'burgee check ./my-plugin.mjs --json', description: 'the same, as data; exit 1 on a refusal' },
144
+ ],
145
+ run: async ({ positionals }) => (await import('./check.js')).checkPlugin(positionals[0] ?? ''),
146
+ });
147
+ export const program = defineProgram({
148
+ name: 'burgee',
149
+ description: 'The agent-native CLI framework, and the tools that come with it',
150
+ commands: [brandCommand, devCommand, migrateCommand, pluginCheckCommand],
151
+ });
package/dist/runtime.d.ts CHANGED
@@ -75,7 +75,9 @@ export declare const host: {
75
75
  * rather than the 80-column fallback.
76
76
  */
77
77
  readonly columns: number | undefined;
78
- readonly exitCode: number | string | null | undefined;
78
+ get exitCode(): number | string | null | undefined;
79
+ /** The code the process leaves with once its work is done — set, never `exit()`, so stdout drains first. */
80
+ set exitCode(code: ExitCode);
79
81
  readonly platform: string;
80
82
  readonly execPath: string;
81
83
  readonly execArgv: string[];
package/dist/runtime.js CHANGED
@@ -29,6 +29,9 @@ export const host = {
29
29
  get exitCode() {
30
30
  return process.exitCode;
31
31
  },
32
+ set exitCode(code) {
33
+ process.exitCode = code;
34
+ },
32
35
  get platform() {
33
36
  return process.platform;
34
37
  },
package/dist/schema.d.ts CHANGED
@@ -49,6 +49,8 @@ export interface CommandSchema {
49
49
  * it cannot tell from a command that does not exist (N6).
50
50
  */
51
51
  effects?: DeclaredEffects;
52
+ /** What `--json=` selects from (N14), when the command declares it. */
53
+ fields?: readonly string[];
52
54
  deprecated?: boolean | string;
53
55
  /** The heading it is listed under (M1). */
54
56
  group?: string;
@@ -99,6 +101,11 @@ export interface ProgramSchema {
99
101
  name: string;
100
102
  version?: string;
101
103
  description?: string;
104
+ /**
105
+ * F1 — what each exit code means, so an agent branches on the number without reading prose:
106
+ * the contract's seven, the same table `ExitCode` exports.
107
+ */
108
+ exitCodes: Readonly<Record<string, number>>;
102
109
  commands: CommandSchema[];
103
110
  }
104
111
  export declare function inputSchemaOf(node: CommandNode): JsonSchema;
package/dist/schema.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExitCode } from './exit-code.js';
1
2
  import { relationsOf } from './manifest.js';
2
3
  import { flagsOf, kebab } from './names.js';
3
4
  export const PREDICATE = '(predicate)';
@@ -66,6 +67,7 @@ export function inputSchemaOf(node) {
66
67
  export function typedName(node, root) {
67
68
  return node.path.slice(root.length).join(' ');
68
69
  }
70
+ const PASSED_THROUGH = ['description', 'summary', 'effects', 'fields', 'deprecated', 'group'];
69
71
  export function commandSchemaOf(node, root) {
70
72
  const out = {
71
73
  name: typedName(node, root),
@@ -74,16 +76,9 @@ export function commandSchemaOf(node, root) {
74
76
  examples: node.examples ?? [],
75
77
  inputSchema: inputSchemaOf(node),
76
78
  };
77
- if (node.description !== undefined)
78
- out.description = node.description;
79
- if (node.summary !== undefined)
80
- out.summary = node.summary;
81
- if (node.effects !== undefined)
82
- out.effects = node.effects;
83
- if (node.deprecated !== undefined)
84
- out.deprecated = node.deprecated;
85
- if (node.group !== undefined)
86
- out.group = node.group;
79
+ for (const key of PASSED_THROUGH)
80
+ if (node[key] !== undefined)
81
+ Object.assign(out, { [key]: node[key] });
87
82
  if (node.load !== undefined)
88
83
  out.lazy = true;
89
84
  if (node.plugin !== undefined)
@@ -121,7 +116,7 @@ export function summaryOf(manifest, budget) {
121
116
  export function schemaOf(manifest) {
122
117
  const root = manifest.rootPath;
123
118
  const program = manifest.find(root);
124
- const out = { schemaVersion: 1, name: root.join(' '), commands: runnable(manifest).map((c) => commandSchemaOf(c, root)) };
119
+ const out = { schemaVersion: 1, name: root.join(' '), exitCodes: ExitCode, commands: runnable(manifest).map((c) => commandSchemaOf(c, root)) };
125
120
  if (manifest.version !== undefined)
126
121
  out.version = manifest.version;
127
122
  const description = program?.description;
package/dist/schema.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"},"widths":{"$ref":"#/$defs/widths"},"resolvers":{"type":"object","description":"bellpull resolvers by name: extra directories searched for an executable, before PATH (negative rank) or after it (positive). Two resolvers with one name shadow; two names both apply, in rank order.","additionalProperties":{"$ref":"#/$defs/resolver"}},"widgets":{"type":"object","description":"caique prompt widgets by kind. The six built-in kinds — text, confirm, select, multiselect, password, path — cannot be replaced: a plugin adds kinds of its own.","additionalProperties":{"$ref":"#/$defs/widget"}},"handlers":{"type":"array","description":"closeout exit handlers. The phase decides the order they run in, not their position in this list.","items":{"$ref":"#/$defs/handler"}},"sources":{"type":"object","description":"seniority configuration sources by name, ranked against the built-in layers: a flag is 0 and a declared default is 40, and a plugin source sits strictly between.","additionalProperties":{"$ref":"#/$defs/source"}},"commands":{"type":"array","description":"burgee commands this plugin contributes. Each is read by exactly the code a program's own command is, and a burgee plugin must declare `contract`.","items":{"$ref":"#/$defs/command"}},"hooks":{"type":"object","description":"burgee lifecycle hooks. preRun opens around a command, and exactly one of postRun or onError closes.","additionalProperties":false,"properties":{"preRun":{"$ref":"#/$defs/hook"},"postRun":{"$ref":"#/$defs/hook"},"onError":{"$ref":"#/$defs/hook"}}},"enforce":{"type":"string","pattern":"^(pre|post)$","description":"burgee: order this plugin's hooks before (`pre`) or after (`post`) the others."}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]},"widthRange":{"type":"array","description":"One inclusive code-point range, as [low, high]. A single code point is written [n, n].","items":{"type":"integer","minimum":0,"maximum":1114111},"minItems":2,"maxItems":2},"widthOverride":{"type":"object","description":"A linegauge width override: the column count a named set of code-point ranges occupies, for a terminal that disagrees with the Unicode tables. Data only — no function, so it can be written in a config file, diffed, and printed by `linegauge check` without running anyone code.","required":["ranges","columns","why"],"additionalProperties":false,"properties":{"ranges":{"type":"array","description":"The code points this override applies to.","items":{"$ref":"#/$defs/widthRange"},"minItems":1},"columns":{"type":"integer","description":"Columns each cluster in those ranges occupies: 0 for a zero-width mark, 1 narrow, 2 wide.","minimum":0,"maximum":2},"why":{"type":"string","description":"The terminal or the reason. Required, because a width table with no provenance is one nobody can audit when it turns out to be wrong — which is the normal outcome for ambiguous width.","minLength":1}}},"widths":{"type":"object","description":"linegauge width overrides by name — the measurement section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/widthOverride"}},"resolver":{"type":"object","required":["rank","paths"],"description":"One bellpull resolver: where to look for an executable, and when.","properties":{"rank":{"type":"number","description":"Search order against PATH: negative searches before it, positive after."},"paths":{"type":"array","minItems":1,"description":"Directories to search. `{VAR}` is substituted from the environment; each must be absolute, or start with a `{VAR}` that holds an absolute path.","items":{"type":"string","minLength":1,"pattern":"^(/|\\{|[A-Za-z]:[\\\\/]|\\\\\\\\)"}},"extensions":{"type":"array","items":{"type":"string"},"description":"Extensions to try on Windows, in place of PATHEXT."},"when":{"type":"object","description":"When the resolver applies. Leave it out and it always does.","properties":{"platform":{"type":"array","items":{"type":"string"},"description":"process.platform values it applies on."},"envAny":{"type":"array","items":{"type":"string"},"description":"It applies when any of these environment variables is set."}}}}},"widget":{"type":"object","required":["static"],"description":"One caique widget: how a prompt kind of the plugin's own is drawn.","properties":{"static":{"description":"(spec) => string. Required: what a pipe, an agent or a screen reader gets instead of the interactive prompt."},"frame":{"description":"(t, spec) => string. Optional: the animated form; leave it out and the widget is line-mode only."},"sample":{"type":"object","required":["running","done"],"description":"Two named states of plain data that `caique check` renders the widget with."}}},"handler":{"type":"object","required":["name","run"],"description":"One closeout exit handler.","properties":{"name":{"type":"string","minLength":1,"description":"How the handler is named in a report, including the one the deadline prints when it does not return."},"phase":{"type":"string","pattern":"^(flush|release)$","description":"When it runs. Defaults to `release`; `restore` is closeout's own last phase and a plugin may not use it."},"run":{"description":"(info) => void | Promise<void>. Required: the cleanup itself."}}},"source":{"type":"object","required":["rank"],"description":"One seniority source. Exactly one of `values` (static) or `read(runtime)` (fetched) gives its answer.","properties":{"rank":{"type":"integer","minimum":1,"maximum":39,"description":"Precedence, strictly between the flag (0) and the declared default (40): a source may not beat what the user typed, nor sink below the default."},"values":{"type":"object","description":"Option name to value."},"read":{"description":"(runtime) => { values, location? } | undefined."},"location":{"type":"string","description":"The file, URL or variable set a person would go and look at."}}},"option":{"type":"object","required":["type"],"description":"One burgee option, keyed by its camelCase name; the flag is its kebab-case form.","properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"short":{"type":"string","minLength":1},"required":{"type":"boolean"},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"dependsOn":{"type":"array","items":{"type":"string"},"description":"Options that must be set with this one; each must be another option of the same command."},"exclusive":{"type":"array","items":{"type":"string"},"description":"Options that may not be set with this one; each must be another option of the same command."},"deprecated":{"description":"What replaces this option, e.g. '--force'. `true` alone, or an empty string, is refused: a deprecation names its replacement."}}},"command":{"type":"object","required":["path"],"description":"One burgee command node, declared exactly as a program's own command is.","properties":{"path":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"description":"The words a user types. May not repeat a path the program or an earlier plugin already declares."},"description":{"type":"string"},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world. Required on any command that runs; `withheld` serves it to people and keeps it out of the MCP tool list."},"deprecated":{"description":"What replaces this command. `true` alone, or an empty string, is refused."},"run":{"description":"(ctx) => unknown. What the command does; its return value is the `--json` envelope's data."},"load":{"description":"() => Promise<module>. A lazy form of `run`, imported on dispatch."}}},"hook":{"type":"object","required":["handler"],"description":"One burgee hook.","properties":{"filter":{"type":"object","description":"`{ command: RegExp }`: fire only for commands whose path matches."},"handler":{"description":"(ctx) => void | Promise<void>. Required."}}}}}
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"},"widths":{"$ref":"#/$defs/widths"},"resolvers":{"type":"object","description":"bellpull resolvers by name: extra directories searched for an executable, before PATH (negative rank) or after it (positive). Two resolvers with one name shadow; two names both apply, in rank order.","additionalProperties":{"$ref":"#/$defs/resolver"}},"widgets":{"type":"object","description":"caique prompt widgets by kind. The six built-in kinds — text, confirm, select, multiselect, password, path — cannot be replaced: a plugin adds kinds of its own.","additionalProperties":{"$ref":"#/$defs/widget"}},"handlers":{"type":"array","description":"closeout exit handlers. The phase decides the order they run in, not their position in this list.","items":{"$ref":"#/$defs/handler"}},"sources":{"type":"object","description":"seniority configuration sources by name, ranked against the built-in layers: a flag is 0 and a declared default is 40, and a plugin source sits strictly between.","additionalProperties":{"$ref":"#/$defs/source"}},"commands":{"type":"array","description":"burgee commands this plugin contributes. Each is read by exactly the code a program's own command is, and a burgee plugin must declare `contract`.","items":{"$ref":"#/$defs/command"}},"hooks":{"type":"object","description":"burgee lifecycle hooks. parse rewrites argv before a command is resolved; preRun opens around a command, and exactly one of postRun or onError closes; shutdown fires once as the program leaves, by any path.","additionalProperties":false,"properties":{"parse":{"$ref":"#/$defs/hook"},"preRun":{"$ref":"#/$defs/hook"},"postRun":{"$ref":"#/$defs/hook"},"onError":{"$ref":"#/$defs/hook"},"shutdown":{"$ref":"#/$defs/hook"}}},"enforce":{"type":"string","pattern":"^(pre|post)$","description":"burgee: order this plugin's hooks before (`pre`) or after (`post`) the others."}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]},"widthRange":{"type":"array","description":"One inclusive code-point range, as [low, high]. A single code point is written [n, n].","items":{"type":"integer","minimum":0,"maximum":1114111},"minItems":2,"maxItems":2},"widthOverride":{"type":"object","description":"A linegauge width override: the column count a named set of code-point ranges occupies, for a terminal that disagrees with the Unicode tables. Data only — no function, so it can be written in a config file, diffed, and printed by `linegauge check` without running anyone code.","required":["ranges","columns","why"],"additionalProperties":false,"properties":{"ranges":{"type":"array","description":"The code points this override applies to.","items":{"$ref":"#/$defs/widthRange"},"minItems":1},"columns":{"type":"integer","description":"Columns each cluster in those ranges occupies: 0 for a zero-width mark, 1 narrow, 2 wide.","minimum":0,"maximum":2},"why":{"type":"string","description":"The terminal or the reason. Required, because a width table with no provenance is one nobody can audit when it turns out to be wrong — which is the normal outcome for ambiguous width.","minLength":1}}},"widths":{"type":"object","description":"linegauge width overrides by name — the measurement section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/widthOverride"}},"resolver":{"type":"object","required":["rank","paths"],"description":"One bellpull resolver: where to look for an executable, and when.","properties":{"rank":{"type":"number","description":"Search order against PATH: negative searches before it, positive after."},"paths":{"type":"array","minItems":1,"description":"Directories to search. `{VAR}` is substituted from the environment; each must be absolute, or start with a `{VAR}` that holds an absolute path.","items":{"type":"string","minLength":1,"pattern":"^(/|\\{|[A-Za-z]:[\\\\/]|\\\\\\\\)"}},"extensions":{"type":"array","items":{"type":"string"},"description":"Extensions to try on Windows, in place of PATHEXT."},"when":{"type":"object","description":"When the resolver applies. Leave it out and it always does.","properties":{"platform":{"type":"array","items":{"type":"string"},"description":"process.platform values it applies on."},"envAny":{"type":"array","items":{"type":"string"},"description":"It applies when any of these environment variables is set."}}}}},"widget":{"type":"object","required":["static"],"description":"One caique widget: how a prompt kind of the plugin's own is drawn.","properties":{"static":{"description":"(spec) => string. Required: what a pipe, an agent or a screen reader gets instead of the interactive prompt."},"frame":{"description":"(t, spec) => string. Optional: the animated form; leave it out and the widget is line-mode only."},"sample":{"type":"object","required":["running","done"],"description":"Two named states of plain data that `caique check` renders the widget with."}}},"handler":{"type":"object","required":["name","run"],"description":"One closeout exit handler.","properties":{"name":{"type":"string","minLength":1,"description":"How the handler is named in a report, including the one the deadline prints when it does not return."},"phase":{"type":"string","pattern":"^(flush|release)$","description":"When it runs. Defaults to `release`; `restore` is closeout's own last phase and a plugin may not use it."},"run":{"description":"(info) => void | Promise<void>. Required: the cleanup itself."}}},"source":{"type":"object","required":["rank"],"description":"One seniority source. Exactly one of `values` (static) or `read(runtime)` (fetched) gives its answer.","properties":{"rank":{"type":"integer","minimum":1,"maximum":39,"description":"Precedence, strictly between the flag (0) and the declared default (40): a source may not beat what the user typed, nor sink below the default."},"values":{"type":"object","description":"Option name to value."},"read":{"description":"(runtime) => { values, location? } | undefined."},"location":{"type":"string","description":"The file, URL or variable set a person would go and look at."}}},"option":{"type":"object","required":["type"],"description":"One burgee option, keyed by its camelCase name; the flag is its kebab-case form.","properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"short":{"type":"string","minLength":1},"required":{"type":"boolean"},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"dependsOn":{"type":"array","items":{"type":"string"},"description":"Options that must be set with this one; each must be another option of the same command."},"exclusive":{"type":"array","items":{"type":"string"},"description":"Options that may not be set with this one; each must be another option of the same command."},"deprecated":{"description":"What replaces this option, e.g. '--force'. `true` alone, or an empty string, is refused: a deprecation names its replacement."}}},"command":{"type":"object","required":["path"],"description":"One burgee command node, declared exactly as a program's own command is.","properties":{"path":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"description":"The words a user types. May not repeat a path the program or an earlier plugin already declares."},"description":{"type":"string"},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world. Required on any command that runs; `withheld` serves it to people and keeps it out of the MCP tool list."},"deprecated":{"description":"What replaces this command. `true` alone, or an empty string, is refused."},"run":{"description":"(ctx) => unknown. What the command does; its return value is the `--json` envelope's data."},"load":{"description":"() => Promise<module>. A lazy form of `run`, imported on dispatch."}}},"hook":{"type":"object","required":["handler"],"description":"One burgee hook.","properties":{"filter":{"type":"object","description":"`{ command: RegExp }`: fire only for commands whose path matches."},"handler":{"description":"(ctx) => void | Promise<void>. Required. On parse it may return the string[] argv to use instead."}}}}}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Copyright (c) 2026 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ /** S4 — imported by `execute.ts` only when a positional is `-` (M2). */
7
+ import { type CommandNode } from './manifest.js';
8
+ /**
9
+ * S4 — the stdin a `-` names, when a `type: 'file'` argument took it; nothing otherwise. A
10
+ * variadic file argument covers every position from its own. Standard input can be read once,
11
+ * so `-` for two file arguments is refused rather than handed to both.
12
+ */
13
+ export declare function stdinFor(node: CommandNode, positionals: readonly string[], stdin: NodeJS.ReadableStream): {
14
+ stdin?: NodeJS.ReadableStream;
15
+ };
@@ -0,0 +1,12 @@
1
+ import { UsageError } from './validate.js';
2
+ export function stdinFor(node, positionals, stdin) {
3
+ const args = node.arguments ?? [];
4
+ const last = args.at(-1);
5
+ const isFile = (i) => (args[i] ?? (last?.variadic === true ? last : undefined))?.type === 'file';
6
+ const dashes = positionals.filter((p, i) => p === '-' && isFile(i)).length;
7
+ if (dashes === 0)
8
+ return {};
9
+ if (dashes > 1)
10
+ throw new UsageError(`"-" was given for ${String(dashes)} file arguments, and standard input can be read once`, 'pass a path for all but one of them');
11
+ return { stdin };
12
+ }
@@ -1,31 +1,4 @@
1
- /**
2
- * Run-time validation in one fixed order (S6): relations first, then each option's value —
3
- * numbers, choices, Standard Schema. Every failure is a usage error with the fix in hand (E3).
4
- *
5
- * The definition-time checks (S3, S5, V5) moved to `./definition.js` when the plugin host
6
- * arrived; that file says why. This one runs on every invocation, that one runs once.
7
- */
8
1
  import { type OptionSpec, type Relation } from './manifest.js';
9
- /** A usage problem the caller can fix, carrying the flag that fixes it (E3). */
10
- export declare class UsageError extends Error {
11
- readonly hint?: string | undefined;
12
- constructor(message: string, hint?: string | undefined);
13
- }
14
- /**
15
- * E6 — the far side said no. Throw this and the run leaves with `ExitCode.AUTH`.
16
- *
17
- * The one error class whose *response* is unambiguous: not "read the message and decide" but
18
- * "get a credential and run it again". A handler that throws a bare `Error` for a 401 gets
19
- * `RUNTIME`, which is the code for everything, and a caller retrying on it retries forever.
20
- *
21
- * `fix` is the exact command that gets the credential, where the program knows it — `hint` is
22
- * prose a person reads and `fix` is a line a caller runs, which is the turn the field saves.
23
- */
24
- export declare class AuthError extends Error {
25
- readonly hint?: string | undefined;
26
- readonly fix?: string | undefined;
27
- constructor(message: string, hint?: string | undefined, fix?: string | undefined);
28
- }
29
2
  type Sources = Record<string, {
30
3
  source: string;
31
4
  }>;
@@ -37,3 +10,4 @@ export declare function splitMultiple(spec: OptionSpec, raw: unknown): unknown[]
37
10
  /** Every option's resolved value, coerced and validated: numbers, choices, then its Standard Schema (S3, S6). */
38
11
  export declare function coerce(specs: Record<string, OptionSpec>, values: Record<string, unknown>): Promise<Record<string, unknown>>;
39
12
  export { camel, kebab } from './names.js';
13
+ export { AuthError, UsageError } from './errors.js';
package/dist/validate.js CHANGED
@@ -1,21 +1,5 @@
1
+ import { UsageError } from './errors.js';
1
2
  import { flagsOf, kebab } from './names.js';
2
- export class UsageError extends Error {
3
- hint;
4
- constructor(message, hint) {
5
- super(message);
6
- this.hint = hint;
7
- }
8
- }
9
- export class AuthError extends Error {
10
- hint;
11
- fix;
12
- constructor(message, hint, fix) {
13
- super(message);
14
- this.hint = hint;
15
- this.fix = fix;
16
- this.name = 'AuthError';
17
- }
18
- }
19
3
  const isSet = (values, key, sources) => values[key] !== undefined && sources[key]?.source !== 'default';
20
4
  const flagList = (keys) => flagsOf(keys).join(', ');
21
5
  function exactlyOne(keys, on) {
@@ -118,3 +102,4 @@ export async function coerce(specs, values) {
118
102
  return { ...values, ...Object.fromEntries(coerced) };
119
103
  }
120
104
  export { camel, kebab } from './names.js';
105
+ export { AuthError, UsageError } from './errors.js';
@@ -1,6 +1,8 @@
1
1
  var _a;
2
2
  import { ExitCode } from '../exit-code.js';
3
+ import { handlerFailure } from '../facade-failure.js';
3
4
  import { Manifest } from '../manifest.js';
5
+ import { host } from '../runtime.js';
4
6
  import { machineJson, schemaOf } from '../schema.js';
5
7
  import { tokenizeArgString } from '../yargs-parser.js';
6
8
  import { projectManifest, render } from './burgee.js';
@@ -343,17 +345,18 @@ export class YargsInstance {
343
345
  this.#hasOutput = true;
344
346
  this.#exitError = err;
345
347
  const burgee = this.#burgee;
348
+ const e1 = code === 0 ? ExitCode.OK : this.#failureOf(err).exit;
346
349
  if (burgee !== undefined && code !== 0 && burgee.json)
347
350
  this.#failureEnvelope(err);
348
351
  if (burgee?.exit !== undefined) {
349
352
  if (burgee.exited)
350
353
  return;
351
354
  burgee.exited = true;
352
- burgee.exit(code === 0 ? ExitCode.OK : err instanceof YError || err === undefined || typeof err === 'string' ? ExitCode.USAGE : ExitCode.RUNTIME);
355
+ burgee.exit(e1);
353
356
  return;
354
357
  }
355
358
  if (this.#exitProcess)
356
- this.#shim.process.exit(code);
359
+ this.#shim.process.exit(burgee?.json === true ? e1 : code);
357
360
  }
358
361
  exitProcess(enabled = true) {
359
362
  argsert('[boolean]', [enabled], arguments.length);
@@ -598,19 +601,28 @@ export class YargsInstance {
598
601
  };
599
602
  const seam = this.#burgee?.exit === undefined ? undefined : this.#burgee;
600
603
  if (seam === undefined) {
604
+ const escape = (err) => {
605
+ const json = this.#burgee?.json === true && !(err instanceof YError);
606
+ if (json)
607
+ this.#failJson(err);
608
+ restore();
609
+ if (!json)
610
+ throw err;
611
+ return undefined;
612
+ };
601
613
  try {
602
614
  const result = this.#parse(args, shortCircuit, _parseFn);
603
615
  if (isPromise(result))
604
- return result.finally(restore);
616
+ return result.then((argv) => (restore(), argv), escape);
605
617
  restore();
606
618
  return result;
607
619
  }
608
620
  catch (err) {
609
- restore();
610
- throw err;
621
+ return escape(err);
611
622
  }
612
623
  }
613
624
  seam.exited = false;
625
+ seam.reported = false;
614
626
  const finish = (argv) => {
615
627
  if (!this.#burgee?.exited)
616
628
  seam.exit?.(ExitCode.OK);
@@ -622,13 +634,13 @@ export class YargsInstance {
622
634
  restore();
623
635
  throw err;
624
636
  }
625
- const message = err instanceof Error ? err.message : String(err);
637
+ const failure = this.#failureOf(err);
626
638
  if (this.#burgee?.json)
627
- this.#failureEnvelope(err instanceof Error ? err : message);
639
+ this.#failureEnvelope(err);
628
640
  else
629
- this.#logger.error(message);
641
+ this.#logger.error(failure.error.message);
630
642
  if (!this.#burgee?.exited)
631
- seam.exit?.(ExitCode.RUNTIME);
643
+ seam.exit?.(failure.exit);
632
644
  restore();
633
645
  return undefined;
634
646
  };
@@ -1236,7 +1248,7 @@ export class YargsInstance {
1236
1248
  return args;
1237
1249
  if (this.#declares('json'))
1238
1250
  return args;
1239
- this.#burgee = { ...(this.#burgee ?? { lastError: '' }), json: true };
1251
+ this.#burgee = { ...(this.#burgee ?? { lastError: '' }), json: true, reported: false };
1240
1252
  return [...list.slice(0, index), ...list.slice(index + 1)];
1241
1253
  }
1242
1254
  #declares(key) {
@@ -1334,11 +1346,25 @@ export class YargsInstance {
1334
1346
  if (text !== '')
1335
1347
  this.#logger.log(text.replace(/\n$/, ''));
1336
1348
  }
1349
+ #failureOf(err) {
1350
+ if (err instanceof YError || err === undefined)
1351
+ return { exit: ExitCode.USAGE, error: { code: 'usage', message: err?.message ?? (this.#burgee?.lastError ?? '') } };
1352
+ return handlerFailure(err);
1353
+ }
1337
1354
  #failureEnvelope(err) {
1338
1355
  const burgee = this.#burgee;
1339
- const message = err instanceof Error ? err.message : typeof err === 'string' ? err : burgee.lastError;
1340
- const code = err instanceof YError || err === undefined || typeof err === 'string' ? 'usage' : 'runtime';
1341
- this.#logger.log(JSON.stringify({ ok: false, error: { code, message } }));
1356
+ if (burgee.reported === true)
1357
+ return;
1358
+ burgee.reported = true;
1359
+ this.#logger.log(JSON.stringify({ ok: false, error: this.#failureOf(err).error }));
1360
+ }
1361
+ #failJson(err) {
1362
+ this.#failureEnvelope(err);
1363
+ const { exit } = this.#failureOf(err);
1364
+ if (this.#exitProcess)
1365
+ this.#shim.process.exit(exit);
1366
+ else
1367
+ host.exitCode = exit;
1342
1368
  }
1343
1369
  #provenance(argv) {
1344
1370
  const out = {};