burgee 0.11.1 → 0.12.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.
Files changed (51) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.d.ts +1 -90
  3. package/dist/cli.js +3 -151
  4. package/dist/commander/command.d.ts +22 -0
  5. package/dist/commander/command.js +41 -10
  6. package/dist/compat.d.ts +39 -2
  7. package/dist/compat.js +90 -2
  8. package/dist/complete-dynamic.d.ts +18 -0
  9. package/dist/complete-dynamic.js +19 -0
  10. package/dist/completions.js +32 -14
  11. package/dist/config-explain.d.ts +22 -0
  12. package/dist/config-explain.js +33 -0
  13. package/dist/define-error.d.ts +29 -0
  14. package/dist/define-error.js +29 -0
  15. package/dist/errors.d.ts +29 -0
  16. package/dist/errors.js +17 -0
  17. package/dist/execute.d.ts +6 -0
  18. package/dist/execute.js +71 -13
  19. package/dist/facade-failure.d.ts +17 -0
  20. package/dist/facade-failure.js +12 -0
  21. package/dist/fields.d.ts +32 -0
  22. package/dist/fields.js +45 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/manifest.d.ts +35 -5
  26. package/dist/manifest.js +6 -0
  27. package/dist/mcp.js +88 -5
  28. package/dist/migrate.d.ts +57 -7
  29. package/dist/migrate.js +413 -34
  30. package/dist/parse-hooks.d.ts +7 -0
  31. package/dist/parse-hooks.js +17 -0
  32. package/dist/plugin.d.ts +2 -6
  33. package/dist/plugin.js +1 -1
  34. package/dist/program-schema.json +1 -0
  35. package/dist/program.d.ts +90 -0
  36. package/dist/program.js +151 -0
  37. package/dist/runtime.d.ts +3 -1
  38. package/dist/runtime.js +3 -0
  39. package/dist/schema.d.ts +7 -0
  40. package/dist/schema.js +6 -11
  41. package/dist/schema.json +1 -1
  42. package/dist/stdin-dash.d.ts +15 -0
  43. package/dist/stdin-dash.js +12 -0
  44. package/dist/validate.d.ts +1 -27
  45. package/dist/validate.js +2 -17
  46. package/dist/yargs/factory.js +39 -13
  47. package/dist/yargs-parser.d.ts +8 -1
  48. package/dist/yargs-parser.js +1 -1
  49. package/dist/yargs.d.ts +1 -0
  50. package/dist/yargs.js +1 -0
  51. package/package.json +7 -6
