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 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-24+-green.svg?style=flat-square" alt="Node.js 24+" />
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. A command becomes
158
- a tool only when it declares its `effects` (`read_only`, `idempotent` or `non_idempotent`),
159
- so nothing reaches an agent by accident. Register it with any stdio client:
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 this repository. `burgee` installs five packages from its own family —
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
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  export declare const brandCommand: import("./execute.js").Command<{
2
3
  readonly lead: {
3
4
  readonly type: "string";
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { DEFAULT_GROUND, defineBurgee, opposedField } from './brand.js';
@@ -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
- operands: string[];
241
- unknown: string[];
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(): Record<string, unknown>;
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
- /** Options as the manifest describes them, on a null-prototype record. */
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
- Object.defineProperty(specs, option.attributeName(), { value: spec, enumerable: true, writable: true, configurable: true });
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
  }
@@ -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 OutputConfiguration, type OutputContext, type ParseOptions, } from './commander/command.js';
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;
@@ -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' ? { ...spec, negatable: true } : spec]));
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.description === undefined ? {} : { description: target.description }),
508
+ ...helpFields(target),
509
509
  options: target.options ?? {},
510
510
  ...(target.run === undefined ? {} : { run: target.run }),
511
511
  });
@@ -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
- /** `config get` → `config_get`: MCP tool names are `[a-zA-Z0-9_-]`. */
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
- if (spec.type === 'boolean') {
36
- if (value === true)
37
- out.push(`--${name}`);
38
- }
39
- else
40
- out.push(`--${name}`, String(value));
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
- /** Why a file was left untouched. Both are named positions, never a guess (A4). */
12
- export type RefusalReason = 'deep-import' | 'non-literal-specifier';
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({ specifier: source.slice(i + 1, literal - 1), start: i + 1, end: literal - 1, line });
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 [...hits].sort((a, b) => b.start - a.start)) {
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 imported = [...new Set(results.flatMap(({ result }) => (result.relevant ? result.mapped.map((m) => m.from) : [])))];
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()),