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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ export const GRADED = {
2
+ commander: { reference: 1360, passed: 1360, rate: 1 },
3
+ yargs: { reference: 804, passed: 804, rate: 1 },
4
+ };
@@ -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: the reserved names of V5, {@link checkDefinition}, and {@link checkEffects}.
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`
@@ -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 checkRelationNames(name, key, spec, options) {
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
- const clash = flags.get(flag);
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
- if ((spec.minimum !== undefined || spec.maximum !== undefined || spec.integer !== undefined) && spec.type !== 'number') {
35
- throw new Error(`burgee: option "${key}" of "${name}" declares a numeric bound but is not a number`);
36
- }
37
- checkRelationNames(name, key, spec, options);
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, ExitCode.OK);
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: boolean;
6
- idempotentHint: boolean;
7
- destructiveHint: boolean;
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
- /** MCP's hints, from the declared effects. `destructiveHint` is only ever false by declaration. */
22
- export declare function annotationsOf(effects: Effects): ToolAnnotations;
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 that declared what running it does.
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
- * Two commands are absent and for different reasons. One declared `'withheld'` — an author
29
- * who thought about it and said no, which is what that word is for. The other has no
30
- * `effects` at all, which `checkCommand` now refuses at declaration, so on burgee's own API
31
- * it cannot reach here; a command built through the commander or yargs façade still can,
32
- * because neither incumbent has a notion of effects and neither can be made to acquire one
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 !== undefined && c.effects !== WITHHELD)
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 exposed = new Set(toolsOf(manifest).map((t) => t.name));
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 {};