burgee 0.7.1 → 0.8.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.
- package/dist/cli.d.ts +25 -3
- package/dist/cli.js +25 -7
- package/dist/commander/argument.js +0 -3
- package/dist/commander/command.d.ts +7 -3
- package/dist/commander/command.js +50 -186
- package/dist/commander/error.js +0 -2
- package/dist/commander/help.js +0 -17
- package/dist/commander/option.js +0 -14
- package/dist/compat.d.ts +30 -0
- package/dist/compat.js +4 -0
- package/dist/definition.d.ts +11 -2
- package/dist/definition.js +9 -11
- package/dist/execute.js +5 -1
- package/dist/mcp.d.ts +33 -13
- package/dist/mcp.js +4 -3
- package/dist/migrate.d.ts +142 -0
- package/dist/migrate.js +284 -0
- package/dist/yargs/burgee.js +0 -14
- package/dist/yargs/cliui.js +0 -53
- package/dist/yargs/command.js +0 -7
- package/dist/yargs/completion.js +0 -5
- package/dist/yargs/factory.js +0 -55
- package/dist/yargs/middleware.js +0 -5
- package/dist/yargs/shim.js +0 -20
- package/dist/yargs/usage.js +0 -8
- package/dist/yargs/utils.js +0 -11
- package/dist/yargs/validation.js +0 -6
- package/dist/yargs/y18n.js +0 -6
- package/package.json +3 -3
package/dist/commander/error.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** commander's error classes, byte-for-byte in shape: `code`, `exitCode`, `nestedError`. */
|
|
2
1
|
export class CommanderError extends Error {
|
|
3
2
|
code;
|
|
4
3
|
exitCode;
|
|
@@ -19,4 +18,3 @@ export class InvalidArgumentError extends CommanderError {
|
|
|
19
18
|
this.name = this.constructor.name;
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
|
-
//# sourceMappingURL=error.js.map
|
package/dist/commander/help.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
2
|
import { humanReadableArgName } from './argument.js';
|
|
3
|
-
/** Methods are static in style so a subclass, or plain functions via `configureHelp`, can override them. */
|
|
4
3
|
export class Help {
|
|
5
4
|
helpWidth = undefined;
|
|
6
5
|
minWidthToWrap = 40;
|
|
7
6
|
sortSubcommands = false;
|
|
8
7
|
sortOptions = false;
|
|
9
8
|
showGlobalOptions = false;
|
|
10
|
-
/** Called after `configureHelp` overrides are applied and just before `formatHelp`. */
|
|
11
9
|
prepareContext(contextOptions) {
|
|
12
10
|
this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
|
|
13
11
|
}
|
|
@@ -28,7 +26,6 @@ export class Help {
|
|
|
28
26
|
const visibleOptions = cmd.options.filter((option) => !option.hidden);
|
|
29
27
|
const helpOption = cmd._getHelpOption();
|
|
30
28
|
if (helpOption && !helpOption.hidden) {
|
|
31
|
-
// Historical: hide the built-in help flags a user option already took.
|
|
32
29
|
const removeShort = helpOption.short && cmd._findOption(helpOption.short);
|
|
33
30
|
const removeLong = helpOption.long && cmd._findOption(helpOption.long);
|
|
34
31
|
if (!removeShort && !removeLong)
|
|
@@ -54,7 +51,6 @@ export class Help {
|
|
|
54
51
|
return globalOptions;
|
|
55
52
|
}
|
|
56
53
|
visibleArguments(cmd) {
|
|
57
|
-
// Side effect: apply the legacy descriptions before the arguments are displayed.
|
|
58
54
|
if (cmd._argsDescription) {
|
|
59
55
|
for (const argument of cmd.registeredArguments) {
|
|
60
56
|
argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
|
|
@@ -110,7 +106,6 @@ export class Help {
|
|
|
110
106
|
commandDescription(cmd) {
|
|
111
107
|
return cmd.description();
|
|
112
108
|
}
|
|
113
|
-
/** Summary, falling back to description for backwards compatibility. */
|
|
114
109
|
subcommandDescription(cmd) {
|
|
115
110
|
return cmd.summary() || cmd.description();
|
|
116
111
|
}
|
|
@@ -120,7 +115,6 @@ export class Help {
|
|
|
120
115
|
extraInfo.push(`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
|
|
121
116
|
}
|
|
122
117
|
if (option.defaultValue !== undefined) {
|
|
123
|
-
// Defaults for boolean and negated are more for the programmer than the end user.
|
|
124
118
|
const showDefault = option.required || option.optional || (option.isBoolean() && typeof option.defaultValue === 'boolean');
|
|
125
119
|
if (showDefault)
|
|
126
120
|
extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
|
|
@@ -154,7 +148,6 @@ export class Help {
|
|
|
154
148
|
return [];
|
|
155
149
|
return [helper.styleTitle(heading), ...items, ''];
|
|
156
150
|
}
|
|
157
|
-
/** Groups in order of first appearance among all items; members in order of the visible items. */
|
|
158
151
|
groupItems(unsortedItems, visibleItems, getGroup) {
|
|
159
152
|
const result = new Map();
|
|
160
153
|
for (const item of unsortedItems) {
|
|
@@ -203,7 +196,6 @@ export class Help {
|
|
|
203
196
|
}
|
|
204
197
|
return output.join('\n');
|
|
205
198
|
}
|
|
206
|
-
/** Width ignoring ANSI escapes, for padding and wrapping. */
|
|
207
199
|
displayWidth(str) {
|
|
208
200
|
return stripVTControlCharacters(str).length;
|
|
209
201
|
}
|
|
@@ -211,7 +203,6 @@ export class Help {
|
|
|
211
203
|
return str;
|
|
212
204
|
}
|
|
213
205
|
styleUsage(str) {
|
|
214
|
-
// Assume the default usage shape: command subcommand [options] [command] <foo> [bar]
|
|
215
206
|
return str
|
|
216
207
|
.split(' ')
|
|
217
208
|
.map((word) => {
|
|
@@ -273,15 +264,9 @@ export class Help {
|
|
|
273
264
|
padWidth(cmd, helper) {
|
|
274
265
|
return Math.max(helper.longestOptionTermLength(cmd, helper), helper.longestGlobalOptionTermLength(cmd, helper), helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper));
|
|
275
266
|
}
|
|
276
|
-
/** Manually wrapped text: a line break followed by whitespace. */
|
|
277
267
|
preformatted(str) {
|
|
278
268
|
return /\n[^\S\r\n]/.test(str);
|
|
279
269
|
}
|
|
280
|
-
/**
|
|
281
|
-
* Pad the term and wrap the description, indenting the following lines:
|
|
282
|
-
* TTT DDD DDDD
|
|
283
|
-
* DD DDD
|
|
284
|
-
*/
|
|
285
270
|
formatItem(term, termWidth, description, helper) {
|
|
286
271
|
const itemIndent = 2;
|
|
287
272
|
const itemIndentStr = ' '.repeat(itemIndent);
|
|
@@ -301,7 +286,6 @@ export class Help {
|
|
|
301
286
|
}
|
|
302
287
|
return itemIndentStr + paddedTerm + ' '.repeat(spacerWidth) + formattedDescription.replace(/\n/g, `\n${itemIndentStr}`);
|
|
303
288
|
}
|
|
304
|
-
/** Wrap at whitespace, preserving existing line breaks; skipped below `minWidthToWrap`. */
|
|
305
289
|
boxWrap(str, width) {
|
|
306
290
|
if (width < this.minWidthToWrap)
|
|
307
291
|
return str;
|
|
@@ -333,4 +317,3 @@ export class Help {
|
|
|
333
317
|
return wrappedLines.join('\n');
|
|
334
318
|
}
|
|
335
319
|
}
|
|
336
|
-
//# sourceMappingURL=help.js.map
|
package/dist/commander/option.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {} from './argument.js';
|
|
2
1
|
import { InvalidArgumentError } from './error.js';
|
|
3
2
|
export class Option {
|
|
4
3
|
flags;
|
|
@@ -25,7 +24,6 @@ export class Option {
|
|
|
25
24
|
this.description = description || '';
|
|
26
25
|
this.required = flags.includes('<');
|
|
27
26
|
this.optional = flags.includes('[');
|
|
28
|
-
// `<value,...>` describes custom splitting of one argument, not a variadic option.
|
|
29
27
|
this.variadic = /\w\.\.\.[>\]]$/.test(flags);
|
|
30
28
|
const { shortFlag, longFlag } = splitOptionFlags(flags);
|
|
31
29
|
this.short = shortFlag;
|
|
@@ -38,7 +36,6 @@ export class Option {
|
|
|
38
36
|
this.defaultValueDescription = description;
|
|
39
37
|
return this;
|
|
40
38
|
}
|
|
41
|
-
/** Value used when the option appears without an option-argument. `parseArg` still runs. */
|
|
42
39
|
preset(arg) {
|
|
43
40
|
this.presetArg = arg;
|
|
44
41
|
return this;
|
|
@@ -47,7 +44,6 @@ export class Option {
|
|
|
47
44
|
this.conflictsWith = this.conflictsWith.concat(names);
|
|
48
45
|
return this;
|
|
49
46
|
}
|
|
50
|
-
/** Values implied for other options when this one is set and they are not. `parseArg` does not run on them. */
|
|
51
47
|
implies(impliedOptionValues) {
|
|
52
48
|
const newImplied = typeof impliedOptionValues === 'string' ? { [impliedOptionValues]: true } : impliedOptionValues;
|
|
53
49
|
this.implied = Object.assign(this.implied ?? {}, newImplied);
|
|
@@ -90,7 +86,6 @@ export class Option {
|
|
|
90
86
|
return this.long.replace(/^--/, '');
|
|
91
87
|
return (this.short ?? '').replace(/^-/, '');
|
|
92
88
|
}
|
|
93
|
-
/** camelCase key used on the options object; `--no-foo` shares `foo` with `--foo`. */
|
|
94
89
|
attributeName() {
|
|
95
90
|
return camelcase(this.negate ? this.name().replace(/^no-/, '') : this.name());
|
|
96
91
|
}
|
|
@@ -101,15 +96,10 @@ export class Option {
|
|
|
101
96
|
is(arg) {
|
|
102
97
|
return this.short === arg || this.long === arg;
|
|
103
98
|
}
|
|
104
|
-
/** Options are one of boolean, negated, required-argument or optional-argument. */
|
|
105
99
|
isBoolean() {
|
|
106
100
|
return !this.required && !this.optional && !this.negate;
|
|
107
101
|
}
|
|
108
102
|
}
|
|
109
|
-
/**
|
|
110
|
-
* `--build` and `--no-build` share one value. This works out which of the pair a
|
|
111
|
-
* value (probably) came from, so implied values are only applied for the right one.
|
|
112
|
-
*/
|
|
113
103
|
export class DualOptions {
|
|
114
104
|
positiveOptions = new Map();
|
|
115
105
|
negativeOptions = new Map();
|
|
@@ -135,7 +125,6 @@ export class DualOptions {
|
|
|
135
125
|
function camelcase(str) {
|
|
136
126
|
return str.split('-').reduce((acc, word) => acc + (word[0] ?? '').toUpperCase() + word.slice(1));
|
|
137
127
|
}
|
|
138
|
-
/** Split `'-m,--mixed <value>'` into its short and long flags, failing noisily on anything else. */
|
|
139
128
|
export function splitOptionFlags(flags) {
|
|
140
129
|
let shortFlag;
|
|
141
130
|
let longFlag;
|
|
@@ -147,10 +136,8 @@ export function splitOptionFlags(flags) {
|
|
|
147
136
|
shortFlag = flagParts.shift();
|
|
148
137
|
if (longFlagExp.test(head()))
|
|
149
138
|
longFlag = flagParts.shift();
|
|
150
|
-
// Long then short. Rarely used but fine.
|
|
151
139
|
if (!shortFlag && shortFlagExp.test(head()))
|
|
152
140
|
shortFlag = flagParts.shift();
|
|
153
|
-
// Two long flags, like '--ws, --workspace': the supported way to have a shortish flag.
|
|
154
141
|
if (!shortFlag && longFlagExp.test(head())) {
|
|
155
142
|
shortFlag = longFlag;
|
|
156
143
|
longFlag = flagParts.shift();
|
|
@@ -175,4 +162,3 @@ export function splitOptionFlags(flags) {
|
|
|
175
162
|
}
|
|
176
163
|
return { shortFlag, longFlag };
|
|
177
164
|
}
|
|
178
|
-
//# sourceMappingURL=option.js.map
|
package/dist/compat.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A7 — the graded pass rate per host, as `burgee migrate` reports it.
|
|
3
|
+
*
|
|
4
|
+
* These are `compat-oracle`'s own numbers: the incumbent's test suite, vendored and run
|
|
5
|
+
* against burgee's front-end, which is what "drop-in" means here and the only claim in the
|
|
6
|
+
* package that is measured rather than argued.
|
|
7
|
+
*
|
|
8
|
+
* **They live here as a data module rather than as a literal in the report**, and
|
|
9
|
+
* `compat-baseline-lock.test.ts` holds this file equal to
|
|
10
|
+
* `packages/compat-oracle/baseline/<host>.json` — the same files the compat page renders.
|
|
11
|
+
* Changing a number here without the measurement moving fails that lock, which is the
|
|
12
|
+
* whole point: a number typed into a report template is a number that goes stale silently,
|
|
13
|
+
* and this repository has published four of those and caught them all late.
|
|
14
|
+
*
|
|
15
|
+
* It cannot be read from the oracle at run time. `compat-oracle` is `private: true` and is
|
|
16
|
+
* never published, so a user who installs `burgee` has no baseline directory to read; a
|
|
17
|
+
* copy with a lock on it is the strongest control available on the published side, and the
|
|
18
|
+
* lock is what makes it a copy rather than a claim.
|
|
19
|
+
*/
|
|
20
|
+
/** One row of the baseline, in the shape `baseline/<host>.json` stores it. */
|
|
21
|
+
export interface Graded {
|
|
22
|
+
/** Cases in the incumbent's own suite. */
|
|
23
|
+
reference: number;
|
|
24
|
+
/** Cases that pass against burgee's front-end. */
|
|
25
|
+
passed: number;
|
|
26
|
+
/** `passed / reference`, carried rather than recomputed so it is the oracle's own division. */
|
|
27
|
+
rate: number;
|
|
28
|
+
}
|
|
29
|
+
/** The two hosts `migrate` rewrites, and nothing else — a row here without a mapping would claim a path that does not exist. */
|
|
30
|
+
export declare const GRADED: Readonly<Record<string, Graded>>;
|
package/dist/compat.js
ADDED
package/dist/definition.d.ts
CHANGED
|
@@ -26,11 +26,20 @@ import { type OptionSpec } from './manifest.js';
|
|
|
26
26
|
export declare const WITHHELD = "withheld";
|
|
27
27
|
/**
|
|
28
28
|
* What must be true of a declaration before anything runs (yargs #1198, #887, #1679):
|
|
29
|
-
* a known type, one short alias per command, no two keys that meet on the command line
|
|
29
|
+
* a known type, one short alias per command, no two keys that meet on the command line, and
|
|
30
|
+
* none of V5's reserved surfaces.
|
|
31
|
+
*
|
|
32
|
+
* The reserved names used to be a second loop over the same keys in {@link checkCommand},
|
|
33
|
+
* and the numeric bound used to be a fourth branch in this one. Both moved to pay for the
|
|
34
|
+
* canonical-key check below without raising a weight ceiling — `./plugin` had 5 bytes of
|
|
35
|
+
* headroom — and both are better where they are now: `flag` is computed once here and
|
|
36
|
+
* `kebab` is the identity on every reserved name, and a numeric bound is a fact about a
|
|
37
|
+
* spec rather than about a name. The file is 51 bytes smaller than before the check existed.
|
|
30
38
|
*/
|
|
31
39
|
export declare function checkDefinition(name: string, options: Record<string, OptionSpec>): void;
|
|
32
40
|
/**
|
|
33
|
-
* The whole door:
|
|
41
|
+
* The whole door: {@link checkDefinition} — which carries V5's reserved names — and
|
|
42
|
+
* {@link checkEffects}.
|
|
34
43
|
*
|
|
35
44
|
* This exists as one function because it was two. `defineCommand` ran both; `Manifest.use()`
|
|
36
45
|
* ran neither, so a plugin's command was admitted unread — and a plugin option named `json`
|
package/dist/definition.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { kebab } from './names.js';
|
|
1
|
+
import { camel, kebab } from './names.js';
|
|
2
2
|
const TYPES = new Set(['string', 'boolean', 'number']);
|
|
3
3
|
const EFFECTS = ['read_only', 'idempotent', 'non_idempotent'];
|
|
4
4
|
export const WITHHELD = 'withheld';
|
|
5
5
|
const DECLARED = [...EFFECTS, WITHHELD];
|
|
6
6
|
const RESERVED = new Set(['json', 'help', 'schema', 'mcp', 'version', 'explain']);
|
|
7
|
-
function
|
|
7
|
+
function checkSpec(name, key, spec, options) {
|
|
8
|
+
if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
|
|
9
|
+
throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
|
|
10
|
+
}
|
|
8
11
|
for (const field of ['dependsOn', 'exclusive']) {
|
|
9
12
|
for (const other of spec[field] ?? []) {
|
|
10
13
|
if (other !== key && other in options)
|
|
@@ -27,14 +30,13 @@ export function checkDefinition(name, options) {
|
|
|
27
30
|
shorts.set(spec.short, key);
|
|
28
31
|
}
|
|
29
32
|
const flag = kebab(key);
|
|
30
|
-
|
|
33
|
+
if (RESERVED.has(flag))
|
|
34
|
+
throw new Error(`burgee: option "${key}" is reserved and cannot be redefined`);
|
|
35
|
+
const clash = flags.get(flag) ?? (camel(flag) === key ? undefined : camel(flag));
|
|
31
36
|
if (clash !== undefined)
|
|
32
37
|
throw new Error(`burgee: options "${clash}" and "${key}" of "${name}" are both --${flag}`);
|
|
33
38
|
flags.set(flag, key);
|
|
34
|
-
|
|
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);
|
|
39
|
+
checkSpec(name, key, spec, options);
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
function checkEffects(name, effects, runs) {
|
|
@@ -48,10 +50,6 @@ function checkEffects(name, effects, runs) {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
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
53
|
checkDefinition(name, options);
|
|
56
54
|
checkEffects(name, effects, runs);
|
|
57
55
|
}
|
package/dist/execute.js
CHANGED
|
@@ -360,6 +360,10 @@ function changedOf(node, data) {
|
|
|
360
360
|
}
|
|
361
361
|
return undefined;
|
|
362
362
|
}
|
|
363
|
+
function exitCodeOf(data) {
|
|
364
|
+
const code = isPlainObject(data) ? data['exitCode'] : undefined;
|
|
365
|
+
return isExitCode(code) ? code : ExitCode.OK;
|
|
366
|
+
}
|
|
363
367
|
function versionOf(manifest, io) {
|
|
364
368
|
const declared = manifest.version ?? (typeof io.pkg?.data['version'] === 'string' ? io.pkg.data['version'] : undefined);
|
|
365
369
|
if (declared === undefined)
|
|
@@ -420,7 +424,7 @@ async function emit(io, outcome) {
|
|
|
420
424
|
const meta = { provenance: outcome.provenance ?? {}, ...(outcome.changed === undefined ? {} : { changed: outcome.changed }) };
|
|
421
425
|
const envelope = { ok: true, data: outcome.data, meta };
|
|
422
426
|
io.out.write(outcome.json ? `${JSON.stringify(envelope)}\n` : `${render(outcome.data)}\n`);
|
|
423
|
-
return await leave(io,
|
|
427
|
+
return await leave(io, exitCodeOf(outcome.data));
|
|
424
428
|
}
|
|
425
429
|
async function report(cause, { manifest, io, argv, json, name }) {
|
|
426
430
|
const failure = await describeFailure(cause, argv, resolveCommand(manifest, argv) ?? undefined);
|
package/dist/mcp.d.ts
CHANGED
|
@@ -2,9 +2,15 @@ import { type CommandNode, type Effects, type Manifest } from './manifest.js';
|
|
|
2
2
|
import { type JsonSchema } from './schema.js';
|
|
3
3
|
export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
4
4
|
export interface ToolAnnotations {
|
|
5
|
-
readOnlyHint
|
|
6
|
-
idempotentHint
|
|
7
|
-
destructiveHint
|
|
5
|
+
readOnlyHint?: boolean;
|
|
6
|
+
idempotentHint?: boolean;
|
|
7
|
+
destructiveHint?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* `'undeclared'`, and only ever that (G1). It appears on a command whose author said
|
|
10
|
+
* nothing — every commander and yargs command that did not call `.effects()` — and never
|
|
11
|
+
* beside a hint, because a hint is what a declaration produces.
|
|
12
|
+
*/
|
|
13
|
+
effects?: 'undeclared';
|
|
8
14
|
}
|
|
9
15
|
export interface Tool {
|
|
10
16
|
name: string;
|
|
@@ -18,20 +24,34 @@ export type Invoke = (argv: string[]) => Promise<{
|
|
|
18
24
|
stderr: string;
|
|
19
25
|
code: number;
|
|
20
26
|
}>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
27
|
+
/**
|
|
28
|
+
* MCP's hints, from the declared effects. `destructiveHint` is only ever false by declaration.
|
|
29
|
+
*
|
|
30
|
+
* No declaration returns no hints (G1). That is not a gap: MCP defines a default for each of
|
|
31
|
+
* the three — `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false` — so an
|
|
32
|
+
* absent hint already reads as *assume the worst*, in the client's own vocabulary and without
|
|
33
|
+
* burgee inventing a value it has no basis for. `effects: 'undeclared'` is the positive half:
|
|
34
|
+
* this command is not a `read_only` one, and it is not a `withheld` one either — nobody said.
|
|
35
|
+
*/
|
|
36
|
+
export declare function annotationsOf(effects?: Effects): ToolAnnotations;
|
|
23
37
|
/** `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`. */
|
|
24
38
|
export declare const toolName: (node: CommandNode, root: string[]) => string;
|
|
25
39
|
/**
|
|
26
|
-
* The tool list: every runnable, visible command
|
|
40
|
+
* The tool list: every runnable, visible command an author has not withheld.
|
|
41
|
+
*
|
|
42
|
+
* One word is absent and one is not, and until 2026-09-21 they were the same thing. An
|
|
43
|
+
* author who wrote `'withheld'` thought about it and said no; that is what the word is for
|
|
44
|
+
* and it still means absent. An author who wrote nothing — which on burgee's own API
|
|
45
|
+
* `checkCommand` refuses, and which **every** command built through the commander or yargs
|
|
46
|
+
* façade is, because neither incumbent has a notion of effects and neither can be made to
|
|
47
|
+
* acquire one without breaking the suites that grade the façades — was treated the same way,
|
|
48
|
+
* so a migrated user's whole program was silently not a tool.
|
|
27
49
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* without breaking the suites that grade the façades. The filter treats both as *not a
|
|
34
|
-
* tool*, which is the same conservative reading it always had.
|
|
50
|
+
* Reading silence as refusal was conservative and it was also the thing standing between the
|
|
51
|
+
* product and its own pitch. Absent-from-the-list is strictly worse for the caller than
|
|
52
|
+
* present-with-honest-annotations: an agent that cannot see a command cannot decide about it,
|
|
53
|
+
* and cannot ask. So an undeclared command is listed and says so — see {@link annotationsOf}
|
|
54
|
+
* for why it carries no hints rather than a reassuring default.
|
|
35
55
|
*/
|
|
36
56
|
export declare function toolsOf(manifest: Manifest): Tool[];
|
|
37
57
|
/** A tool call's arguments back into argv: the command, its options, `--json`, then positionals in declared order. */
|
package/dist/mcp.js
CHANGED
|
@@ -6,6 +6,8 @@ const JSON_RPC_INVALID_REQUEST = -32600;
|
|
|
6
6
|
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
7
7
|
const JSON_RPC_INVALID_PARAMS = -32602;
|
|
8
8
|
export function annotationsOf(effects) {
|
|
9
|
+
if (effects === undefined)
|
|
10
|
+
return { effects: 'undeclared' };
|
|
9
11
|
return {
|
|
10
12
|
readOnlyHint: effects === 'read_only',
|
|
11
13
|
idempotentHint: effects !== 'non_idempotent',
|
|
@@ -21,7 +23,7 @@ function describe(node) {
|
|
|
21
23
|
}
|
|
22
24
|
export function toolsOf(manifest) {
|
|
23
25
|
return runnable(manifest)
|
|
24
|
-
.filter((c) => c.effects !==
|
|
26
|
+
.filter((c) => c.effects !== WITHHELD)
|
|
25
27
|
.map((c) => ({ name: toolName(c, manifest.rootPath), description: describe(c), inputSchema: inputSchemaOf(c), annotations: annotationsOf(c.effects) }));
|
|
26
28
|
}
|
|
27
29
|
function optionArgs(node, args) {
|
|
@@ -59,8 +61,7 @@ async function callTool(session, params) {
|
|
|
59
61
|
const given = params ?? {};
|
|
60
62
|
const raw = given['name'];
|
|
61
63
|
const name = typeof raw === 'string' ? raw : '';
|
|
62
|
-
const
|
|
63
|
-
const node = exposed.has(name) ? runnable(manifest).find((c) => toolName(c, root) === name) : undefined;
|
|
64
|
+
const node = runnable(manifest).find((c) => c.effects !== WITHHELD && toolName(c, root) === name);
|
|
64
65
|
if (node === undefined)
|
|
65
66
|
return { error: { code: JSON_RPC_INVALID_PARAMS, message: `unknown tool "${name}"` } };
|
|
66
67
|
const args = (given['arguments'] ?? {});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { type Graded } from './compat.js';
|
|
2
|
+
/**
|
|
3
|
+
* A2 — the whole mapping, as data.
|
|
4
|
+
*
|
|
5
|
+
* Whole specifiers only. `burgee/commander` is not a key, which is what makes a second run
|
|
6
|
+
* over an already-migrated tree a no-op and lets the command declare `effects: 'idempotent'`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAPPING: Readonly<Record<string, string>>;
|
|
9
|
+
/** The packages a project depends on that this command is about (A1). */
|
|
10
|
+
export declare const HOSTS: readonly ["commander", "yargs"];
|
|
11
|
+
/** Why a file was left untouched. Both are named positions, never a guess (A4). */
|
|
12
|
+
export type RefusalReason = 'deep-import' | 'non-literal-specifier';
|
|
13
|
+
export interface Refusal {
|
|
14
|
+
/** Relative to the directory being migrated, with forward slashes on every platform. */
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
/** The specifier as written, or `''` for a dynamic specifier that is not a literal. */
|
|
18
|
+
specifier: string;
|
|
19
|
+
reason: RefusalReason;
|
|
20
|
+
}
|
|
21
|
+
/** One rewritten specifier, for the per-mapping rollup the report prints. */
|
|
22
|
+
export interface Mapped {
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
imports: number;
|
|
26
|
+
files: number;
|
|
27
|
+
}
|
|
28
|
+
export interface Detection {
|
|
29
|
+
/** Hosts named in `package.json`'s dependencies — declared. */
|
|
30
|
+
declared: string[];
|
|
31
|
+
/** Hosts a source file actually imports — used. These two disagree more often than not. */
|
|
32
|
+
imported: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface MigrationReport {
|
|
35
|
+
files: number;
|
|
36
|
+
imports: number;
|
|
37
|
+
mapped: Mapped[];
|
|
38
|
+
refused: Refusal[];
|
|
39
|
+
detected: Detection;
|
|
40
|
+
dependencies: {
|
|
41
|
+
before: string[];
|
|
42
|
+
removable: string[];
|
|
43
|
+
after: number;
|
|
44
|
+
};
|
|
45
|
+
graded: (Graded & {
|
|
46
|
+
host: string;
|
|
47
|
+
})[];
|
|
48
|
+
dryRun: boolean;
|
|
49
|
+
/** N7 — an idempotent command says whether it changed anything; silence is what an agent misreads. */
|
|
50
|
+
changed: boolean;
|
|
51
|
+
/** A8 — `RUNTIME` when anything was refused, so an agent branches on the code. */
|
|
52
|
+
exitCode: number;
|
|
53
|
+
}
|
|
54
|
+
interface Site {
|
|
55
|
+
specifier: string;
|
|
56
|
+
/** Offsets of the specifier's text, excluding the quotes. */
|
|
57
|
+
start: number;
|
|
58
|
+
end: number;
|
|
59
|
+
line: number;
|
|
60
|
+
}
|
|
61
|
+
interface Scan {
|
|
62
|
+
sites: Site[];
|
|
63
|
+
/** `import(x)` / `require(x)` where `x` is not a string literal — refused, never guessed. */
|
|
64
|
+
nonLiteral: number[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One linear pass over the text: comments, strings, templates and regular expressions are
|
|
68
|
+
* skipped as the shapes they are, and every quoted literal is classified by the two code
|
|
69
|
+
* tokens in front of it.
|
|
70
|
+
*
|
|
71
|
+
* It is a scan, not a parse. What it cannot classify it refuses (A4); what it can, it knows
|
|
72
|
+
* exactly, because a module specifier is a string literal in one of five positions and
|
|
73
|
+
* nothing else in the grammar looks like that.
|
|
74
|
+
*/
|
|
75
|
+
export declare function scan(source: string): Scan;
|
|
76
|
+
export interface Rewrite {
|
|
77
|
+
source: string;
|
|
78
|
+
mapped: {
|
|
79
|
+
from: string;
|
|
80
|
+
to: string;
|
|
81
|
+
}[];
|
|
82
|
+
refused: Omit<Refusal, 'file'>[];
|
|
83
|
+
/** Whether this file references a host at all. A file that does not is not "untouched", it is unrelated. */
|
|
84
|
+
relevant: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A file that names neither host anywhere cannot contain a specifier for one — and in a real
|
|
88
|
+
* repository that is almost every file.
|
|
89
|
+
*
|
|
90
|
+
* `includes` is a `memmem`; `scan` is a character loop. It is not what makes the command
|
|
91
|
+
* fast (the whole scan phase is 25 ms of a ~400 ms run over 1,000 files) — it is what keeps
|
|
92
|
+
* the work proportional to the files that could matter rather than to the repository, and
|
|
93
|
+
* on a `Buffer` it also skips decoding 3 MB of UTF-8 that nothing was going to read.
|
|
94
|
+
*/
|
|
95
|
+
export declare function mentionsAHost(source: string | Buffer): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* A2/A5 — map every host specifier in one file, or map none of them.
|
|
98
|
+
*
|
|
99
|
+
* The edits are applied from the end backwards so earlier offsets stay valid, and the
|
|
100
|
+
* source is returned unchanged the moment there is a refusal in it: the unit of success is
|
|
101
|
+
* the file, so the worst case is *nothing changed here, and here is why* (D-051).
|
|
102
|
+
*/
|
|
103
|
+
export declare function rewriteSource(source: string): Rewrite;
|
|
104
|
+
/** Every source file under `dir`, relative and slash-separated so a report reads the same everywhere. */
|
|
105
|
+
export declare function sourceFiles(dir: string, at?: string, found?: string[]): string[];
|
|
106
|
+
/**
|
|
107
|
+
* `git status --porcelain`, or `undefined` where there is no repository to ask (A6).
|
|
108
|
+
*
|
|
109
|
+
* Through `bellpull` rather than `node:child_process`, because spawning is bellpull's job
|
|
110
|
+
* and `scripts/inline-implementation-lock.test.ts` says so — it caught the first draft of
|
|
111
|
+
* this file, which reached for `execFileSync` out of habit. burgee already depends on the
|
|
112
|
+
* package, and `migrate.js` is loaded lazily, so nothing that does not run `migrate` pays
|
|
113
|
+
* for the edge.
|
|
114
|
+
*
|
|
115
|
+
* Not a repository, no `git` on `PATH`, an unreadable directory: all the same answer, which
|
|
116
|
+
* is *there is nothing here that could be dirty*. A6 protects a reviewable diff, and where
|
|
117
|
+
* there is no repository there is no diff to protect — refusing instead would make the
|
|
118
|
+
* command unusable on a tarball for a safety that was never available.
|
|
119
|
+
*/
|
|
120
|
+
export declare function workingTree(dir: string): Promise<string[] | undefined>;
|
|
121
|
+
/** A6 — a dirty tree has no reviewable diff to add to, so the command declines rather than writes. */
|
|
122
|
+
export declare class DirtyTreeError extends Error {
|
|
123
|
+
readonly entries: string[];
|
|
124
|
+
readonly fix = "commit or stash your changes, or pass --force";
|
|
125
|
+
constructor(entries: string[]);
|
|
126
|
+
}
|
|
127
|
+
export interface MigrateOptions {
|
|
128
|
+
dir: string;
|
|
129
|
+
dryRun?: boolean;
|
|
130
|
+
force?: boolean;
|
|
131
|
+
/** Injected by the tests; the real `git status --porcelain` otherwise. */
|
|
132
|
+
status?: (dir: string) => string[] | undefined | Promise<string[] | undefined>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Migrate one project: detect, rewrite, report (A1, A7, A8).
|
|
136
|
+
*
|
|
137
|
+
* Non-interactive by design (D-052). The second audience is an agent migrating a
|
|
138
|
+
* repository unattended, and a prompt is a wall; safety is `--dry-run` and the refusal on
|
|
139
|
+
* a dirty tree, not a question.
|
|
140
|
+
*/
|
|
141
|
+
export declare function migrate(options: MigrateOptions): Promise<MigrationReport>;
|
|
142
|
+
export {};
|