burgee 0.10.0 → 0.11.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 +9 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1 -0
- package/dist/commander/command.d.ts +31 -8
- package/dist/commander/command.js +8 -1
- package/dist/commander.d.ts +8 -2
- package/dist/completions.js +1 -1
- package/dist/execute.js +3 -3
- package/dist/manifest.d.ts +7 -0
- package/dist/mcp.d.ts +5 -1
- package/dist/mcp.js +9 -7
- package/dist/migrate.d.ts +56 -2
- package/dist/migrate.js +152 -10
- package/dist/yargs/types.d.ts +987 -0
- package/dist/yargs/types.js +1 -0
- package/dist/yargs.d.ts +10 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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
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
|
-
<img src="https://img.shields.io/badge/Node.js-
|
|
18
|
+
<img src="https://img.shields.io/badge/Node.js-20.19%2B%20%7C%2022.13%2B-green.svg?style=flat-square" alt="Node.js 20.19+ or 22.13+" />
|
|
19
19
|
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -46,6 +46,7 @@ run(defineCommand({
|
|
|
46
46
|
name: 'greet',
|
|
47
47
|
description: 'Greet someone by name',
|
|
48
48
|
options: { name: { type: 'string', required: true, description: 'who to greet' } },
|
|
49
|
+
effects: 'read_only', // what running it does to the world; required, and what --mcp reads
|
|
49
50
|
run: ({ options }) => ({ greeting: `hello, ${options.name}` }),
|
|
50
51
|
}));
|
|
51
52
|
```
|
|
@@ -55,7 +56,7 @@ $ node cli.mjs --name ada
|
|
|
55
56
|
greeting: hello, ada
|
|
56
57
|
|
|
57
58
|
$ node cli.mjs --json --name ada
|
|
58
|
-
{"ok":true,"data":{"greeting":"hello, ada"}}
|
|
59
|
+
{"ok":true,"data":{"greeting":"hello, ada"},"meta":{"provenance":{"name":{"source":"flag","location":"--name"}}}}
|
|
59
60
|
|
|
60
61
|
$ node cli.mjs # exit 2
|
|
61
62
|
error: missing required option --name
|
|
@@ -154,9 +155,11 @@ the declaration read by a different reader.
|
|
|
154
155
|
|
|
155
156
|
### How do I expose a CLI over MCP?
|
|
156
157
|
|
|
157
|
-
Run it with `--mcp`: the same manifest is served as MCP tools over stdio.
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
Run it with `--mcp`: the same manifest is served as MCP tools over stdio. Every runnable
|
|
159
|
+
command declares its `effects` — `read_only`, `idempotent` or `non_idempotent`, which become
|
|
160
|
+
MCP's hints, or `withheld`, which keeps it out of the tool list — so nothing reaches an
|
|
161
|
+
agent by accident. On `burgee/commander` and `burgee/yargs`, a command that declared
|
|
162
|
+
nothing is still listed, marked `effects: 'undeclared'`. Register it with any stdio client:
|
|
160
163
|
|
|
161
164
|
```json
|
|
162
165
|
{ "mcpServers": { "mytool": { "command": "npx", "args": ["mytool", "--mcp"] } } }
|
|
@@ -164,7 +167,7 @@ so nothing reaches an agent by accident. Register it with any stdio client:
|
|
|
164
167
|
|
|
165
168
|
### Does it have dependencies?
|
|
166
169
|
|
|
167
|
-
None outside
|
|
170
|
+
None outside the burgee family. `burgee` installs five packages from that family —
|
|
168
171
|
`bellpull`, `closeout`, `linegauge`, `roundel` and `seniority` — and each of those takes
|
|
169
172
|
nothing from outside it either: one repository, one release pipeline, one supply chain to
|
|
170
173
|
audit.
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,22 @@ export interface OutputContext {
|
|
|
58
58
|
error?: boolean;
|
|
59
59
|
}
|
|
60
60
|
export type HookEvent = 'preSubcommand' | 'preAction' | 'postAction';
|
|
61
|
+
/**
|
|
62
|
+
* commander's option-value types, as `typings/index.d.ts` declares them. `any` is
|
|
63
|
+
* commander's choice and the point: `program.opts().port` is usable without a cast, and
|
|
64
|
+
* `opts<T>()` narrows it — a program written against commander's types relies on both;
|
|
65
|
+
* `unknown` here breaks every `opts().x` such a program reads.
|
|
66
|
+
*/
|
|
67
|
+
export type OptionValues = Record<string, any>;
|
|
68
|
+
/** Where an option's value came from. A string, so an author can define their own; the known ones autocomplete. */
|
|
69
|
+
export type OptionValueSource = 'default' | 'config' | 'env' | 'cli' | 'implied' | (string & Record<never, never>) | undefined;
|
|
70
|
+
/** What `.configureHelp()` takes: any subset of `Help`'s methods and settings. */
|
|
71
|
+
export type HelpConfiguration = Partial<Help>;
|
|
72
|
+
/** `.parseOptions()`'s split of an argv into operands and unknown options. */
|
|
73
|
+
export interface ParseOptionsResult {
|
|
74
|
+
operands: string[];
|
|
75
|
+
unknown: string[];
|
|
76
|
+
}
|
|
61
77
|
export type HookListener = (thisCommand: Command, actionCommand: Command) => void | Promise<void>;
|
|
62
78
|
export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
|
|
63
79
|
export type AddHelpTextContext = {
|
|
@@ -236,14 +252,11 @@ export declare class Command extends EventEmitter {
|
|
|
236
252
|
* sub --unknown uuu op => [sub], [--unknown uuu op]
|
|
237
253
|
* sub -- --unknown uuu op => [sub --unknown uuu op], []
|
|
238
254
|
*/
|
|
239
|
-
parseOptions(args: string[]):
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
/** Local option values as key-value pairs. */
|
|
244
|
-
opts(): Record<string, unknown>;
|
|
255
|
+
parseOptions(args: string[]): ParseOptionsResult;
|
|
256
|
+
/** Local option values as key-value pairs; `opts<T>()` types them, as commander's own declaration does. */
|
|
257
|
+
opts<T extends OptionValues = OptionValues>(): T;
|
|
245
258
|
/** Merged local and global option values; globals overwrite locals. */
|
|
246
|
-
optsWithGlobals():
|
|
259
|
+
optsWithGlobals<T extends OptionValues = OptionValues>(): T;
|
|
247
260
|
/** Display an error message and exit (or call exitOverride). */
|
|
248
261
|
error(message: string, errorOptions?: ErrorOptions): never;
|
|
249
262
|
/** One failure as the envelope (E3, G5); `fix` only when one candidate was named. */
|
|
@@ -316,7 +329,17 @@ export declare class Command extends EventEmitter {
|
|
|
316
329
|
*/
|
|
317
330
|
get manifest(): Manifest;
|
|
318
331
|
_project(manifest: Manifest): void;
|
|
319
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Options as the manifest describes them, on a null-prototype record — keyed so that the
|
|
334
|
+
* spelling every surface derives from a key, `--${kebab(key)}`, is a flag commander accepts.
|
|
335
|
+
*
|
|
336
|
+
* Commander negates only what it was told to, where burgee's own parser negates every
|
|
337
|
+
* boolean. So a boolean is `negatable` here only when the program declared its `--no-` twin,
|
|
338
|
+
* which folds onto it rather than appearing twice; and a `--no-x` declared alone, which has
|
|
339
|
+
* no `--x`, is described as the switch it is, under `noX`. Before this, `--no-color` was
|
|
340
|
+
* published as `color` with `flag: '--color'`: completions offered `--color` and
|
|
341
|
+
* `--no-skip-blank`, and `--mcp` sent `--color` — each refused by the parser behind them.
|
|
342
|
+
*/
|
|
320
343
|
_optionSpecs(): Record<string, OptionSpec>;
|
|
321
344
|
/** burgee: declare what the command does to the world (N6). This is what exposes it as an MCP tool (N2). */
|
|
322
345
|
effects(value: DeclaredEffects): this;
|
|
@@ -5,6 +5,7 @@ import { stripVTControlCharacters } from 'node:util';
|
|
|
5
5
|
import * as crossSpawn from 'bellpull/cross-spawn';
|
|
6
6
|
import { ExitCode } from '../exit-code.js';
|
|
7
7
|
import { Manifest } from '../manifest.js';
|
|
8
|
+
import { camel } from '../names.js';
|
|
8
9
|
import { host } from '../runtime.js';
|
|
9
10
|
import { machineJson, schemaOf } from '../schema.js';
|
|
10
11
|
import { suggestSimilar } from '../suggest.js';
|
|
@@ -1413,6 +1414,10 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
|
|
|
1413
1414
|
_optionSpecs() {
|
|
1414
1415
|
const specs = Object.create(null);
|
|
1415
1416
|
for (const option of this.options) {
|
|
1417
|
+
const name = option.attributeName();
|
|
1418
|
+
const paired = this.options.some((o) => o.negate !== option.negate && o.attributeName() === name);
|
|
1419
|
+
if (option.negate && paired)
|
|
1420
|
+
continue;
|
|
1416
1421
|
const spec = { type: option.required || option.optional ? 'string' : 'boolean' };
|
|
1417
1422
|
if (option.description)
|
|
1418
1423
|
spec.description = option.description;
|
|
@@ -1424,7 +1429,9 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
|
|
|
1424
1429
|
spec.default = option.defaultValue;
|
|
1425
1430
|
if (option.envVar)
|
|
1426
1431
|
spec.env = option.envVar;
|
|
1427
|
-
|
|
1432
|
+
if (spec.type === 'boolean')
|
|
1433
|
+
spec.negatable = paired;
|
|
1434
|
+
Object.defineProperty(specs, option.negate ? camel(option.name()) : name, { value: spec, enumerable: true, writable: true, configurable: true });
|
|
1428
1435
|
}
|
|
1429
1436
|
return specs;
|
|
1430
1437
|
}
|
package/dist/commander.d.ts
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
* `.sdlc/intents/commander-compat/spec.md` and `npm run compat`.
|
|
5
5
|
*/
|
|
6
6
|
import { Argument } from './commander/argument.js';
|
|
7
|
-
import { Command } from './commander/command.js';
|
|
7
|
+
import { Command, type OutputConfiguration as ResolvedOutputConfiguration } from './commander/command.js';
|
|
8
8
|
import { Option } from './commander/option.js';
|
|
9
9
|
export { Argument, humanReadableArgName } from './commander/argument.js';
|
|
10
|
-
export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HookEvent, type HookListener, type
|
|
10
|
+
export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HelpConfiguration, type HookEvent, type HookListener, type OptionValueSource, type OptionValues, type OutputContext, type ParseOptions, type ParseOptionsResult, } from './commander/command.js';
|
|
11
11
|
export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander/error.js';
|
|
12
12
|
export { Help, type HelpContext } from './commander/help.js';
|
|
13
13
|
export { DualOptions, Option } from './commander/option.js';
|
|
14
|
+
/**
|
|
15
|
+
* commander's `OutputConfiguration`: every member optional, because `.configureOutput()`
|
|
16
|
+
* takes any subset of them and a typed program annotates exactly that subset. What
|
|
17
|
+
* `.configureOutput()` hands back is the resolved, complete one.
|
|
18
|
+
*/
|
|
19
|
+
export type OutputConfiguration = Partial<ResolvedOutputConfiguration>;
|
|
14
20
|
/** The root command, for programs that never construct their own. */
|
|
15
21
|
export declare const program: Command;
|
|
16
22
|
export declare const createCommand: (name?: string) => Command;
|
package/dist/completions.js
CHANGED
|
@@ -16,7 +16,7 @@ function children(manifest, at) {
|
|
|
16
16
|
function nodeOf(manifest, c) {
|
|
17
17
|
const visible = Object.fromEntries(Object.entries(c.options)
|
|
18
18
|
.filter(([, spec]) => spec.hidden !== true)
|
|
19
|
-
.map(([name, spec]) => [name, spec.type === 'boolean'
|
|
19
|
+
.map(([name, spec]) => [name, { ...spec, negatable: spec.type === 'boolean' && spec.negatable !== false }]));
|
|
20
20
|
const isRoot = c.path.length === manifest.rootPath.length;
|
|
21
21
|
return {
|
|
22
22
|
path: c.path.slice(manifest.rootPath.length),
|
package/dist/execute.js
CHANGED
|
@@ -127,7 +127,7 @@ function toParseConfig(specs, withConfig) {
|
|
|
127
127
|
...(spec.short === undefined ? {} : { short: spec.short }),
|
|
128
128
|
...(spec.multiple === true ? { multiple: true } : {}),
|
|
129
129
|
};
|
|
130
|
-
if (spec.type === 'boolean')
|
|
130
|
+
if (spec.type === 'boolean' && spec.negatable !== false)
|
|
131
131
|
config[`${NO}${kebab(name)}`] = { type: 'boolean' };
|
|
132
132
|
}
|
|
133
133
|
return config;
|
|
@@ -238,7 +238,7 @@ async function describeFailure(cause, argv, node) {
|
|
|
238
238
|
const dash = explain.singleDashHint(argv);
|
|
239
239
|
if (dash !== undefined)
|
|
240
240
|
return { code: ExitCode.USAGE, message, hint: dash };
|
|
241
|
-
const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}));
|
|
241
|
+
const better = explain.unknownOption(cause, Object.keys(node?.options ?? {}).map(kebab));
|
|
242
242
|
return { code: ExitCode.USAGE, message, hint: 'run --help to see the available options', ...better };
|
|
243
243
|
}
|
|
244
244
|
return { code: ExitCode.RUNTIME, message };
|
|
@@ -505,7 +505,7 @@ export async function run(target, opts = {}) {
|
|
|
505
505
|
manifest.rootPath = [target.name];
|
|
506
506
|
manifest.add({
|
|
507
507
|
path: [target.name],
|
|
508
|
-
...(target
|
|
508
|
+
...helpFields(target),
|
|
509
509
|
options: target.options ?? {},
|
|
510
510
|
...(target.run === undefined ? {} : { run: target.run }),
|
|
511
511
|
});
|
package/dist/manifest.d.ts
CHANGED
|
@@ -78,6 +78,13 @@ export interface OptionSpec {
|
|
|
78
78
|
/** `true` renders `(deprecated)`; a string names the replacement: `(deprecated: use --force)` (yargs #2248). */
|
|
79
79
|
deprecated?: boolean | string;
|
|
80
80
|
hidden?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* `boolean` only: whether `--no-<name>` is accepted. Every boolean is negatable unless this
|
|
83
|
+
* says `false`. burgee's own parser negates every boolean (see `toParseConfig`); the
|
|
84
|
+
* commander façade sets `false` where commander would refuse the negation, so completions
|
|
85
|
+
* and `--mcp` never offer or send a flag the parser behind them rejects.
|
|
86
|
+
*/
|
|
87
|
+
negatable?: boolean;
|
|
81
88
|
/** The shared set this option was copied from (M4); `--schema` carries it, help lists the option like any other. */
|
|
82
89
|
sharedFrom?: string;
|
|
83
90
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -34,7 +34,11 @@ export type Invoke = (argv: string[]) => Promise<{
|
|
|
34
34
|
* this command is not a `read_only` one, and it is not a `withheld` one either — nobody said.
|
|
35
35
|
*/
|
|
36
36
|
export declare function annotationsOf(effects?: Effects): ToolAnnotations;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`, one character or more. A
|
|
39
|
+
* program whose root runs — a single `run(defineCommand(…))`, a commander program with a root
|
|
40
|
+
* `.action()` — has no typed name at all, so its tool is named after the program.
|
|
41
|
+
*/
|
|
38
42
|
export declare const toolName: (node: CommandNode, root: string[]) => string;
|
|
39
43
|
/**
|
|
40
44
|
* The tool list: every runnable, visible command an author has not withheld.
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { WITHHELD } from './definition.js';
|
|
3
|
+
import { kebab } from './names.js';
|
|
3
4
|
import { inputSchemaOf, runnable, typedName } from './schema.js';
|
|
4
5
|
export const MCP_PROTOCOL_VERSION = '2025-06-18';
|
|
5
6
|
const JSON_RPC_INVALID_REQUEST = -32600;
|
|
@@ -14,7 +15,7 @@ export function annotationsOf(effects) {
|
|
|
14
15
|
destructiveHint: effects === 'non_idempotent',
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
|
-
export const toolName = (node, root) => typedName(node, root).replaceAll(' ', '_');
|
|
18
|
+
export const toolName = (node, root) => (typedName(node, root) || root.join(' ')).replaceAll(' ', '_');
|
|
18
19
|
function describe(node) {
|
|
19
20
|
const parts = [node.description ?? node.summary ?? ''];
|
|
20
21
|
for (const e of node.examples ?? [])
|
|
@@ -32,12 +33,13 @@ function optionArgs(node, args) {
|
|
|
32
33
|
const value = args[name];
|
|
33
34
|
if (value === undefined || value === null)
|
|
34
35
|
continue;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const flag = `--${kebab(name)}`;
|
|
37
|
+
if (spec.type !== 'boolean')
|
|
38
|
+
out.push(flag, String(value));
|
|
39
|
+
else if (value === true)
|
|
40
|
+
out.push(flag);
|
|
41
|
+
else if (value === false && spec.negatable !== false && (spec.default === true || spec.env !== undefined))
|
|
42
|
+
out.push(`--no-${kebab(name)}`);
|
|
41
43
|
}
|
|
42
44
|
return out;
|
|
43
45
|
}
|
package/dist/migrate.d.ts
CHANGED
|
@@ -8,8 +8,24 @@ import { type Graded } from './compat.js';
|
|
|
8
8
|
export declare const MAPPING: Readonly<Record<string, string>>;
|
|
9
9
|
/** The packages a project depends on that this command is about (A1). */
|
|
10
10
|
export declare const HOSTS: readonly ["commander", "yargs"];
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Every name each target exports — values and types alike — so a rewrite that moves
|
|
13
|
+
* `import { Argv } from 'yargs'` can first ask whether `burgee/yargs` has an `Argv`.
|
|
14
|
+
*
|
|
15
|
+
* A specifier-only rewrite (D-050) cannot tell a value from a type, and does not need to:
|
|
16
|
+
* a name the target does not export breaks the build either way. This is data rather than
|
|
17
|
+
* a lookup because the lookup is the TypeScript checker, which is the dependency this
|
|
18
|
+
* command exists not to have; `facade-types.test.ts` holds the table equal to what the
|
|
19
|
+
* checker sees in `dist/*.d.ts`, so it cannot drift from the façades it describes.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FACADE_EXPORTS: Readonly<Record<string, readonly string[]>>;
|
|
22
|
+
/**
|
|
23
|
+
* Why a file was left untouched. Each is a named position, never a guess (A4).
|
|
24
|
+
*
|
|
25
|
+
* `unknown-export` is a named import the target does not export — rewriting it would turn a
|
|
26
|
+
* working import into TS2305 or a `SyntaxError` at load, so the file stays as it was.
|
|
27
|
+
*/
|
|
28
|
+
export type RefusalReason = 'deep-import' | 'non-literal-specifier' | 'unknown-export';
|
|
13
29
|
export interface Refusal {
|
|
14
30
|
/** Relative to the directory being migrated, with forward slashes on every platform. */
|
|
15
31
|
file: string;
|
|
@@ -17,6 +33,23 @@ export interface Refusal {
|
|
|
17
33
|
/** The specifier as written, or `''` for a dynamic specifier that is not a literal. */
|
|
18
34
|
specifier: string;
|
|
19
35
|
reason: RefusalReason;
|
|
36
|
+
/** For `unknown-export`: the names the target does not export. */
|
|
37
|
+
names?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A type-only import left on the incumbent because the façade does not export every name in
|
|
41
|
+
* it. Types are erased, so the file's values still move to burgee and the program runs on it;
|
|
42
|
+
* the types keep compiling against the incumbent's declarations, which is why the incumbent
|
|
43
|
+
* is then not reported removable.
|
|
44
|
+
*/
|
|
45
|
+
export interface Kept {
|
|
46
|
+
file: string;
|
|
47
|
+
line: number;
|
|
48
|
+
specifier: string;
|
|
49
|
+
/** The names the façade does not export. */
|
|
50
|
+
names: string[];
|
|
51
|
+
/** The note a reader acts on. */
|
|
52
|
+
note: string;
|
|
20
53
|
}
|
|
21
54
|
/** One rewritten specifier, for the per-mapping rollup the report prints. */
|
|
22
55
|
export interface Mapped {
|
|
@@ -36,6 +69,8 @@ export interface MigrationReport {
|
|
|
36
69
|
imports: number;
|
|
37
70
|
mapped: Mapped[];
|
|
38
71
|
refused: Refusal[];
|
|
72
|
+
/** Type-only imports left pointing at the incumbent, each with the note that says why. */
|
|
73
|
+
kept: Kept[];
|
|
39
74
|
detected: Detection;
|
|
40
75
|
dependencies: {
|
|
41
76
|
before: string[];
|
|
@@ -57,6 +92,11 @@ interface Site {
|
|
|
57
92
|
start: number;
|
|
58
93
|
end: number;
|
|
59
94
|
line: number;
|
|
95
|
+
/**
|
|
96
|
+
* For `import … from` and `export … from`: the code tokens between the keyword and `from`
|
|
97
|
+
* — the import clause, which is all `bindingsOf` needs. Absent for the other three positions.
|
|
98
|
+
*/
|
|
99
|
+
clause?: string[];
|
|
60
100
|
}
|
|
61
101
|
interface Scan {
|
|
62
102
|
sites: Site[];
|
|
@@ -80,6 +120,7 @@ export interface Rewrite {
|
|
|
80
120
|
to: string;
|
|
81
121
|
}[];
|
|
82
122
|
refused: Omit<Refusal, 'file'>[];
|
|
123
|
+
kept: Omit<Kept, 'file'>[];
|
|
83
124
|
/** Whether this file references a host at all. A file that does not is not "untouched", it is unrelated. */
|
|
84
125
|
relevant: boolean;
|
|
85
126
|
}
|
|
@@ -93,6 +134,19 @@ export interface Rewrite {
|
|
|
93
134
|
* on a `Buffer` it also skips decoding 3 MB of UTF-8 that nothing was going to read.
|
|
94
135
|
*/
|
|
95
136
|
export declare function mentionsAHost(source: string | Buffer): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The names an import or re-export clause asks the module for, and whether the whole
|
|
139
|
+
* statement is type-only (`import type …`, `export type …`).
|
|
140
|
+
*
|
|
141
|
+
* Read off the tokens the scan already has: `{ a, type b, c as d }` asks for `a`, `b` and
|
|
142
|
+
* `c`; a default or namespace binding asks for no name a façade could lack (`default` is
|
|
143
|
+
* the factory, and `* as ns` is checked where it is used, which a scan cannot see). A name
|
|
144
|
+
* written as a string (`{ 'a-b' as c }`) is left unverified rather than guessed at.
|
|
145
|
+
*/
|
|
146
|
+
export declare function bindingsOf(clause: readonly string[]): {
|
|
147
|
+
typeOnly: boolean;
|
|
148
|
+
names: string[];
|
|
149
|
+
};
|
|
96
150
|
/**
|
|
97
151
|
* A2/A5 — map every host specifier in one file, or map none of them.
|
|
98
152
|
*
|
package/dist/migrate.js
CHANGED
|
@@ -11,6 +11,89 @@ export const MAPPING = {
|
|
|
11
11
|
'yargs/helpers': 'burgee/yargs/helpers',
|
|
12
12
|
};
|
|
13
13
|
export const HOSTS = ['commander', 'yargs'];
|
|
14
|
+
export const FACADE_EXPORTS = {
|
|
15
|
+
'burgee/commander': [
|
|
16
|
+
'AddHelpTextContext',
|
|
17
|
+
'AddHelpTextPosition',
|
|
18
|
+
'Argument',
|
|
19
|
+
'BurgeeParseOptions',
|
|
20
|
+
'Command',
|
|
21
|
+
'CommandOptions',
|
|
22
|
+
'CommanderError',
|
|
23
|
+
'DualOptions',
|
|
24
|
+
'ErrorOptions',
|
|
25
|
+
'ExecutableCommandOptions',
|
|
26
|
+
'Help',
|
|
27
|
+
'HelpConfiguration',
|
|
28
|
+
'HelpContext',
|
|
29
|
+
'HookEvent',
|
|
30
|
+
'HookListener',
|
|
31
|
+
'InvalidArgumentError',
|
|
32
|
+
'InvalidOptionArgumentError',
|
|
33
|
+
'Option',
|
|
34
|
+
'OptionValueSource',
|
|
35
|
+
'OptionValues',
|
|
36
|
+
'OutputConfiguration',
|
|
37
|
+
'OutputContext',
|
|
38
|
+
'ParseOptions',
|
|
39
|
+
'ParseOptionsResult',
|
|
40
|
+
'createArgument',
|
|
41
|
+
'createCommand',
|
|
42
|
+
'createOption',
|
|
43
|
+
'humanReadableArgName',
|
|
44
|
+
'program',
|
|
45
|
+
'useColor',
|
|
46
|
+
],
|
|
47
|
+
'burgee/yargs': [
|
|
48
|
+
'Arguments',
|
|
49
|
+
'ArgumentsCamelCase',
|
|
50
|
+
'Argv',
|
|
51
|
+
'AsyncCompletionFunction',
|
|
52
|
+
'BuilderArguments',
|
|
53
|
+
'BuilderCallback',
|
|
54
|
+
'Choices',
|
|
55
|
+
'CommandBuilder',
|
|
56
|
+
'CommandModule',
|
|
57
|
+
'CompletionCallback',
|
|
58
|
+
'Defined',
|
|
59
|
+
'DetailedArguments',
|
|
60
|
+
'FallbackCompletionFunction',
|
|
61
|
+
'InferredOptionType',
|
|
62
|
+
'InferredOptionTypeInner',
|
|
63
|
+
'InferredOptionTypePrimitive',
|
|
64
|
+
'InferredOptionTypes',
|
|
65
|
+
'MiddlewareFunction',
|
|
66
|
+
'Options',
|
|
67
|
+
'ParseCallback',
|
|
68
|
+
'ParsedCommand',
|
|
69
|
+
'Parser',
|
|
70
|
+
'ParserConfigurationOptions',
|
|
71
|
+
'PlatformShim',
|
|
72
|
+
'PositionalOptions',
|
|
73
|
+
'PositionalOptionsType',
|
|
74
|
+
'PromiseCompletionFunction',
|
|
75
|
+
'RequireDirectoryOptions',
|
|
76
|
+
'SyncCompletionFunction',
|
|
77
|
+
'ToArray',
|
|
78
|
+
'ToNumber',
|
|
79
|
+
'ToString',
|
|
80
|
+
'YError',
|
|
81
|
+
'YargsInstance',
|
|
82
|
+
'applyExtends',
|
|
83
|
+
'argsert',
|
|
84
|
+
'camelCase',
|
|
85
|
+
'decamelize',
|
|
86
|
+
'default',
|
|
87
|
+
'hideBin',
|
|
88
|
+
'isPromise',
|
|
89
|
+
'isYargsInstance',
|
|
90
|
+
'looksLikeNumber',
|
|
91
|
+
'objFilter',
|
|
92
|
+
'parseCommand',
|
|
93
|
+
'platformShim',
|
|
94
|
+
],
|
|
95
|
+
'burgee/yargs/helpers': ['Parser', 'applyExtends', 'hideBin'],
|
|
96
|
+
};
|
|
14
97
|
const WORD = /[A-Za-z0-9_$]/;
|
|
15
98
|
const DIVIDES = new Set([')', ']', '}']);
|
|
16
99
|
function startsRegex(previous) {
|
|
@@ -123,12 +206,25 @@ function push(tokens, text, line) {
|
|
|
123
206
|
if (tokens.pending && text !== QUOTED && text !== ')')
|
|
124
207
|
tokens.nonLiteral.push(line);
|
|
125
208
|
tokens.pending = text === '(' && (tokens.previous === 'import' || tokens.previous === 'require');
|
|
209
|
+
if (text === 'import' || text === 'export')
|
|
210
|
+
tokens.clause = [];
|
|
211
|
+
else if (text === '(' || text === '=' || tokens.previous === 'from')
|
|
212
|
+
tokens.clause = undefined;
|
|
213
|
+
else
|
|
214
|
+
tokens.clause?.push(text);
|
|
126
215
|
tokens.before = tokens.previous;
|
|
127
216
|
tokens.previous = text;
|
|
128
217
|
}
|
|
218
|
+
function siteOf(source, at, tokens) {
|
|
219
|
+
const { open, close, line } = at;
|
|
220
|
+
const site = { specifier: source.slice(open + 1, close - 1), start: open + 1, end: close - 1, line };
|
|
221
|
+
if (tokens.previous === 'from' && tokens.clause !== undefined)
|
|
222
|
+
site.clause = tokens.clause.slice(0, -1);
|
|
223
|
+
return site;
|
|
224
|
+
}
|
|
129
225
|
export function scan(source) {
|
|
130
226
|
const sites = [];
|
|
131
|
-
const tokens = { previous: '', before: '', pending: false, nonLiteral: [] };
|
|
227
|
+
const tokens = { previous: '', before: '', pending: false, nonLiteral: [], clause: undefined };
|
|
132
228
|
let line = 1;
|
|
133
229
|
let i = 0;
|
|
134
230
|
while (i < source.length) {
|
|
@@ -143,7 +239,7 @@ export function scan(source) {
|
|
|
143
239
|
const c = source.charAt(i);
|
|
144
240
|
const quoted = c === "'" || c === '"';
|
|
145
241
|
if (quoted && isSpecifier(tokens.previous, tokens.before))
|
|
146
|
-
sites.push(
|
|
242
|
+
sites.push(siteOf(source, { open: i, close: literal, line }, tokens));
|
|
147
243
|
push(tokens, quoted ? QUOTED : 'lit', line);
|
|
148
244
|
line += newlines(source, i, literal);
|
|
149
245
|
i = literal;
|
|
@@ -169,27 +265,71 @@ function isDeep(specifier) {
|
|
|
169
265
|
return false;
|
|
170
266
|
return HOSTS.some((host) => specifier.startsWith(`${host}/`));
|
|
171
267
|
}
|
|
268
|
+
export function bindingsOf(clause) {
|
|
269
|
+
const typeOnly = clause[0] === 'type' && clause.length > 1;
|
|
270
|
+
const open = clause.indexOf('{');
|
|
271
|
+
const close = clause.indexOf('}', open);
|
|
272
|
+
if (open === -1 || close === -1)
|
|
273
|
+
return { typeOnly, names: [] };
|
|
274
|
+
const names = [];
|
|
275
|
+
let element = [];
|
|
276
|
+
for (const token of [...clause.slice(open + 1, close), ',']) {
|
|
277
|
+
if (token !== ',') {
|
|
278
|
+
element.push(token);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const modifier = element[0] === 'type' && element.length > 1 && element[1] !== 'as';
|
|
282
|
+
const name = modifier ? element[1] : element[0];
|
|
283
|
+
if (name !== undefined && name !== 'default' && name !== QUOTED && WORD.test(name.charAt(0)))
|
|
284
|
+
names.push(name);
|
|
285
|
+
element = [];
|
|
286
|
+
}
|
|
287
|
+
return { typeOnly, names };
|
|
288
|
+
}
|
|
289
|
+
function missingFrom(site, to) {
|
|
290
|
+
const exported = FACADE_EXPORTS[to];
|
|
291
|
+
if (site.clause === undefined || exported === undefined)
|
|
292
|
+
return { typeOnly: false, missing: [] };
|
|
293
|
+
const { typeOnly, names } = bindingsOf(site.clause);
|
|
294
|
+
return { typeOnly, missing: names.filter((name) => !exported.includes(name)) };
|
|
295
|
+
}
|
|
172
296
|
export function rewriteSource(source) {
|
|
173
297
|
if (!mentionsAHost(source))
|
|
174
|
-
return { source, mapped: [], refused: [], relevant: false };
|
|
298
|
+
return { source, mapped: [], refused: [], kept: [], relevant: false };
|
|
175
299
|
const { sites, nonLiteral } = scan(source);
|
|
176
300
|
const hits = sites.filter((s) => MAPPING[s.specifier] !== undefined || isDeep(s.specifier));
|
|
177
301
|
if (hits.length === 0)
|
|
178
|
-
return { source, mapped: [], refused: [], relevant: false };
|
|
302
|
+
return { source, mapped: [], refused: [], kept: [], relevant: false };
|
|
303
|
+
const kept = [];
|
|
304
|
+
const unknown = [];
|
|
305
|
+
const moving = [];
|
|
306
|
+
for (const site of hits) {
|
|
307
|
+
const to = MAPPING[site.specifier];
|
|
308
|
+
if (to === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
const { typeOnly, missing } = missingFrom(site, to);
|
|
311
|
+
if (missing.length === 0)
|
|
312
|
+
moving.push(site);
|
|
313
|
+
else if (typeOnly)
|
|
314
|
+
kept.push({ line: site.line, specifier: site.specifier, names: missing, note: `${to} does not export ${missing.join(', ')}; this type-only import stays on '${site.specifier}', so keep its types installed` });
|
|
315
|
+
else
|
|
316
|
+
unknown.push({ line: site.line, specifier: site.specifier, reason: 'unknown-export', names: missing });
|
|
317
|
+
}
|
|
179
318
|
const refused = [
|
|
180
319
|
...hits.filter((s) => isDeep(s.specifier)).map((s) => ({ line: s.line, specifier: s.specifier, reason: 'deep-import' })),
|
|
181
320
|
...nonLiteral.map((line) => ({ line, specifier: '', reason: 'non-literal-specifier' })),
|
|
321
|
+
...unknown,
|
|
182
322
|
].sort((a, b) => a.line - b.line);
|
|
183
323
|
if (refused.length > 0)
|
|
184
|
-
return { source, mapped: [], refused, relevant: true };
|
|
324
|
+
return { source, mapped: [], refused, kept: [], relevant: true };
|
|
185
325
|
let out = source;
|
|
186
326
|
const mapped = [];
|
|
187
|
-
for (const site of
|
|
327
|
+
for (const site of moving.sort((a, b) => b.start - a.start)) {
|
|
188
328
|
const to = MAPPING[site.specifier];
|
|
189
329
|
out = `${out.slice(0, site.start)}${to}${out.slice(site.end)}`;
|
|
190
330
|
mapped.push({ from: site.specifier, to });
|
|
191
331
|
}
|
|
192
|
-
return { source: out, mapped: mapped.reverse(), refused: [], relevant: true };
|
|
332
|
+
return { source: out, mapped: mapped.reverse(), refused: [], kept, relevant: true };
|
|
193
333
|
}
|
|
194
334
|
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
195
335
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.next', '.output', '.cache', '.vercel']);
|
|
@@ -230,7 +370,7 @@ export class DirtyTreeError extends Error {
|
|
|
230
370
|
this.name = 'DirtyTreeError';
|
|
231
371
|
}
|
|
232
372
|
}
|
|
233
|
-
const EMPTY = { source: '', mapped: [], refused: [], relevant: false };
|
|
373
|
+
const EMPTY = { source: '', mapped: [], refused: [], kept: [], relevant: false };
|
|
234
374
|
async function migrateBatch(dir, batch, write) {
|
|
235
375
|
const sources = await Promise.all(batch.map(async (file) => await readFile(join(dir, file))));
|
|
236
376
|
const results = sources.map((bytes) => (mentionsAHost(bytes) ? rewriteSource(bytes.toString('utf8')) : EMPTY));
|
|
@@ -264,9 +404,10 @@ export async function migrate(options) {
|
|
|
264
404
|
}
|
|
265
405
|
const all = results.flatMap(({ file, result }) => result.mapped.map((m) => ({ ...m, file })));
|
|
266
406
|
const refused = results.flatMap(({ file, result }) => result.refused.map((r) => ({ file, ...r })));
|
|
267
|
-
const
|
|
407
|
+
const kept = results.flatMap(({ file, result }) => result.kept.map((k) => ({ file, ...k })));
|
|
408
|
+
const imported = [...new Set(results.flatMap(({ result }) => (result.relevant ? [...result.mapped.map((m) => m.from), ...result.kept.map((k) => k.specifier)] : [])))];
|
|
268
409
|
const declared = await declaredHosts(dir);
|
|
269
|
-
const stillUsed = new Set(refused.map((r) => r.specifier.split('/')[0] ?? ''));
|
|
410
|
+
const stillUsed = new Set([...refused, ...kept].map((r) => r.specifier.split('/')[0] ?? ''));
|
|
270
411
|
const removable = declared.filter((host) => !stillUsed.has(host));
|
|
271
412
|
const touched = [...new Set(all.map((m) => m.file))];
|
|
272
413
|
return {
|
|
@@ -274,6 +415,7 @@ export async function migrate(options) {
|
|
|
274
415
|
imports: all.length,
|
|
275
416
|
mapped: rollup(all),
|
|
276
417
|
refused,
|
|
418
|
+
kept,
|
|
277
419
|
detected: { declared, imported: [...new Set(imported.map((s) => s.split('/')[0] ?? s))].sort() },
|
|
278
420
|
dependencies: { before: declared, removable, after: declared.length - removable.length },
|
|
279
421
|
graded: gradedFor([...new Set([...declared, ...imported.map((s) => s.split('/')[0] ?? s)])].sort()),
|