burgee 0.11.1 → 0.12.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.
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 +68 -4
  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
package/dist/migrate.js CHANGED
@@ -1,17 +1,40 @@
1
- import { readdirSync } from 'node:fs';
1
+ import { existsSync, readdirSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { ambientRuntime, run } from 'bellpull';
5
- import { GRADED } from './compat.js';
5
+ import { DROP_INS, GRADED, GRADED_VERSIONS, isLevel } from './compat.js';
6
6
  import { ExitCode } from './exit-code.js';
7
- export const MAPPING = {
8
- commander: 'burgee/commander',
9
- yargs: 'burgee/yargs',
10
- 'yargs/yargs': 'burgee/yargs',
11
- 'yargs/helpers': 'burgee/yargs/helpers',
12
- };
13
- export const HOSTS = ['commander', 'yargs'];
7
+ export function packageOf(specifier) {
8
+ const parts = specifier.split('/');
9
+ return parts.slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
10
+ }
11
+ export const MAPPING = Object.fromEntries(DROP_INS.filter((d) => isLevel(d.host)).flatMap((d) => (d.from === 'yargs' ? [[d.from, d.to], ['yargs/yargs', d.to]] : [[d.from, d.to]])));
12
+ export const HOSTS = [...new Set(Object.keys(MAPPING).map(packageOf))];
13
+ const PARTIAL = DROP_INS.filter((d) => !isLevel(d.host));
14
+ export function majorOf(version) {
15
+ const digits = /\d+/.exec(version);
16
+ return digits === null ? undefined : Number(digits[0]);
17
+ }
18
+ const HOST_OF = new Map(DROP_INS.map((d) => [packageOf(d.from), d.host]));
14
19
  export const FACADE_EXPORTS = {
20
+ 'bellpull/cross-spawn': [
21
+ 'ChildProcess',
22
+ 'Parsed',
23
+ 'SpawnOptions',
24
+ 'SpawnSyncReturns',
25
+ '_enoent',
26
+ 'crossSpawn',
27
+ 'default',
28
+ 'module.exports',
29
+ 'parse',
30
+ 'spawn',
31
+ 'sync',
32
+ ],
33
+ 'bellpull/node-which': [
34
+ 'NodeWhichOptions',
35
+ 'default',
36
+ 'module.exports',
37
+ ],
15
38
  'burgee/commander': [
16
39
  'AddHelpTextContext',
17
40
  'AddHelpTextPosition',
@@ -88,12 +111,312 @@ export const FACADE_EXPORTS = {
88
111
  'isPromise',
89
112
  'isYargsInstance',
90
113
  'looksLikeNumber',
114
+ 'module.exports',
91
115
  'objFilter',
92
116
  'parseCommand',
93
117
  'platformShim',
94
118
  ],
95
- 'burgee/yargs/helpers': ['Parser', 'applyExtends', 'hideBin'],
119
+ 'burgee/yargs/helpers': [
120
+ 'Parser',
121
+ 'applyExtends',
122
+ 'hideBin',
123
+ ],
124
+ 'burgee/yargs/parser': [
125
+ 'Arguments',
126
+ 'Configuration',
127
+ 'DetailedArguments',
128
+ 'Options',
129
+ 'Parser',
130
+ 'ParserMixin',
131
+ 'YargsParser',
132
+ 'camelCase',
133
+ 'decamelize',
134
+ 'default',
135
+ 'looksLikeNumber',
136
+ 'module.exports',
137
+ 'tokenizeArgString',
138
+ ],
139
+ 'caique/inquirer': [
140
+ 'AbortPromptError',
141
+ 'CancelPromptError',
142
+ 'CancelablePromise',
143
+ 'Context',
144
+ 'ExitPromptError',
145
+ 'HookError',
146
+ 'Keybinding',
147
+ 'KeypressEvent',
148
+ 'PartialTheme',
149
+ 'Prompt',
150
+ 'Separator',
151
+ 'SetState',
152
+ 'Status',
153
+ 'Theme',
154
+ 'ValidationError',
155
+ 'ViewFunction',
156
+ 'createPrompt',
157
+ 'defaultTheme',
158
+ 'getDefaultKeybindings',
159
+ 'getDefaultTheme',
160
+ 'isBackspaceKey',
161
+ 'isDownKey',
162
+ 'isEnterKey',
163
+ 'isNumberKey',
164
+ 'isShiftKey',
165
+ 'isSpaceKey',
166
+ 'isTabKey',
167
+ 'isUpKey',
168
+ 'makeTheme',
169
+ 'useEffect',
170
+ 'useKeypress',
171
+ 'useMemo',
172
+ 'usePrefix',
173
+ 'useRef',
174
+ 'useState',
175
+ ],
176
+ 'closeout/exit-hook': [
177
+ 'AsyncExitHookOptions',
178
+ 'ExitHookCallback',
179
+ 'Options',
180
+ 'asyncExitHook',
181
+ 'default',
182
+ 'gracefulExit',
183
+ ],
184
+ 'closeout/restore-cursor': [
185
+ 'default',
186
+ ],
187
+ 'closeout/signal-exit': [
188
+ 'load',
189
+ 'onExit',
190
+ 'signals',
191
+ 'unload',
192
+ ],
193
+ 'closeout/signal-exit/signals': [
194
+ 'signals',
195
+ ],
196
+ 'flagstaff/boxen': [
197
+ 'BoxenBorderStyle',
198
+ 'BoxenOptions',
199
+ 'Boxes',
200
+ 'CustomBorderStyle',
201
+ 'Options',
202
+ 'Spacing',
203
+ '_borderStyles',
204
+ 'default',
205
+ ],
206
+ 'flagstaff/cli-table3': [
207
+ 'Cell',
208
+ 'ColSpanCell',
209
+ 'RowSpanCell',
210
+ 'Table',
211
+ 'TableChars',
212
+ 'TableOptions',
213
+ 'TableStyle',
214
+ 'computeHeights',
215
+ 'computeWidths',
216
+ 'default',
217
+ 'hyperlink',
218
+ 'makeTableLayout',
219
+ 'mergeOptions',
220
+ 'module.exports',
221
+ 'pad',
222
+ 'strlen',
223
+ 'truncate',
224
+ 'wordWrap',
225
+ ],
226
+ 'flagstaff/log-update': [
227
+ 'LogUpdate',
228
+ 'LogUpdateOptions',
229
+ 'LogUpdateStream',
230
+ 'Options',
231
+ 'createLogUpdate',
232
+ 'default',
233
+ 'logUpdateStderr',
234
+ ],
235
+ 'flagstaff/ora': [
236
+ 'Affix',
237
+ 'Color',
238
+ 'Options',
239
+ 'Ora',
240
+ 'OraStream',
241
+ 'PersistOptions',
242
+ 'PrefixTextGenerator',
243
+ 'PromiseOptions',
244
+ 'Spinner',
245
+ 'SpinnerDefinition',
246
+ 'SuffixTextGenerator',
247
+ 'default',
248
+ 'oraPromise',
249
+ 'spinners',
250
+ ],
251
+ 'linegauge': [
252
+ 'Options',
253
+ 'TruncateOptions',
254
+ 'WidthOptions',
255
+ 'WrapOptions',
256
+ 'default',
257
+ 'lineCount',
258
+ 'measure',
259
+ 'slice',
260
+ 'strip',
261
+ 'truncate',
262
+ 'widest',
263
+ 'width',
264
+ 'wrap',
265
+ ],
266
+ 'linegauge/slice': [
267
+ 'default',
268
+ 'slice',
269
+ ],
270
+ 'linegauge/strip': [
271
+ 'default',
272
+ 'strip',
273
+ ],
274
+ 'linegauge/wrap': [
275
+ 'Options',
276
+ 'WrapOptions',
277
+ 'default',
278
+ 'visibleWidth',
279
+ 'wrap',
280
+ ],
281
+ 'paratext': [
282
+ 'AnsiEscapes',
283
+ 'Capability',
284
+ 'CapabilityError',
285
+ 'ConEmu',
286
+ 'DEPRECATED',
287
+ 'Fields',
288
+ 'ImageOptions',
289
+ 'NotImplemented',
290
+ 'Runtime',
291
+ 'Support',
292
+ 'ansiEscapesFor',
293
+ 'beep',
294
+ 'beginSynchronizedOutput',
295
+ 'bell',
296
+ 'builtins',
297
+ 'capabilities',
298
+ 'capability',
299
+ 'check',
300
+ 'clearScreen',
301
+ 'clearTerminal',
302
+ 'clearViewport',
303
+ 'clipboard',
304
+ 'cursorBackward',
305
+ 'cursorDown',
306
+ 'cursorForward',
307
+ 'cursorGetPosition',
308
+ 'cursorHide',
309
+ 'cursorLeft',
310
+ 'cursorMove',
311
+ 'cursorNextLine',
312
+ 'cursorPrevLine',
313
+ 'cursorRestorePosition',
314
+ 'cursorSavePosition',
315
+ 'cursorShow',
316
+ 'cursorTo',
317
+ 'cursorUp',
318
+ 'cwd',
319
+ 'default',
320
+ 'emit',
321
+ 'endSynchronizedOutput',
322
+ 'enterAlternativeScreen',
323
+ 'eraseDown',
324
+ 'eraseEndLine',
325
+ 'eraseLine',
326
+ 'eraseLines',
327
+ 'eraseScreen',
328
+ 'eraseStartLine',
329
+ 'eraseUp',
330
+ 'exitAlternativeScreen',
331
+ 'fieldsUsed',
332
+ 'iTerm',
333
+ 'image',
334
+ 'isDeprecation',
335
+ 'link',
336
+ 'notify',
337
+ 'processRuntime',
338
+ 'refusals',
339
+ 'register',
340
+ 'registerBuiltins',
341
+ 'render',
342
+ 'reset',
343
+ 'scrollDown',
344
+ 'scrollUp',
345
+ 'setCwd',
346
+ 'supports',
347
+ 'synchronizedOutput',
348
+ 'title',
349
+ ],
350
+ 'paratext/terminal-link': [
351
+ 'LinkOptions',
352
+ 'Options',
353
+ 'Target',
354
+ 'TerminalLink',
355
+ 'default',
356
+ 'terminalLinkFor',
357
+ ],
358
+ 'roundel/chalk': [
359
+ 'BackgroundColor',
360
+ 'BackgroundColorName',
361
+ 'Chalk',
362
+ 'ChalkInstance',
363
+ 'ChalkOptions',
364
+ 'Color',
365
+ 'ColorInfo',
366
+ 'ColorName',
367
+ 'ColorSupport',
368
+ 'ColorSupportLevel',
369
+ 'ForegroundColor',
370
+ 'ForegroundColorName',
371
+ 'ModifierName',
372
+ 'Modifiers',
373
+ 'Options',
374
+ 'UnderlineColorName',
375
+ 'backgroundColorNames',
376
+ 'chalkStderr',
377
+ 'colorNames',
378
+ 'default',
379
+ 'foregroundColorNames',
380
+ 'modifierNames',
381
+ 'supportsColor',
382
+ 'supportsColorStderr',
383
+ 'underlineColorNames',
384
+ ],
385
+ 'seniority/lilconfig': [
386
+ 'AsyncSearcher',
387
+ 'LilconfigResult',
388
+ 'Loader',
389
+ 'LoaderSync',
390
+ 'Loaders',
391
+ 'LoadersSync',
392
+ 'Options',
393
+ 'OptionsSync',
394
+ 'SyncSearcher',
395
+ 'Transform',
396
+ 'TransformSync',
397
+ 'defaultLoaders',
398
+ 'defaultLoadersSync',
399
+ 'lilconfig',
400
+ 'lilconfigSync',
401
+ ],
402
+ 'seniority/rc': [
403
+ 'RcConfig',
404
+ 'RcOptions',
405
+ 'RcParse',
406
+ 'default',
407
+ 'module.exports',
408
+ 'parse',
409
+ 'rc',
410
+ ],
96
411
  };
412
+ export const REQUIRE_NAMESPACE = ['ansi-escapes', 'chalk', 'ora', 'log-update', 'boxen', 'string-width', 'strip-ansi', 'wrap-ansi', 'slice-ansi', 'restore-cursor', 'exit-hook', 'terminal-link'];
413
+ function requireShapesDiffer(from, to) {
414
+ const exported = FACADE_EXPORTS[to] ?? [];
415
+ const toGivesDefault = exported.includes('module.exports');
416
+ if (REQUIRE_NAMESPACE.includes(from))
417
+ return toGivesDefault;
418
+ return exported.includes('default') && !toGivesDefault;
419
+ }
97
420
  const WORD = /[A-Za-z0-9_$]/;
98
421
  const DIVIDES = new Set([')', ']', '}']);
99
422
  function startsRegex(previous) {
@@ -220,6 +543,8 @@ function siteOf(source, at, tokens) {
220
543
  const site = { specifier: source.slice(open + 1, close - 1), start: open + 1, end: close - 1, line };
221
544
  if (tokens.previous === 'from' && tokens.clause !== undefined)
222
545
  site.clause = tokens.clause.slice(0, -1);
546
+ if (tokens.previous === '(' && tokens.before === 'require')
547
+ site.require = true;
223
548
  return site;
224
549
  }
225
550
  export function scan(source) {
@@ -293,27 +618,40 @@ function missingFrom(site, to) {
293
618
  const { typeOnly, names } = bindingsOf(site.clause);
294
619
  return { typeOnly, missing: names.filter((name) => !exported.includes(name)) };
295
620
  }
296
- export function rewriteSource(source) {
621
+ function classify(site) {
622
+ const to = MAPPING[site.specifier];
623
+ if (to === undefined)
624
+ return 'unmapped';
625
+ if (site.require === true && requireShapesDiffer(site.specifier, to))
626
+ return { line: site.line, specifier: site.specifier, reason: 'require-of-default' };
627
+ const { typeOnly, missing } = missingFrom(site, to);
628
+ if (missing.length === 0)
629
+ return 'moves';
630
+ if (typeOnly)
631
+ return { 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` };
632
+ return { line: site.line, specifier: site.specifier, reason: 'unknown-export', names: missing };
633
+ }
634
+ const NONE = new Set();
635
+ export function rewriteSource(source, skip = NONE) {
297
636
  if (!mentionsAHost(source))
298
637
  return { source, mapped: [], refused: [], kept: [], relevant: false };
299
638
  const { sites, nonLiteral } = scan(source);
300
- const hits = sites.filter((s) => MAPPING[s.specifier] !== undefined || isDeep(s.specifier));
639
+ const hits = sites.filter((s) => !skip.has(packageOf(s.specifier)) && (MAPPING[s.specifier] !== undefined || isDeep(s.specifier)));
301
640
  if (hits.length === 0)
302
641
  return { source, mapped: [], refused: [], kept: [], relevant: false };
303
642
  const kept = [];
304
643
  const unknown = [];
305
644
  const moving = [];
306
645
  for (const site of hits) {
307
- const to = MAPPING[site.specifier];
308
- if (to === undefined)
646
+ const verdict = classify(site);
647
+ if (verdict === 'unmapped')
309
648
  continue;
310
- const { typeOnly, missing } = missingFrom(site, to);
311
- if (missing.length === 0)
649
+ if (verdict === 'moves')
312
650
  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` });
651
+ else if ('note' in verdict)
652
+ kept.push(verdict);
315
653
  else
316
- unknown.push({ line: site.line, specifier: site.specifier, reason: 'unknown-export', names: missing });
654
+ unknown.push(verdict);
317
655
  }
318
656
  const refused = [
319
657
  ...hits.filter((s) => isDeep(s.specifier)).map((s) => ({ line: s.line, specifier: s.specifier, reason: 'deep-import' })),
@@ -345,16 +683,48 @@ export function sourceFiles(dir, at = '', found = []) {
345
683
  }
346
684
  return found;
347
685
  }
348
- async function declaredHosts(dir) {
686
+ async function declaredDependencies(dir) {
349
687
  try {
350
688
  const raw = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
351
- const declared = { ...raw.dependencies, ...raw.devDependencies };
352
- return HOSTS.filter((host) => declared[host] !== undefined);
689
+ return new Map(Object.entries({ ...raw.dependencies, ...raw.devDependencies }));
353
690
  }
354
691
  catch {
355
- return [];
692
+ return new Map();
356
693
  }
357
694
  }
695
+ async function installedVersion(dir, name) {
696
+ try {
697
+ return JSON.parse(await readFile(join(dir, 'node_modules', name, 'package.json'), 'utf8')).version;
698
+ }
699
+ catch {
700
+ return undefined;
701
+ }
702
+ }
703
+ async function offMajorOf(dir, dependencies) {
704
+ const found = await Promise.all(HOSTS.map(async (from) => ({ from, found: (await installedVersion(dir, from)) ?? dependencies.get(from) })));
705
+ return found.flatMap(({ from, found: version }) => {
706
+ const graded = GRADED_VERSIONS[from];
707
+ if (version === undefined || graded === undefined)
708
+ return [];
709
+ const major = majorOf(version);
710
+ return major === undefined || major === majorOf(graded) ? [] : [{ from, found: version, graded }];
711
+ });
712
+ }
713
+ function installer(dir) {
714
+ const has = (file) => existsSync(join(dir, file));
715
+ if (has('pnpm-lock.yaml'))
716
+ return { add: 'pnpm add', remove: 'pnpm remove' };
717
+ if (has('yarn.lock'))
718
+ return { add: 'yarn add', remove: 'yarn remove' };
719
+ if (has('bun.lockb') || has('bun.lock'))
720
+ return { add: 'bun add', remove: 'bun remove' };
721
+ return { add: 'npm install', remove: 'npm uninstall' };
722
+ }
723
+ function nextStep(dir, add, remove) {
724
+ const pm = installer(dir);
725
+ const steps = [add.length > 0 ? `${pm.add} ${add.join(' ')}` : '', remove.length > 0 ? `${pm.remove} ${remove.join(' ')}` : ''];
726
+ return steps.filter((c) => c !== '').join(' && ');
727
+ }
358
728
  export async function workingTree(dir) {
359
729
  const result = await run('git', ['-C', dir, 'status', '--porcelain'], { runtime: ambientRuntime(), stdio: 'pipe' });
360
730
  if (!result.ok)
@@ -371,9 +741,9 @@ export class DirtyTreeError extends Error {
371
741
  }
372
742
  }
373
743
  const EMPTY = { source: '', mapped: [], refused: [], kept: [], relevant: false };
374
- async function migrateBatch(dir, batch, write) {
744
+ async function migrateBatch(dir, batch, write, skip) {
375
745
  const sources = await Promise.all(batch.map(async (file) => await readFile(join(dir, file))));
376
- const results = sources.map((bytes) => (mentionsAHost(bytes) ? rewriteSource(bytes.toString('utf8')) : EMPTY));
746
+ const results = sources.map((bytes) => (mentionsAHost(bytes) ? rewriteSource(bytes.toString('utf8'), skip) : EMPTY));
377
747
  if (write)
378
748
  await Promise.all(results.map(async (r, i) => (r.mapped.length === 0 ? undefined : await writeFile(join(dir, batch[i]), r.source))));
379
749
  return results;
@@ -386,7 +756,8 @@ function rollup(all) {
386
756
  })
387
757
  .filter((row) => row.imports > 0);
388
758
  }
389
- function gradedFor(hosts) {
759
+ function gradedFor(packages) {
760
+ const hosts = [...new Set(packages.map((p) => HOST_OF.get(p)).filter((h) => h !== undefined))];
390
761
  return hosts.filter((host) => GRADED[host] !== undefined).map((host) => ({ host, ...GRADED[host] }));
391
762
  }
392
763
  const BATCH = 256;
@@ -395,30 +766,38 @@ export async function migrate(options) {
395
766
  const entries = await (options.status ?? workingTree)(dir);
396
767
  if (!dryRun && !force && entries !== undefined && entries.length > 0)
397
768
  throw new DirtyTreeError(entries);
769
+ const dependencies = await declaredDependencies(dir);
770
+ const offMajor = await offMajorOf(dir, dependencies);
771
+ const skip = new Set(offMajor.map((o) => o.from));
398
772
  const files = sourceFiles(dir);
399
773
  const results = [];
400
774
  for (let i = 0; i < files.length; i += BATCH) {
401
775
  const batch = files.slice(i, i + BATCH);
402
- const done = await migrateBatch(dir, batch, !dryRun);
776
+ const done = await migrateBatch(dir, batch, !dryRun, skip);
403
777
  results.push(...done.map((result, k) => ({ file: batch[k], result })));
404
778
  }
405
779
  const all = results.flatMap(({ file, result }) => result.mapped.map((m) => ({ ...m, file })));
406
780
  const refused = results.flatMap(({ file, result }) => result.refused.map((r) => ({ file, ...r })));
407
781
  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)] : [])))];
409
- const declared = await declaredHosts(dir);
410
- const stillUsed = new Set([...refused, ...kept].map((r) => r.specifier.split('/')[0] ?? ''));
411
- const removable = declared.filter((host) => !stillUsed.has(host));
782
+ const imported = [...new Set(results.flatMap(({ result }) => (result.relevant ? [...result.mapped.map((m) => packageOf(m.from)), ...result.kept.map((k) => packageOf(k.specifier))] : [])))].sort();
783
+ const declared = HOSTS.filter((host) => dependencies.has(host));
784
+ const stillUsed = new Set([...refused, ...kept].map((r) => packageOf(r.specifier)));
785
+ const removable = declared.filter((host) => !stillUsed.has(host) && !skip.has(host));
412
786
  const touched = [...new Set(all.map((m) => m.file))];
787
+ const add = [...new Set(all.map((m) => packageOf(m.to)))].filter((p) => !dependencies.has(p)).sort();
788
+ const partial = PARTIAL.filter((d) => dependencies.has(d.from)).map((d) => ({ from: d.from, to: d.to, ...GRADED[d.host] }));
413
789
  return {
414
790
  files: touched.length,
415
791
  imports: all.length,
416
792
  mapped: rollup(all),
417
793
  refused,
418
794
  kept,
419
- detected: { declared, imported: [...new Set(imported.map((s) => s.split('/')[0] ?? s))].sort() },
420
- dependencies: { before: declared, removable, after: declared.length - removable.length },
421
- graded: gradedFor([...new Set([...declared, ...imported.map((s) => s.split('/')[0] ?? s)])].sort()),
795
+ detected: { declared, imported },
796
+ dependencies: { before: declared, removable, after: declared.length - removable.length, add },
797
+ graded: gradedFor([...new Set([...declared, ...imported])].sort()),
798
+ partial,
799
+ offMajor,
800
+ next: nextStep(dir, add, removable),
422
801
  dryRun,
423
802
  changed: !dryRun && touched.length > 0,
424
803
  exitCode: refused.length > 0 ? ExitCode.RUNTIME : ExitCode.OK,
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+ import { type Plugin } from './plugin.js';
7
+ export declare function runParseHooks(plugins: readonly Plugin[], argv: string[]): Promise<string[]>;
@@ -0,0 +1,17 @@
1
+ import { hookApplies } from './manifest.js';
2
+ export async function runParseHooks(plugins, argv) {
3
+ let current = argv;
4
+ for (const plugin of plugins) {
5
+ const hook = plugin.hooks?.parse;
6
+ const typed = current.join(' ');
7
+ if (!hookApplies(hook, typed))
8
+ continue;
9
+ const next = await hook.handler({ command: typed, options: {}, argv: [...current] });
10
+ if (next === undefined)
11
+ continue;
12
+ if (!Array.isArray(next) || !next.every((a) => typeof a === 'string'))
13
+ throw new Error(`burgee: plugin "${plugin.name}"'s parse hook returned ${typeof next}; return string[] or nothing`);
14
+ current = next;
15
+ }
16
+ return current;
17
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CommandNode, type Hook } from './manifest.js';
1
+ import { type CommandNode, type Hook, type HookStage } from './manifest.js';
2
2
  /**
3
3
  * The plugin contract version. One number for the family — the same `1` flagstaff, caique and
4
4
  * closeout declare, written out rather than imported because a layer never imports a layer.
@@ -12,11 +12,7 @@ export interface Plugin {
12
12
  name: string;
13
13
  contract?: number;
14
14
  commands?: CommandNode[];
15
- hooks?: {
16
- preRun?: Hook;
17
- postRun?: Hook;
18
- onError?: Hook;
19
- };
15
+ hooks?: Partial<Record<HookStage, Hook>>;
20
16
  enforce?: 'pre' | 'post';
21
17
  }
22
18
  export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
package/dist/plugin.js CHANGED
@@ -13,7 +13,7 @@ export class PluginError extends Error {
13
13
  }
14
14
  const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
15
15
  const ENFORCE = ['pre', 'post'];
16
- const STAGES = ['preRun', 'postRun', 'onError'];
16
+ const STAGES = ['parse', 'preRun', 'postRun', 'onError', 'shutdown'];
17
17
  const schema = (message, fix) => new PluginError('E_PLUGIN_SCHEMA', message, fix);
18
18
  export function validate(plugin, taken = []) {
19
19
  if (!isRecord(plugin))
@@ -0,0 +1 @@
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://burgee.dev/program-schema.json","title":"burgee --schema","description":"The document `<program> --schema` prints: the program as data, for an agent or an MCP host to read without running anything. Published so a reader can validate what a program says about itself (burgee F1, D-123); burgee's own test validates `--schema` output against this file on every run.","type":"object","required":["schemaVersion","name","exitCodes","commands"],"additionalProperties":false,"properties":{"schemaVersion":{"const":1,"description":"Bumped only when a field changes meaning."},"name":{"type":"string","description":"The program as typed."},"version":{"type":"string"},"description":{"type":"string"},"exitCodes":{"type":"object","additionalProperties":{"type":"integer"},"description":"What each exit code means (F1): the contract's names and numbers, so a caller branches on the code without reading prose."},"commands":{"type":"array","description":"Every runnable, visible command, in declaration order.","items":{"$ref":"#/$defs/command"}}},"$defs":{"command":{"type":"object","required":["name","arguments","options","examples","inputSchema"],"additionalProperties":false,"properties":{"name":{"type":"string","description":"The command as typed, without the program name: `config get`."},"description":{"type":"string"},"summary":{"type":"string"},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world, or `withheld` (N6)."},"deprecated":{"description":"`true`, or the replacement's name (M5)."},"group":{"type":"string"},"lazy":{"const":true,"description":"Its handler loads on dispatch (M2)."},"plugin":{"type":"string","description":"The plugin that contributed it (M3)."},"fields":{"type":"array","items":{"type":"string"},"description":"The result's top-level fields, what `--json=` selects from (N14)."},"arguments":{"type":"array","items":{"$ref":"#/$defs/argument"}},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"relations":{"type":"array","items":{"type":"object"},"description":"Constraints between options (S2, S6): exactlyOneOf, atLeastOneOf, atMostOneOf, conflicts, implies."},"examples":{"type":"array","items":{"$ref":"#/$defs/example"}},"inputSchema":{"$ref":"#/$defs/inputSchema"}}},"argument":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string"},"required":{"type":"boolean"},"variadic":{"type":"boolean"},"default":{"type":"string"},"type":{"type":"string","pattern":"^file$","description":"`file`: `-` means standard input (S4)."}}},"option":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"required":{"type":"boolean"},"short":{"type":"string"},"default":{"description":"A string, boolean, number, or a list of strings or numbers."},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"multiple":{"type":"boolean"},"separator":{"type":"string"},"dependsOn":{"type":"array","items":{"type":"string"}},"exclusive":{"type":"array","items":{"type":"string"}},"minimum":{"type":"number"},"maximum":{"type":"number"},"integer":{"type":"boolean"},"schema":{"type":"object","description":"A Standard Schema validator, as its own library serialises it."},"placeholder":{"type":"string"},"deprecated":{"description":"`true`, or the replacement's name."},"hidden":{"type":"boolean"},"sharedFrom":{"type":"string"}}},"example":{"type":"object","required":["command"],"additionalProperties":false,"properties":{"command":{"type":"string"},"description":{"type":"string"}}},"inputSchema":{"type":"object","required":["type","properties","required","additionalProperties"],"additionalProperties":false,"description":"The arguments and options as one JSON Schema object — what an MCP tool call takes.","properties":{"type":{"const":"object"},"properties":{"type":"object"},"required":{"type":"array","items":{"type":"string"}},"additionalProperties":{"const":false}}}}}