@@ -36,6 +36,13 @@ export type StandardResult<Output> = {
36
36
  export interface OptionSpec {
37
37
  /** `boolean` never consumes a value (S7); `number` rejects NaN and Infinity (S3). */
38
38
  type: 'string' | 'boolean' | 'number';
39
+ /**
40
+ * D3 / D-119 — values computed when a person presses TAB: the generated script calls the
41
+ * program back (`<program> __complete <command> --<option> <partial>`) for this option and
42
+ * no other. Declaring it is the opt-in; an option without one completes from `choices`, or
43
+ * not at all, and never runs the program.
44
+ */
45
+ complete?: (partial: string) => Iterable<string> | Promise<Iterable<string>>;
39
46
  description?: string;
40
47
  required?: boolean;
41
48
  short?: string;
@@ -157,6 +164,8 @@ export interface ArgumentSpec {
157
164
  required?: boolean;
158
165
  variadic?: boolean;
159
166
  default?: string;
167
+ /** `'file'`: a path, where `-` means standard input — handed to the handler as `ctx.stdin` (S4). */
168
+ type?: 'file';
160
169
  }
161
170
  /** One example: a single copy-pasteable command line, the description below it (H2). */
162
171
  export interface Example {
@@ -180,6 +189,11 @@ export interface RunContext {
180
189
  options: Record<string, unknown>;
181
190
  positionals: string[];
182
191
  passthrough: string[];
192
+ /**
193
+ * Standard input, present only when a `type: 'file'` argument was given `-` (S4). The
194
+ * positional still reads `-`, so a handler checks it the same way it checks a path.
195
+ */
196
+ stdin?: NodeJS.ReadableStream;
183
197
  env: Record<string, string | undefined>;
184
198
  /** Exit with an E1 code. Unwinds cleanly: the code is honoured and nothing is printed. */
185
199
  exit: (code: number) => never;
@@ -226,6 +240,8 @@ export interface CommandNode {
226
240
  * none, so a façade's command is withheld in fact and cannot be made to say so.
227
241
  */
228
242
  effects?: DeclaredEffects;
243
+ /** The result's top-level fields, as declared (N14); what `--json=` lists. */
244
+ fields?: readonly string[];
229
245
  run?: (ctx: RunContext) => unknown;
230
246
  /**
231
247
  * The handler's module, imported on dispatch only (M2): the manifest — help, schema,
@@ -242,13 +258,19 @@ export declare function lazyRun(load: () => Promise<LazyModule>): (ctx: RunConte
242
258
  export interface HookFilter {
243
259
  command?: RegExp;
244
260
  }
261
+ /** What a hook is handed. `argv` on `parse` only; `options` is empty on `parse` and `shutdown`. */
262
+ export interface HookContext {
263
+ command: string;
264
+ options: Record<string, unknown>;
265
+ argv?: string[];
266
+ }
245
267
  export interface Hook {
246
268
  filter?: HookFilter;
247
- handler: (ctx: {
248
- command: string;
249
- options: Record<string, unknown>;
250
- }) => void | Promise<void>;
269
+ /** `parse` may return the argv to use instead; every other stage's return is ignored. */
270
+ handler: (ctx: HookContext) => unknown;
251
271
  }
272
+ /** The stages a plugin hook fires at. `parse` and `shutdown` bracket the run (D-122). */
273
+ export type HookStage = 'parse' | 'preRun' | 'postRun' | 'onError' | 'shutdown';
252
274
  /** Rolldown's lesson: evaluate the filter before crossing the boundary. */
253
275
  export declare function hookApplies(hook: Hook | undefined, command: string): hook is Hook;
254
276
  export declare class Manifest {
@@ -277,7 +299,15 @@ export declare class Manifest {
277
299
  use(plugin: Plugin): void;
278
300
  /** `enforce: 'pre'` first, then unordered, then `'post'` — the Vite/Rolldown convention. */
279
301
  private ordered;
280
- fire(stage: 'preRun' | 'postRun' | 'onError', command: string, options: Record<string, unknown>): Promise<void>;
302
+ /** Whether any registered plugin declares a hook at `stage` — so a run without one pays nothing. */
303
+ declares(stage: HookStage): boolean;
304
+ /**
305
+ * `parse` (D-122): argv in, argv out, before the command is resolved. Each plugin, in
306
+ * `enforce` order, is handed what the previous one returned; returning nothing keeps it.
307
+ * A filter is matched against the typed argv, since no command has been resolved yet.
308
+ */
309
+ parse(argv: string[]): Promise<string[]>;
310
+ fire(stage: Exclude<HookStage, 'parse'>, command: string, options: Record<string, unknown>): Promise<void>;
281
311
  find(path: string[]): CommandNode | undefined;
282
312
  /**
283
313
  * Longest-prefix match of argv against declared command paths. The root's own
package/dist/manifest.js CHANGED
@@ -49,6 +49,12 @@ export class Manifest {
49
49
  ordered() {
50
50
  return [...this.plugins].sort((a, b) => (a.enforce === undefined ? 1 : ORDER[a.enforce]) - (b.enforce === undefined ? 1 : ORDER[b.enforce]));
51
51
  }
52
+ declares(stage) {
53
+ return this.plugins.some((p) => p.hooks?.[stage] !== undefined);
54
+ }
55
+ async parse(argv) {
56
+ return (await import('./parse-hooks.js')).runParseHooks(this.ordered(), argv);
57
+ }
52
58
  async fire(stage, command, options) {
53
59
  for (const plugin of this.ordered()) {
54
60
  const hook = plugin.hooks?.[stage];
package/dist/mcp.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { Console } from 'node:console';
1
2
  import { createInterface } from 'node:readline';
3
+ import { Writable } from 'node:stream';
2
4
  import { WITHHELD } from './definition.js';
3
5
  import { kebab } from './names.js';
6
+ import { host } from './runtime.js';
4
7
  import { inputSchemaOf, runnable, typedName } from './schema.js';
5
8
  export const MCP_PROTOCOL_VERSION = '2025-06-18';
6
9
  const JSON_RPC_INVALID_REQUEST = -32600;
@@ -57,6 +60,64 @@ export function argvOf(node, root, args) {
57
60
  const command = typedName(node, root).split(' ').filter((s) => s !== '');
58
61
  return [...command, ...optionArgs(node, args), '--json', ...positionalArgs(node, args)];
59
62
  }
63
+ const STDOUT_CONSOLE = ['log', 'info', 'debug', 'dir', 'dirxml', 'table', 'group', 'groupCollapsed', 'groupEnd', 'count', 'countReset', 'time', 'timeLog', 'timeEnd'];
64
+ let framing = false;
65
+ let sessions = 0;
66
+ let release = () => undefined;
67
+ function holdStdout() {
68
+ if (sessions++ === 0) {
69
+ const stdout = host.stdout;
70
+ const write = stdout.write;
71
+ stdout.write = function (...args) {
72
+ return Reflect.apply(framing ? write : host.stderr.write, framing ? stdout : host.stderr, args);
73
+ };
74
+ release = () => void (stdout.write = write);
75
+ }
76
+ let held = true;
77
+ return () => {
78
+ if (held && --sessions === 0)
79
+ release();
80
+ held = false;
81
+ };
82
+ }
83
+ async function printedBy(run) {
84
+ const chunks = [];
85
+ const decoder = new TextDecoder();
86
+ const take = (chunk) => void chunks.push(typeof chunk === 'string' ? chunk : decoder.decode(chunk));
87
+ const sink = new Writable({
88
+ decodeStrings: false,
89
+ write(chunk, _encoding, done) {
90
+ take(chunk);
91
+ done();
92
+ },
93
+ });
94
+ const stdout = host.stdout;
95
+ const write = stdout.write;
96
+ stdout.write = function (chunk, ...rest) {
97
+ if (framing)
98
+ return Reflect.apply(write, stdout, [chunk, ...rest]);
99
+ take(chunk);
100
+ const done = rest.find((r) => typeof r === 'function');
101
+ if (done !== undefined)
102
+ queueMicrotask(done);
103
+ return true;
104
+ };
105
+ const printer = new Console({ stdout: sink, stderr: host.stderr });
106
+ const saved = STDOUT_CONSOLE.map((m) => [m, console[m]]);
107
+ for (const m of STDOUT_CONSOLE)
108
+ Reflect.set(console, m, printer[m].bind(printer));
109
+ try {
110
+ return { settled: { status: 'fulfilled', value: await run() }, printed: chunks.join('') };
111
+ }
112
+ catch (reason) {
113
+ return { settled: { status: 'rejected', reason }, printed: chunks.join('') };
114
+ }
115
+ finally {
116
+ for (const [m, fn] of saved)
117
+ Reflect.set(console, m, fn);
118
+ stdout.write = write;
119
+ }
120
+ }
60
121
  async function callTool(session, params) {
61
122
  const { manifest, invoke } = session;
62
123
  const root = manifest.rootPath;
@@ -67,9 +128,22 @@ async function callTool(session, params) {
67
128
  if (node === undefined)
68
129
  return { error: { code: JSON_RPC_INVALID_PARAMS, message: `unknown tool "${name}"` } };
69
130
  const args = (given['arguments'] ?? {});
70
- const { stdout, stderr, code } = await invoke(argvOf(node, root, args));
71
- const text = stdout.trim() !== '' ? stdout.trim() : stderr.trim();
72
- return { result: { content: [{ type: 'text', text }], isError: code !== 0 } };
131
+ const { settled, printed } = await printedBy(async () => await invoke(argvOf(node, root, args)));
132
+ let text;
133
+ let isError;
134
+ if (settled.status === 'fulfilled') {
135
+ const { stdout, stderr, code } = settled.value;
136
+ text = stdout.trim() !== '' ? stdout.trim() : stderr.trim();
137
+ isError = code !== 0;
138
+ }
139
+ else {
140
+ text = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
141
+ isError = true;
142
+ }
143
+ const content = [{ type: 'text', text }];
144
+ if (printed.trim() !== '')
145
+ content.push({ type: 'text', text: printed.trimEnd() });
146
+ return { result: { content, isError } };
73
147
  }
74
148
  async function handle(session, request) {
75
149
  switch (request.method) {
@@ -91,14 +165,23 @@ export function startMcp(manifest, opts) {
91
165
  invoke: opts.invoke,
92
166
  serverInfo: { name: manifest.rootPath.join(' ') || 'burgee', version: manifest.version ?? '0.0.0' },
93
167
  };
94
- const reply = (body) => void opts.output.write(`${JSON.stringify({ jsonrpc: '2.0', ...body })}\n`);
168
+ const reply = (body) => {
169
+ framing = true;
170
+ try {
171
+ opts.output.write(`${JSON.stringify({ jsonrpc: '2.0', ...body })}\n`);
172
+ }
173
+ finally {
174
+ framing = false;
175
+ }
176
+ };
95
177
  const swap = (next, invoke) => {
96
178
  session.manifest = next;
97
179
  if (invoke !== undefined)
98
180
  session.invoke = invoke;
99
181
  reply({ method: 'notifications/tools/list_changed' });
100
182
  };
101
- const done = serve(session, opts.input, reply);
183
+ const unhold = holdStdout();
184
+ const done = serve(session, opts.input, reply).finally(unhold);
102
185
  return { done, swap };
103
186
  }
104
187
  export async function serveMcp(manifest, opts) {
package/dist/migrate.d.ts CHANGED
@@ -1,13 +1,25 @@
1
- import { type Graded } from './compat.js';
1
+ import { type Row } from './compat.js';
2
+ /** The npm package a specifier names: `@scope/name/x` → `@scope/name`, `name/x` → `name`. */
3
+ export declare function packageOf(specifier: string): string;
2
4
  /**
3
- * A2 — the whole mapping, as data.
5
+ * A2, A12 — the whole mapping, as data, and none of it typed here.
6
+ *
7
+ * Every drop-in the oracle grades **level** with its incumbent (D-137): the incumbent's own
8
+ * suite passes as many cases against the family's replacement as against the incumbent
9
+ * itself, in the same harness. That is commander and yargs, and chalk, ora, string-width,
10
+ * cross-spawn, signal-exit and the rest — `compat.ts` holds the list and a lock re-derives it
11
+ * from `compat-oracle`. A drop-in that is not level yet is reported, never rewritten.
4
12
  *
5
13
  * Whole specifiers only. `burgee/commander` is not a key, which is what makes a second run
6
14
  * over an already-migrated tree a no-op and lets the command declare `effects: 'idempotent'`.
15
+ * `yargs/yargs` is the one key the oracle does not name: yargs documents it as an entry,
16
+ * and it is the same module as `yargs`.
7
17
  */
8
18
  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"];
19
+ /** The packages a project depends on that this command rewrites (A1). */
20
+ export declare const HOSTS: readonly string[];
21
+ /** The leading number of a version or a range — `^3.0.7` is 3, `>=18` is 18; `undefined` for `*` or a tag. */
22
+ export declare function majorOf(version: string): number | undefined;
11
23
  /**
12
24
  * Every name each target exports — values and types alike — so a rewrite that moves
13
25
  * `import { Argv } from 'yargs'` can first ask whether `burgee/yargs` has an `Argv`.
@@ -24,8 +36,24 @@ export declare const FACADE_EXPORTS: Readonly<Record<string, readonly string[]>>
24
36
  *
25
37
  * `unknown-export` is a named import the target does not export — rewriting it would turn a
26
38
  * working import into TS2305 or a `SyntaxError` at load, so the file stays as it was.
39
+ *
40
+ * `require-of-default` is a `require()` whose two sides hand back different kinds of value.
41
+ * `require()` of an ES module returns its namespace, so `require('cross-spawn')` — a function —
42
+ * rewritten to a target with a default and no `'module.exports'` would make `spawn(...)` throw.
43
+ * A `require('chalk')` of chalk 6, which is ESM only, already returns a namespace, and moves to
44
+ * a target that does too ({@link REQUIRE_NAMESPACE}, A29). Where the shapes differ, the file
45
+ * stays on the incumbent, where it works.
27
46
  */
28
- export type RefusalReason = 'deep-import' | 'non-literal-specifier' | 'unknown-export';
47
+ export type RefusalReason = 'deep-import' | 'non-literal-specifier' | 'unknown-export' | 'require-of-default';
48
+ /**
49
+ * Incumbents whose own `require()` already returns an ES namespace — they ship ESM only and
50
+ * export no `'module.exports'`, so `require('chalk').default` is how a CommonJS caller of
51
+ * chalk 6 reaches it. Moving that line to a target that also hands back its namespace is
52
+ * exact (A29). Derived, not typed: `migrate-require.test.ts` holds this list equal to what
53
+ * Node's own `require()` returns for every installed incumbent in `MAPPING`, so a name here
54
+ * is a measurement and an incumbent that is not installed is left out, and refused as before.
55
+ */
56
+ export declare const REQUIRE_NAMESPACE: readonly string[];
29
57
  export interface Refusal {
30
58
  /** Relative to the directory being migrated, with forward slashes on every platform. */
31
59
  file: string;
@@ -64,6 +92,18 @@ export interface Detection {
64
92
  /** Hosts a source file actually imports — used. These two disagree more often than not. */
65
93
  imported: string[];
66
94
  }
95
+ /** An incumbent the project has on a different major from the one graded — left alone (A12). */
96
+ export interface OffMajor {
97
+ from: string;
98
+ /** The installed version, or the declared range when nothing is installed here. */
99
+ found: string;
100
+ graded: string;
101
+ }
102
+ /** A declared incumbent with a graded drop-in that is not level yet (A12). */
103
+ export interface NotLevel extends Row {
104
+ from: string;
105
+ to: string;
106
+ }
67
107
  export interface MigrationReport {
68
108
  files: number;
69
109
  imports: number;
@@ -72,14 +112,22 @@ export interface MigrationReport {
72
112
  /** Type-only imports left pointing at the incumbent, each with the note that says why. */
73
113
  kept: Kept[];
74
114
  detected: Detection;
115
+ /** `add`: the family packages the rewritten imports now name, which the project must depend on. */
75
116
  dependencies: {
76
117
  before: string[];
77
118
  removable: string[];
78
119
  after: number;
120
+ add: string[];
79
121
  };
80
- graded: (Graded & {
122
+ graded: (Row & {
81
123
  host: string;
82
124
  })[];
125
+ /** Declared incumbents whose drop-in is graded but not level — left alone, with the grade that says why. */
126
+ partial: NotLevel[];
127
+ /** Incumbents on a major the oracle did not grade — left alone, never rewritten onto an API they do not use. */
128
+ offMajor: OffMajor[];
129
+ /** The install and uninstall to run next, for the package manager the lockfile names; `''` when there is none. */
130
+ next: string;
83
131
  dryRun: boolean;
84
132
  /** N7 — an idempotent command says whether it changed anything; silence is what an agent misreads. */
85
133
  changed: boolean;
@@ -97,6 +145,8 @@ interface Site {
97
145
  * — the import clause, which is all `bindingsOf` needs. Absent for the other three positions.
98
146
  */
99
147
  clause?: string[];
148
+ /** `require('x')`: CommonJS receives the target's whole namespace, not its default export. */
149
+ require?: true;
100
150
  }
101
151
  interface Scan {
102
152
  sites: Site[];
@@ -154,7 +204,7 @@ export declare function bindingsOf(clause: readonly string[]): {
154
204
  * source is returned unchanged the moment there is a refusal in it: the unit of success is
155
205
  * the file, so the worst case is *nothing changed here, and here is why* (D-051).
156
206
  */
157
- export declare function rewriteSource(source: string): Rewrite;
207
+ export declare function rewriteSource(source: string, skip?: ReadonlySet<string>): Rewrite;
158
208
  /** Every source file under `dir`, relative and slash-separated so a report reads the same everywhere. */
159
209
  export declare function sourceFiles(dir: string, at?: string, found?: string[]): string[];
160
210
  /**