burgee 0.6.1 → 0.7.1
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.
- package/README.md +37 -2
- package/dist/cli.js +2 -0
- package/dist/commander/command.d.ts +6 -6
- package/dist/commander/command.js +43 -32
- package/dist/commander.d.ts +1 -1
- package/dist/completions.d.ts +8 -0
- package/dist/completions.js +5 -1
- package/dist/definition.d.ts +45 -0
- package/dist/definition.js +57 -0
- package/dist/execute.d.ts +12 -3
- package/dist/execute.js +57 -37
- package/dist/help.d.ts +6 -1
- package/dist/help.js +35 -17
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/manifest.d.ts +95 -13
- package/dist/manifest.js +16 -5
- package/dist/mcp.d.ts +11 -1
- package/dist/mcp.js +2 -1
- package/dist/names.d.ts +6 -0
- package/dist/names.js +3 -0
- package/dist/plugin.d.ts +48 -0
- package/dist/plugin.js +82 -0
- package/dist/runtime.d.ts +57 -1
- package/dist/runtime.js +80 -11
- package/dist/schema.d.ts +19 -3
- package/dist/schema.js +9 -3
- package/dist/schema.json +1 -0
- package/dist/shutdown.d.ts +70 -0
- package/dist/shutdown.js +27 -0
- package/dist/testing-helpers.js +8 -7
- package/dist/unknown-option.d.ts +1 -0
- package/dist/unknown-option.js +1 -0
- package/dist/validate.d.ts +5 -8
- package/dist/validate.js +2 -25
- package/dist/yargs/burgee.d.ts +2 -2
- package/dist/yargs/cliui.js +26 -4
- package/dist/yargs/factory.d.ts +2 -2
- package/dist/yargs/factory.js +7 -2
- package/dist/yargs/shim.js +18 -11
- package/dist/yargs/utils.js +5 -4
- package/dist/yargs-parser.js +3 -3
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<p align="center">
|
|
15
15
|
<a href="https://www.npmjs.com/package/burgee"><img src="https://img.shields.io/npm/v/burgee?style=flat-square&color=0a6b47" alt="npm version" /></a>
|
|
16
16
|
<a href="https://www.npmjs.com/package/burgee"><img src="https://img.shields.io/npm/dm/burgee?style=flat-square" alt="npm downloads" /></a>
|
|
17
|
-
<img src="https://img.shields.io/badge/
|
|
17
|
+
<img src="https://img.shields.io/badge/dependencies-5%20in--family-0a6b47?style=flat-square" alt="Five dependencies, all in this repository: bellpull, closeout, linegauge, roundel, seniority" />
|
|
18
18
|
<img src="https://img.shields.io/badge/Node.js-24+-green.svg?style=flat-square" alt="Node.js 24+" />
|
|
19
19
|
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
20
20
|
</p>
|
|
@@ -62,7 +62,7 @@ that on every commit.
|
|
|
62
62
|
```text
|
|
63
63
|
defineCommand() ──▶ manifest ──┬──▶ human help
|
|
64
64
|
├──▶ --json one stable envelope
|
|
65
|
-
├──▶ --schema versioned,
|
|
65
|
+
├──▶ --schema versioned, one document per surface
|
|
66
66
|
├──▶ --mcp an MCP server, generated
|
|
67
67
|
├──▶ completions bash · zsh · fish · pwsh
|
|
68
68
|
├──▶ TypeScript types
|
|
@@ -92,6 +92,25 @@ host's own suite passes 100%; below that the rate is published instead of claime
|
|
|
92
92
|
| `burgee/commander` | The commander API, graded by commander's suite. |
|
|
93
93
|
| `burgee/testing` | Run a command in-process and assert on its result — no spawning. |
|
|
94
94
|
| `burgee/brand` | One brand declaration → flag, favicon, OG card, cover, lockup. The logo above is its own output. |
|
|
95
|
+
| `burgee/plugin` | `definePlugin()`, `validate()`, `CONTRACT` and `PluginError` — the host, at the subpath every package in the family publishes its host at. |
|
|
96
|
+
|
|
97
|
+
## Writing a plugin
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { definePlugin } from 'burgee/plugin';
|
|
101
|
+
|
|
102
|
+
export default definePlugin({
|
|
103
|
+
name: 'acme',
|
|
104
|
+
commands: [{ path: ['audit'], description: 'Audit the tree', options: {}, effects: 'read_only', run: () => ({ findings: 0 }) }],
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A plugin's command is read by exactly the code a first-party one is read by, so the same
|
|
109
|
+
refusals apply: reserved option names, duplicate flags, a contributed path that is already
|
|
110
|
+
declared — and `effects`, which every runnable command declares. `definePlugin` stamps the
|
|
111
|
+
`contract` this burgee was compiled against; an object that reaches `use()` without one is
|
|
112
|
+
refused rather than accepted on trust, because burgee's extension point shipped before it
|
|
113
|
+
validated anything.
|
|
95
114
|
|
|
96
115
|
## Status
|
|
97
116
|
|
|
@@ -112,3 +131,19 @@ what it is, [roundel](https://www.npmjs.com/package/roundel) carries its colours
|
|
|
112
131
|
none requires the others.
|
|
113
132
|
|
|
114
133
|
MIT © Ofri Peretz — see [LICENSE](./LICENSE).
|
|
134
|
+
|
|
135
|
+
## Benchmarks
|
|
136
|
+
|
|
137
|
+
Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
|
|
138
|
+
|
|
139
|
+
Graded by the incumbent's own test suite:
|
|
140
|
+
|
|
141
|
+
| suite | passing |
|
|
142
|
+
| :-- | --: |
|
|
143
|
+
| `commander` | 1360 / 1360 |
|
|
144
|
+
| `yargs` | 804 / 804 |
|
|
145
|
+
## Where it sits
|
|
146
|
+
|
|
147
|
+
Plugins register under the `commands` and `hooks` keys, against the one schema the whole family shares.
|
|
148
|
+
|
|
149
|
+
Nothing in this family builds on it yet, and it builds on `bellpull`, `closeout`, `linegauge`, `roundel`, `seniority`.
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ function surfaces(brand, tagline) {
|
|
|
21
21
|
export const brandCommand = defineCommand({
|
|
22
22
|
name: 'brand',
|
|
23
23
|
description: 'Generate a burgee — flag, favicon, social card, cover and lockup — from two colours',
|
|
24
|
+
effects: 'non_idempotent',
|
|
24
25
|
options: {
|
|
25
26
|
lead: {
|
|
26
27
|
type: 'string',
|
|
@@ -104,6 +105,7 @@ export const devCommand = defineCommand({
|
|
|
104
105
|
options: {
|
|
105
106
|
'no-watch': { type: 'boolean', description: 'load once and serve; do not watch for changes' },
|
|
106
107
|
},
|
|
108
|
+
effects: 'withheld',
|
|
107
109
|
run: async ({ positionals, options }) => {
|
|
108
110
|
const [entry] = positionals;
|
|
109
111
|
if (entry === undefined)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
/**
|
|
2
3
|
* commander's `Command`, ported method for method from commander 15 and graded by
|
|
3
4
|
* commander's own suite through `compat-oracle`. The parse pipeline, the option
|
|
@@ -12,9 +13,8 @@
|
|
|
12
13
|
* - `parse(argv, { stdout, stderr, exit })` injects the streams and the exit, and
|
|
13
14
|
* then reports through the E1 taxonomy — the harness's seam (T1).
|
|
14
15
|
*/
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import { type Effects, Manifest, type OptionSpec, type Plugin } from '../manifest.js';
|
|
16
|
+
import * as crossSpawn from 'bellpull/cross-spawn';
|
|
17
|
+
import { type DeclaredEffects, Manifest, type OptionSpec, type Plugin } from '../manifest.js';
|
|
18
18
|
import { Argument, type ParseArg } from './argument.js';
|
|
19
19
|
import { CommanderError } from './error.js';
|
|
20
20
|
import { Help } from './help.js';
|
|
@@ -85,7 +85,7 @@ export declare class Command extends EventEmitter {
|
|
|
85
85
|
rawArgs: string[];
|
|
86
86
|
/** like .args but after custom processing and collecting variadic */
|
|
87
87
|
processedArgs: unknown[];
|
|
88
|
-
runningCommand:
|
|
88
|
+
runningCommand: crossSpawn.ChildProcess | undefined;
|
|
89
89
|
_allowUnknownOption: boolean;
|
|
90
90
|
_allowExcessArguments: boolean;
|
|
91
91
|
_scriptPath: string | null;
|
|
@@ -126,7 +126,7 @@ export declare class Command extends EventEmitter {
|
|
|
126
126
|
/** burgee: the root's projection, created on first use. */
|
|
127
127
|
_manifest: Manifest | undefined;
|
|
128
128
|
/** burgee: what this command does to the world (N6); declaring it exposes the command as an MCP tool. */
|
|
129
|
-
_effects:
|
|
129
|
+
_effects: DeclaredEffects | undefined;
|
|
130
130
|
/** burgee: `true`, or the replacement's name (M5). Shown in help, schema and a one-line warning on use. */
|
|
131
131
|
_deprecated: boolean | string | undefined;
|
|
132
132
|
_deprecationWarned: boolean;
|
|
@@ -315,7 +315,7 @@ export declare class Command extends EventEmitter {
|
|
|
315
315
|
/** Options as the manifest describes them, on a null-prototype record. */
|
|
316
316
|
_optionSpecs(): Record<string, OptionSpec>;
|
|
317
317
|
/** burgee: declare what the command does to the world (N6). This is what exposes it as an MCP tool (N2). */
|
|
318
|
-
effects(value:
|
|
318
|
+
effects(value: DeclaredEffects): this;
|
|
319
319
|
/**
|
|
320
320
|
* burgee: mark the command deprecated (M5). Help and `--schema` show it; running it prints
|
|
321
321
|
* `warning: 'old' is deprecated, use 'new'` on stderr once and goes on, exit unchanged.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
1
5
|
/**
|
|
2
6
|
* commander's `Command`, ported method for method from commander 15 and graded by
|
|
3
7
|
* commander's own suite through `compat-oracle`. The parse pipeline, the option
|
|
@@ -12,15 +16,12 @@
|
|
|
12
16
|
* - `parse(argv, { stdout, stderr, exit })` injects the streams and the exit, and
|
|
13
17
|
* then reports through the E1 taxonomy — the harness's seam (T1).
|
|
14
18
|
*/
|
|
15
|
-
import childProcess from
|
|
16
|
-
import
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import process from 'node:process';
|
|
20
|
-
import { stripVTControlCharacters } from 'node:util';
|
|
19
|
+
// eslint-disable-next-line import-next/no-namespace -- `spawn` is read off the namespace at the call site and never captured into a local. commander's own suite mocks `childProcess.spawn` in roughly 23 `executableSubcommand` cases, and a binding captured at import never re-syncs; bellpull's `cross-spawn.ts` reads `spawn` off its default import for this exact consumer, so a named import here would undo that and take the row from 1360 / 1360 to ungradeable.
|
|
20
|
+
import * as crossSpawn from 'bellpull/cross-spawn';
|
|
21
21
|
import { ExitCode } from '../exit-code.js';
|
|
22
22
|
import { Manifest } from '../manifest.js';
|
|
23
23
|
import { serveMcp } from '../mcp.js';
|
|
24
|
+
import { host } from '../runtime.js';
|
|
24
25
|
import { machineJson, schemaOf } from '../schema.js';
|
|
25
26
|
import { suggestSimilar } from '../suggest.js';
|
|
26
27
|
import { Argument, humanReadableArgName } from './argument.js';
|
|
@@ -132,13 +133,13 @@ export class Command extends EventEmitter {
|
|
|
132
133
|
this._args = this.registeredArguments;
|
|
133
134
|
this._name = name || '';
|
|
134
135
|
this._outputConfiguration = {
|
|
135
|
-
writeOut: (str) =>
|
|
136
|
-
writeErr: (str) =>
|
|
136
|
+
writeOut: (str) => host.stdout.write(str),
|
|
137
|
+
writeErr: (str) => host.stderr.write(str),
|
|
137
138
|
outputError: (str, write) => write(str),
|
|
138
|
-
getOutHelpWidth: () => (
|
|
139
|
-
getErrHelpWidth: () => (
|
|
140
|
-
getOutHasColors: () => useColor() ?? (
|
|
141
|
-
getErrHasColors: () => useColor() ?? (
|
|
139
|
+
getOutHelpWidth: () => (host.stdout.isTTY ? host.stdout.columns : undefined),
|
|
140
|
+
getErrHelpWidth: () => (host.stderr.isTTY ? host.stderr.columns : undefined),
|
|
141
|
+
getOutHasColors: () => useColor() ?? (host.stdout.isTTY && host.stdout.hasColors?.()),
|
|
142
|
+
getErrHasColors: () => useColor() ?? (host.stderr.isTTY && host.stderr.hasColors?.()),
|
|
142
143
|
stripColor: (str) => stripVTControlCharacters(str),
|
|
143
144
|
};
|
|
144
145
|
}
|
|
@@ -335,7 +336,7 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
335
336
|
this._exitCallback(new CommanderError(exitCode, code, message));
|
|
336
337
|
// Expecting this line is not reached.
|
|
337
338
|
}
|
|
338
|
-
|
|
339
|
+
return host.exit(exitCode);
|
|
339
340
|
}
|
|
340
341
|
// commander's contract: the positional args, then the options, then the command itself.
|
|
341
342
|
action(fn) {
|
|
@@ -532,15 +533,15 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
532
533
|
throw new Error('first parameter to parse must be array or undefined');
|
|
533
534
|
parseOptions = parseOptions ?? {};
|
|
534
535
|
if (argv === undefined && parseOptions.from === undefined) {
|
|
535
|
-
if (
|
|
536
|
+
if (host.versions['electron'])
|
|
536
537
|
parseOptions.from = 'electron';
|
|
537
|
-
const execArgv =
|
|
538
|
+
const execArgv = host.execArgv ?? [];
|
|
538
539
|
if (execArgv.includes('-e') || execArgv.includes('--eval') || execArgv.includes('-p') || execArgv.includes('--print')) {
|
|
539
540
|
parseOptions.from = 'eval'; // internal usage, not documented
|
|
540
541
|
}
|
|
541
542
|
}
|
|
542
543
|
if (argv === undefined)
|
|
543
|
-
argv =
|
|
544
|
+
argv = host.argv;
|
|
544
545
|
this.rawArgs = argv.slice();
|
|
545
546
|
let userArgs;
|
|
546
547
|
switch (parseOptions.from) {
|
|
@@ -693,27 +694,37 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
693
694
|
executableFile = localFile || executableFile;
|
|
694
695
|
}
|
|
695
696
|
const launchWithNode = SOURCE_EXT.includes(path.extname(executableFile));
|
|
697
|
+
// Through `bellpull`, not `node:child_process`: it resolves the executable properly on
|
|
698
|
+
// Windows, where upstream sends every spawn through `node` to dodge `PATHEXT`. The
|
|
699
|
+
// reasoning, the measurement and the mock constraint are in `weight.test.ts`'s
|
|
700
|
+
// `./commander` entry — they are prose, and prose in this file ships.
|
|
696
701
|
let proc;
|
|
697
|
-
if (
|
|
702
|
+
if (host.platform !== 'win32') {
|
|
698
703
|
if (launchWithNode) {
|
|
699
704
|
args.unshift(executableFile);
|
|
700
|
-
args = incrementNodeInspectorPort(
|
|
701
|
-
proc =
|
|
705
|
+
args = incrementNodeInspectorPort(host.execArgv).concat(args);
|
|
706
|
+
proc = crossSpawn.spawn(host.argv[0] ?? host.execPath, args, { stdio: 'inherit' });
|
|
702
707
|
}
|
|
703
708
|
else {
|
|
704
|
-
proc =
|
|
709
|
+
proc = crossSpawn.spawn(executableFile, args, { stdio: 'inherit' });
|
|
705
710
|
}
|
|
706
711
|
}
|
|
707
712
|
else {
|
|
708
713
|
this._checkForMissingExecutable(executableFile, executableDir, subcommand._name);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
if (launchWithNode) {
|
|
715
|
+
args.unshift(executableFile);
|
|
716
|
+
args = incrementNodeInspectorPort(host.execArgv).concat(args);
|
|
717
|
+
proc = crossSpawn.spawn(host.execPath, args, { stdio: 'inherit' });
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// The case upstream cannot reach: a `.cmd`, a `.bat`, or a shebang that is not node.
|
|
721
|
+
proc = crossSpawn.spawn(executableFile, args, { stdio: 'inherit' });
|
|
722
|
+
}
|
|
712
723
|
}
|
|
713
724
|
if (!proc.killed) {
|
|
714
725
|
// Testing mainly to avoid leak warnings during unit tests with mocked spawn.
|
|
715
726
|
for (const signal of FORWARDED_SIGNALS) {
|
|
716
|
-
|
|
727
|
+
host.on(signal, () => {
|
|
717
728
|
if (proc.killed === false && proc.exitCode === null)
|
|
718
729
|
proc.kill(signal);
|
|
719
730
|
});
|
|
@@ -723,7 +734,7 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
723
734
|
proc.on('close', (code) => {
|
|
724
735
|
code = code ?? 1; // null when the spawned process terminated due to a signal
|
|
725
736
|
if (!exitCallback)
|
|
726
|
-
|
|
737
|
+
host.exit(code);
|
|
727
738
|
else
|
|
728
739
|
exitCallback(new CommanderError(code, 'commander.executeSubCommandAsync', '(close)'));
|
|
729
740
|
});
|
|
@@ -735,7 +746,7 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
735
746
|
throw new Error(`'${executableFile}' not executable`);
|
|
736
747
|
}
|
|
737
748
|
if (!exitCallback) {
|
|
738
|
-
|
|
749
|
+
host.exit(1);
|
|
739
750
|
}
|
|
740
751
|
else {
|
|
741
752
|
const wrappedError = new CommanderError(1, 'commander.executeSubCommandAsync', '(error)');
|
|
@@ -1113,12 +1124,12 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
1113
1124
|
/** Apply environment variables to options that have no value from the cli or client code. */
|
|
1114
1125
|
_parseOptionsEnv() {
|
|
1115
1126
|
for (const option of this.options) {
|
|
1116
|
-
if (option.envVar && option.envVar in
|
|
1127
|
+
if (option.envVar && option.envVar in host.env) {
|
|
1117
1128
|
const optionKey = option.attributeName();
|
|
1118
1129
|
// Do not overwrite cli values or values from an unknown (client-code) source.
|
|
1119
1130
|
if (this.getOptionValue(optionKey) === undefined || ENV_SOURCES.includes(this.getOptionValueSource(optionKey) ?? '')) {
|
|
1120
1131
|
if (option.required || option.optional)
|
|
1121
|
-
this.emit(`optionEnv:${option.name()}`,
|
|
1132
|
+
this.emit(`optionEnv:${option.name()}`, host.env[option.envVar]);
|
|
1122
1133
|
else
|
|
1123
1134
|
this.emit(`optionEnv:${option.name()}`);
|
|
1124
1135
|
}
|
|
@@ -1428,7 +1439,7 @@ Expecting one of '${HOOK_EVENTS.join("', '")}'`);
|
|
|
1428
1439
|
/** Output help and exit. */
|
|
1429
1440
|
help(contextOptions) {
|
|
1430
1441
|
this.outputHelp(contextOptions);
|
|
1431
|
-
let exitCode = Number(
|
|
1442
|
+
let exitCode = Number(host.exitCode ?? 0);
|
|
1432
1443
|
if (exitCode === 0 && contextOptions && typeof contextOptions !== 'function' && contextOptions.error)
|
|
1433
1444
|
exitCode = 1;
|
|
1434
1445
|
// message: not all displayed text is available, so only a placeholder is passed.
|
|
@@ -1574,7 +1585,7 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
|
|
|
1574
1585
|
await root.parseAsync(args, { from: 'user', stdout: { write: (s) => out.push(s) }, stderr: { write: (s) => err.push(s) }, exit: (c) => void (code = c) });
|
|
1575
1586
|
return { stdout: out.join(''), stderr: err.join(''), code };
|
|
1576
1587
|
};
|
|
1577
|
-
return serveMcp(this.manifest, { input:
|
|
1588
|
+
return serveMcp(this.manifest, { input: host.stdin, output: { write: (s) => root._outputConfiguration.writeOut(s) }, invoke }).then(() => true);
|
|
1578
1589
|
}
|
|
1579
1590
|
return false;
|
|
1580
1591
|
}
|
|
@@ -1749,9 +1760,9 @@ function incrementNodeInspectorPort(args) {
|
|
|
1749
1760
|
* and CLICOLOR_FORCE enable, otherwise undecided (the stream's TTY-ness decides).
|
|
1750
1761
|
*/
|
|
1751
1762
|
export function useColor() {
|
|
1752
|
-
if (
|
|
1763
|
+
if (host.env['NO_COLOR'] || host.env['FORCE_COLOR'] === '0' || host.env['FORCE_COLOR'] === 'false')
|
|
1753
1764
|
return false;
|
|
1754
|
-
if (
|
|
1765
|
+
if (host.env['FORCE_COLOR'] || host.env['CLICOLOR_FORCE'] !== undefined)
|
|
1755
1766
|
return true;
|
|
1756
1767
|
return undefined;
|
|
1757
1768
|
}
|
package/dist/commander.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `burgee/commander` — commander's public surface, implemented over burgee (J9: no
|
|
3
3
|
* dependency on commander itself). Graded by commander's own suite; see
|
|
4
|
-
* `.sdlc/intents/commander-compat/
|
|
4
|
+
* `.sdlc/intents/commander-compat/spec.md` and `npm run compat`.
|
|
5
5
|
*/
|
|
6
6
|
import { Argument } from './commander/argument.js';
|
|
7
7
|
import { Command } from './commander/command.js';
|
package/dist/completions.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ interface FigOption {
|
|
|
26
26
|
name: string;
|
|
27
27
|
suggestions?: string[];
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Fig's own two keys for option relationships, spelled as Fig spells them:
|
|
31
|
+
* `dependsOn` keeps our name, `exclusive` becomes `exclusiveOn`. Both take the option's
|
|
32
|
+
* *spelling*, which is what Fig matches against on the command line — `fig-schema.test.ts`
|
|
33
|
+
* pins that these keys are ones `@withfig/autocomplete-types` declares on `Option`.
|
|
34
|
+
*/
|
|
35
|
+
dependsOn?: string[];
|
|
36
|
+
exclusiveOn?: string[];
|
|
29
37
|
}
|
|
30
38
|
interface FigSubcommand {
|
|
31
39
|
name: string;
|
package/dist/completions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { kebab } from './names.js';
|
|
1
|
+
import { flagsOf, kebab } from './names.js';
|
|
2
2
|
export const SHELLS = ['bash', 'zsh', 'fish', 'pwsh'];
|
|
3
3
|
const GLOBAL = {
|
|
4
4
|
json: { type: 'boolean', description: 'machine-readable output' },
|
|
@@ -201,6 +201,10 @@ function figOptions(node) {
|
|
|
201
201
|
o.description = s.description;
|
|
202
202
|
if (takesValue(s))
|
|
203
203
|
o.args = { name: s.placeholder ?? 'value', ...(s.choices === undefined ? {} : { suggestions: [...s.choices] }) };
|
|
204
|
+
if (s.dependsOn !== undefined && s.dependsOn.length > 0)
|
|
205
|
+
o.dependsOn = flagsOf(s.dependsOn);
|
|
206
|
+
if (s.exclusive !== undefined && s.exclusive.length > 0)
|
|
207
|
+
o.exclusiveOn = flagsOf(s.exclusive);
|
|
204
208
|
return o;
|
|
205
209
|
});
|
|
206
210
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What must be true of a command's declaration before it is added — whoever declared it.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `validate.ts` on 2026-09-16, and the weight lock is what asked for it. That
|
|
5
|
+
* file holds two unrelated jobs: these checks, which run once when a program is built, and
|
|
6
|
+
* the run-time half — relations, numbers, choices, Standard Schema — which runs on every
|
|
7
|
+
* invocation and is four fifths of the bytes. Nothing minded while the only caller was
|
|
8
|
+
* `defineCommand`, because the engine carries both anyway.
|
|
9
|
+
*
|
|
10
|
+
* The plugin host is what made it matter. `Manifest.use()` has to run these checks, so
|
|
11
|
+
* `plugin.ts` imports them, so **every** graph that reaches the manifest now reaches them —
|
|
12
|
+
* and that includes the commander and yargs front-ends, which reach the manifest and nothing
|
|
13
|
+
* else of the engine. Importing `validate.js` for `checkDefinition` put 6,409 bytes of
|
|
14
|
+
* run-time coercion into both front-ends to get 1,600 bytes of definition checking, and took
|
|
15
|
+
* `./commander` over 128,000 — the budget that exists to prove the front-end is no heavier
|
|
16
|
+
* than commander's own `lib/`. One file per job, and the front-ends pay for the job they use.
|
|
17
|
+
*/
|
|
18
|
+
import { type OptionSpec } from './manifest.js';
|
|
19
|
+
/**
|
|
20
|
+
* The fourth answer, which is not about the world: *this command is not offered to agents*.
|
|
21
|
+
*
|
|
22
|
+
* Exported because `mcp.ts` filters on it and a second spelling of a magic string is a
|
|
23
|
+
* second thing to keep in step. See `DeclaredEffects` in `manifest.ts` for why the word is
|
|
24
|
+
* `withheld` and not `none`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const WITHHELD = "withheld";
|
|
27
|
+
/**
|
|
28
|
+
* What must be true of a declaration before anything runs (yargs #1198, #887, #1679):
|
|
29
|
+
* a known type, one short alias per command, no two keys that meet on the command line.
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkDefinition(name: string, options: Record<string, OptionSpec>): void;
|
|
32
|
+
/**
|
|
33
|
+
* The whole door: the reserved names of V5, {@link checkDefinition}, and {@link checkEffects}.
|
|
34
|
+
*
|
|
35
|
+
* This exists as one function because it was two. `defineCommand` ran both; `Manifest.use()`
|
|
36
|
+
* ran neither, so a plugin's command was admitted unread — and a plugin option named `json`
|
|
37
|
+
* did not clash with the envelope flag, it replaced it in the parse config. The fix is not a
|
|
38
|
+
* second copy of the guard beside `use()`; it is that there is one guard and both callers
|
|
39
|
+
* reach it, which is the only arrangement a reader can check by looking.
|
|
40
|
+
*
|
|
41
|
+
* `effects` and `runs` are required rather than optional for exactly that reason. An optional
|
|
42
|
+
* third argument would be a check a caller can decline by writing nothing, which is the shape
|
|
43
|
+
* of the defect `checkEffects` exists to remove, one level up.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkCommand(name: string, options: Record<string, OptionSpec>, effects: unknown, runs: boolean): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { kebab } from './names.js';
|
|
2
|
+
const TYPES = new Set(['string', 'boolean', 'number']);
|
|
3
|
+
const EFFECTS = ['read_only', 'idempotent', 'non_idempotent'];
|
|
4
|
+
export const WITHHELD = 'withheld';
|
|
5
|
+
const DECLARED = [...EFFECTS, WITHHELD];
|
|
6
|
+
const RESERVED = new Set(['json', 'help', 'schema', 'mcp', 'version', 'explain']);
|
|
7
|
+
function checkRelationNames(name, key, spec, options) {
|
|
8
|
+
for (const field of ['dependsOn', 'exclusive']) {
|
|
9
|
+
for (const other of spec[field] ?? []) {
|
|
10
|
+
if (other !== key && other in options)
|
|
11
|
+
continue;
|
|
12
|
+
const why = other === key ? 'itself' : `not an option of "${name}"`;
|
|
13
|
+
throw new Error(`burgee: option "${key}" of "${name}" declares ${field} "${other}", which is ${why}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function checkDefinition(name, options) {
|
|
18
|
+
const shorts = new Map();
|
|
19
|
+
const flags = new Map();
|
|
20
|
+
for (const [key, spec] of Object.entries(options)) {
|
|
21
|
+
if (!TYPES.has(spec.type))
|
|
22
|
+
throw new Error(`burgee: option "${key}" of "${name}" has unknown type "${String(spec.type)}"`);
|
|
23
|
+
if (spec.short !== undefined) {
|
|
24
|
+
const owner = shorts.get(spec.short);
|
|
25
|
+
if (owner !== undefined)
|
|
26
|
+
throw new Error(`burgee: options "${owner}" and "${key}" of "${name}" both use -${spec.short}`);
|
|
27
|
+
shorts.set(spec.short, key);
|
|
28
|
+
}
|
|
29
|
+
const flag = kebab(key);
|
|
30
|
+
const clash = flags.get(flag);
|
|
31
|
+
if (clash !== undefined)
|
|
32
|
+
throw new Error(`burgee: options "${clash}" and "${key}" of "${name}" are both --${flag}`);
|
|
33
|
+
flags.set(flag, key);
|
|
34
|
+
if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
|
|
35
|
+
throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
|
|
36
|
+
}
|
|
37
|
+
checkRelationNames(name, key, spec, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function checkEffects(name, effects, runs) {
|
|
41
|
+
if (effects === undefined) {
|
|
42
|
+
if (!runs)
|
|
43
|
+
return;
|
|
44
|
+
throw new Error(`burgee: command "${name}" is runnable and declares no effects; declare ${EFFECTS.join(', ')} — or ${WITHHELD}, which serves it to people and keeps it out of the MCP tool list`);
|
|
45
|
+
}
|
|
46
|
+
if (!DECLARED.includes(effects)) {
|
|
47
|
+
throw new Error(`burgee: command "${name}" declares effects ${JSON.stringify(effects)}, which is not an effects; use ${DECLARED.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function checkCommand(name, options, effects, runs) {
|
|
51
|
+
for (const key of Object.keys(options)) {
|
|
52
|
+
if (RESERVED.has(key) || RESERVED.has(kebab(key)))
|
|
53
|
+
throw new Error(`burgee: option "${key}" is reserved and cannot be redefined`);
|
|
54
|
+
}
|
|
55
|
+
checkDefinition(name, options);
|
|
56
|
+
checkEffects(name, effects, runs);
|
|
57
|
+
}
|
package/dist/execute.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ArgumentSpec, type CommandNode, type
|
|
1
|
+
import { type ArgumentSpec, type CommandNode, type DeclaredEffects, type Example, type LazyModule, Manifest, type OptionSpec, type Relation, type RunContext } from './manifest.js';
|
|
2
2
|
export interface CommandContext<O> extends Omit<RunContext, 'options'> {
|
|
3
3
|
options: O;
|
|
4
4
|
}
|
|
@@ -33,8 +33,17 @@ export interface Command<S extends OptionSpecs = OptionSpecs> {
|
|
|
33
33
|
epilogue?: string;
|
|
34
34
|
hidden?: boolean;
|
|
35
35
|
deprecated?: boolean | string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* What running it does to the world (N6). Declaring one of the three is what exposes the
|
|
38
|
+
* command as an MCP tool (N2); `'withheld'` declares that it is not offered to agents.
|
|
39
|
+
*
|
|
40
|
+
* Optional on the type and **required at definition time** on a command that runs:
|
|
41
|
+
* `defineCommand` refuses one that omits it. It stays optional here because a group that
|
|
42
|
+
* only holds subcommands declares none, and TypeScript cannot make a field's presence
|
|
43
|
+
* depend on a sibling's without splitting `Command` into a union that would cost the
|
|
44
|
+
* option-spec inference every caller of this type relies on.
|
|
45
|
+
*/
|
|
46
|
+
effects?: DeclaredEffects;
|
|
38
47
|
/** Relationships between options, validated before choices and the handler (S2, S6). */
|
|
39
48
|
relations?: readonly Relation[];
|
|
40
49
|
/** Absent on a group that only holds subcommands. `NoInfer`: the spec fixes S, the handler only reads it. */
|