burgee 0.9.2 → 0.11.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.
- package/README.md +2 -2
- package/dist/commander/command.d.ts +20 -7
- package/dist/commander.d.ts +8 -2
- package/dist/execute.js +1 -10
- package/dist/migrate.d.ts +56 -2
- package/dist/migrate.js +152 -10
- package/dist/schema-surface.d.ts +21 -0
- package/dist/schema-surface.js +38 -0
- package/dist/yargs/types.d.ts +987 -0
- package/dist/yargs/types.js +1 -0
- package/dist/yargs.d.ts +10 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="https://www.npmjs.com/package/burgee"><img src="https://img.shields.io/npm/v/burgee?style=flat-square&color=0a6b47" alt="npm version" /></a>
|
|
16
16
|
<a href="https://www.npmjs.com/package/burgee"><img src="https://img.shields.io/npm/dm/burgee?style=flat-square" alt="npm downloads" /></a>
|
|
17
17
|
<img src="https://img.shields.io/badge/dependencies-5%20in--family-0a6b47?style=flat-square" alt="Five dependencies, all in this repository: bellpull, closeout, linegauge, roundel, seniority" />
|
|
18
|
-
<img src="https://img.shields.io/badge/Node.js-
|
|
18
|
+
<img src="https://img.shields.io/badge/Node.js-20.19%2B%20%7C%2022.13%2B-green.svg?style=flat-square" alt="Node.js 20.19+ or 22.13+" />
|
|
19
19
|
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -164,7 +164,7 @@ so nothing reaches an agent by accident. Register it with any stdio client:
|
|
|
164
164
|
|
|
165
165
|
### Does it have dependencies?
|
|
166
166
|
|
|
167
|
-
None outside
|
|
167
|
+
None outside the burgee family. `burgee` installs five packages from that family —
|
|
168
168
|
`bellpull`, `closeout`, `linegauge`, `roundel` and `seniority` — and each of those takes
|
|
169
169
|
nothing from outside it either: one repository, one release pipeline, one supply chain to
|
|
170
170
|
audit.
|
|
@@ -58,6 +58,22 @@ export interface OutputContext {
|
|
|
58
58
|
error?: boolean;
|
|
59
59
|
}
|
|
60
60
|
export type HookEvent = 'preSubcommand' | 'preAction' | 'postAction';
|
|
61
|
+
/**
|
|
62
|
+
* commander's option-value types, as `typings/index.d.ts` declares them. `any` is
|
|
63
|
+
* commander's choice and the point: `program.opts().port` is usable without a cast, and
|
|
64
|
+
* `opts<T>()` narrows it — a program written against commander's types relies on both;
|
|
65
|
+
* `unknown` here breaks every `opts().x` such a program reads.
|
|
66
|
+
*/
|
|
67
|
+
export type OptionValues = Record<string, any>;
|
|
68
|
+
/** Where an option's value came from. A string, so an author can define their own; the known ones autocomplete. */
|
|
69
|
+
export type OptionValueSource = 'default' | 'config' | 'env' | 'cli' | 'implied' | (string & Record<never, never>) | undefined;
|
|
70
|
+
/** What `.configureHelp()` takes: any subset of `Help`'s methods and settings. */
|
|
71
|
+
export type HelpConfiguration = Partial<Help>;
|
|
72
|
+
/** `.parseOptions()`'s split of an argv into operands and unknown options. */
|
|
73
|
+
export interface ParseOptionsResult {
|
|
74
|
+
operands: string[];
|
|
75
|
+
unknown: string[];
|
|
76
|
+
}
|
|
61
77
|
export type HookListener = (thisCommand: Command, actionCommand: Command) => void | Promise<void>;
|
|
62
78
|
export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll';
|
|
63
79
|
export type AddHelpTextContext = {
|
|
@@ -236,14 +252,11 @@ export declare class Command extends EventEmitter {
|
|
|
236
252
|
* sub --unknown uuu op => [sub], [--unknown uuu op]
|
|
237
253
|
* sub -- --unknown uuu op => [sub --unknown uuu op], []
|
|
238
254
|
*/
|
|
239
|
-
parseOptions(args: string[]):
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
/** Local option values as key-value pairs. */
|
|
244
|
-
opts(): Record<string, unknown>;
|
|
255
|
+
parseOptions(args: string[]): ParseOptionsResult;
|
|
256
|
+
/** Local option values as key-value pairs; `opts<T>()` types them, as commander's own declaration does. */
|
|
257
|
+
opts<T extends OptionValues = OptionValues>(): T;
|
|
245
258
|
/** Merged local and global option values; globals overwrite locals. */
|
|
246
|
-
optsWithGlobals():
|
|
259
|
+
optsWithGlobals<T extends OptionValues = OptionValues>(): T;
|
|
247
260
|
/** Display an error message and exit (or call exitOverride). */
|
|
248
261
|
error(message: string, errorOptions?: ErrorOptions): never;
|
|
249
262
|
/** One failure as the envelope (E3, G5); `fix` only when one candidate was named. */
|
package/dist/commander.d.ts
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
* `.sdlc/intents/commander-compat/spec.md` and `npm run compat`.
|
|
5
5
|
*/
|
|
6
6
|
import { Argument } from './commander/argument.js';
|
|
7
|
-
import { Command } from './commander/command.js';
|
|
7
|
+
import { Command, type OutputConfiguration as ResolvedOutputConfiguration } from './commander/command.js';
|
|
8
8
|
import { Option } from './commander/option.js';
|
|
9
9
|
export { Argument, humanReadableArgName } from './commander/argument.js';
|
|
10
|
-
export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HookEvent, type HookListener, type
|
|
10
|
+
export { Command, useColor, type AddHelpTextContext, type AddHelpTextPosition, type BurgeeParseOptions, type CommandOptions, type ErrorOptions, type ExecutableCommandOptions, type HelpConfiguration, type HookEvent, type HookListener, type OptionValueSource, type OptionValues, type OutputContext, type ParseOptions, type ParseOptionsResult, } from './commander/command.js';
|
|
11
11
|
export { CommanderError, InvalidArgumentError, InvalidArgumentError as InvalidOptionArgumentError } from './commander/error.js';
|
|
12
12
|
export { Help, type HelpContext } from './commander/help.js';
|
|
13
13
|
export { DualOptions, Option } from './commander/option.js';
|
|
14
|
+
/**
|
|
15
|
+
* commander's `OutputConfiguration`: every member optional, because `.configureOutput()`
|
|
16
|
+
* takes any subset of them and a typed program annotates exactly that subset. What
|
|
17
|
+
* `.configureOutput()` hands back is the resolved, complete one.
|
|
18
|
+
*/
|
|
19
|
+
export type OutputConfiguration = Partial<ResolvedOutputConfiguration>;
|
|
14
20
|
/** The root command, for programs that never construct their own. */
|
|
15
21
|
export declare const program: Command;
|
|
16
22
|
export declare const createCommand: (name?: string) => Command;
|
package/dist/execute.js
CHANGED
|
@@ -326,6 +326,7 @@ async function surface(manifest, argv, io) {
|
|
|
326
326
|
return true;
|
|
327
327
|
}
|
|
328
328
|
if (head.includes('--schema')) {
|
|
329
|
+
const { schemaSurface } = await import('./schema-surface.js');
|
|
329
330
|
io.out.write(`${await machineJson(await schemaSurface(manifest, argv), head)}\n`);
|
|
330
331
|
return true;
|
|
331
332
|
}
|
|
@@ -351,16 +352,6 @@ async function surface(manifest, argv, io) {
|
|
|
351
352
|
}
|
|
352
353
|
return false;
|
|
353
354
|
}
|
|
354
|
-
const SCHEMA_BUDGET = 48_000;
|
|
355
|
-
async function schemaSurface(manifest, argv) {
|
|
356
|
-
const { commandSchemaOf, schemaOf, summaryOf } = await import('./schema.js');
|
|
357
|
-
const { node } = manifest.resolve(beforeTerminator(argv).filter((a) => a !== '--schema' && !a.startsWith('--format=')), manifest.rootPath);
|
|
358
|
-
if (node?.run !== undefined)
|
|
359
|
-
return commandSchemaOf(node, manifest.rootPath);
|
|
360
|
-
const full = schemaOf(manifest);
|
|
361
|
-
const budget = manifest.schemaBudget ?? SCHEMA_BUDGET;
|
|
362
|
-
return JSON.stringify(full).length <= budget ? full : summaryOf(manifest, budget);
|
|
363
|
-
}
|
|
364
355
|
async function helpCommand(manifest, argv, root, io) {
|
|
365
356
|
const { node } = manifest.resolve(argv, root);
|
|
366
357
|
return renderHelp(manifest, node ?? rootNode(manifest, root), io);
|
package/dist/migrate.d.ts
CHANGED
|
@@ -8,8 +8,24 @@ import { type Graded } from './compat.js';
|
|
|
8
8
|
export declare const MAPPING: Readonly<Record<string, string>>;
|
|
9
9
|
/** The packages a project depends on that this command is about (A1). */
|
|
10
10
|
export declare const HOSTS: readonly ["commander", "yargs"];
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Every name each target exports — values and types alike — so a rewrite that moves
|
|
13
|
+
* `import { Argv } from 'yargs'` can first ask whether `burgee/yargs` has an `Argv`.
|
|
14
|
+
*
|
|
15
|
+
* A specifier-only rewrite (D-050) cannot tell a value from a type, and does not need to:
|
|
16
|
+
* a name the target does not export breaks the build either way. This is data rather than
|
|
17
|
+
* a lookup because the lookup is the TypeScript checker, which is the dependency this
|
|
18
|
+
* command exists not to have; `facade-types.test.ts` holds the table equal to what the
|
|
19
|
+
* checker sees in `dist/*.d.ts`, so it cannot drift from the façades it describes.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FACADE_EXPORTS: Readonly<Record<string, readonly string[]>>;
|
|
22
|
+
/**
|
|
23
|
+
* Why a file was left untouched. Each is a named position, never a guess (A4).
|
|
24
|
+
*
|
|
25
|
+
* `unknown-export` is a named import the target does not export — rewriting it would turn a
|
|
26
|
+
* working import into TS2305 or a `SyntaxError` at load, so the file stays as it was.
|
|
27
|
+
*/
|
|
28
|
+
export type RefusalReason = 'deep-import' | 'non-literal-specifier' | 'unknown-export';
|
|
13
29
|
export interface Refusal {
|
|
14
30
|
/** Relative to the directory being migrated, with forward slashes on every platform. */
|
|
15
31
|
file: string;
|
|
@@ -17,6 +33,23 @@ export interface Refusal {
|
|
|
17
33
|
/** The specifier as written, or `''` for a dynamic specifier that is not a literal. */
|
|
18
34
|
specifier: string;
|
|
19
35
|
reason: RefusalReason;
|
|
36
|
+
/** For `unknown-export`: the names the target does not export. */
|
|
37
|
+
names?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A type-only import left on the incumbent because the façade does not export every name in
|
|
41
|
+
* it. Types are erased, so the file's values still move to burgee and the program runs on it;
|
|
42
|
+
* the types keep compiling against the incumbent's declarations, which is why the incumbent
|
|
43
|
+
* is then not reported removable.
|
|
44
|
+
*/
|
|
45
|
+
export interface Kept {
|
|
46
|
+
file: string;
|
|
47
|
+
line: number;
|
|
48
|
+
specifier: string;
|
|
49
|
+
/** The names the façade does not export. */
|
|
50
|
+
names: string[];
|
|
51
|
+
/** The note a reader acts on. */
|
|
52
|
+
note: string;
|
|
20
53
|
}
|
|
21
54
|
/** One rewritten specifier, for the per-mapping rollup the report prints. */
|
|
22
55
|
export interface Mapped {
|
|
@@ -36,6 +69,8 @@ export interface MigrationReport {
|
|
|
36
69
|
imports: number;
|
|
37
70
|
mapped: Mapped[];
|
|
38
71
|
refused: Refusal[];
|
|
72
|
+
/** Type-only imports left pointing at the incumbent, each with the note that says why. */
|
|
73
|
+
kept: Kept[];
|
|
39
74
|
detected: Detection;
|
|
40
75
|
dependencies: {
|
|
41
76
|
before: string[];
|
|
@@ -57,6 +92,11 @@ interface Site {
|
|
|
57
92
|
start: number;
|
|
58
93
|
end: number;
|
|
59
94
|
line: number;
|
|
95
|
+
/**
|
|
96
|
+
* For `import … from` and `export … from`: the code tokens between the keyword and `from`
|
|
97
|
+
* — the import clause, which is all `bindingsOf` needs. Absent for the other three positions.
|
|
98
|
+
*/
|
|
99
|
+
clause?: string[];
|
|
60
100
|
}
|
|
61
101
|
interface Scan {
|
|
62
102
|
sites: Site[];
|
|
@@ -80,6 +120,7 @@ export interface Rewrite {
|
|
|
80
120
|
to: string;
|
|
81
121
|
}[];
|
|
82
122
|
refused: Omit<Refusal, 'file'>[];
|
|
123
|
+
kept: Omit<Kept, 'file'>[];
|
|
83
124
|
/** Whether this file references a host at all. A file that does not is not "untouched", it is unrelated. */
|
|
84
125
|
relevant: boolean;
|
|
85
126
|
}
|
|
@@ -93,6 +134,19 @@ export interface Rewrite {
|
|
|
93
134
|
* on a `Buffer` it also skips decoding 3 MB of UTF-8 that nothing was going to read.
|
|
94
135
|
*/
|
|
95
136
|
export declare function mentionsAHost(source: string | Buffer): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The names an import or re-export clause asks the module for, and whether the whole
|
|
139
|
+
* statement is type-only (`import type …`, `export type …`).
|
|
140
|
+
*
|
|
141
|
+
* Read off the tokens the scan already has: `{ a, type b, c as d }` asks for `a`, `b` and
|
|
142
|
+
* `c`; a default or namespace binding asks for no name a façade could lack (`default` is
|
|
143
|
+
* the factory, and `* as ns` is checked where it is used, which a scan cannot see). A name
|
|
144
|
+
* written as a string (`{ 'a-b' as c }`) is left unverified rather than guessed at.
|
|
145
|
+
*/
|
|
146
|
+
export declare function bindingsOf(clause: readonly string[]): {
|
|
147
|
+
typeOnly: boolean;
|
|
148
|
+
names: string[];
|
|
149
|
+
};
|
|
96
150
|
/**
|
|
97
151
|
* A2/A5 — map every host specifier in one file, or map none of them.
|
|
98
152
|
*
|
package/dist/migrate.js
CHANGED
|
@@ -11,6 +11,89 @@ export const MAPPING = {
|
|
|
11
11
|
'yargs/helpers': 'burgee/yargs/helpers',
|
|
12
12
|
};
|
|
13
13
|
export const HOSTS = ['commander', 'yargs'];
|
|
14
|
+
export const FACADE_EXPORTS = {
|
|
15
|
+
'burgee/commander': [
|
|
16
|
+
'AddHelpTextContext',
|
|
17
|
+
'AddHelpTextPosition',
|
|
18
|
+
'Argument',
|
|
19
|
+
'BurgeeParseOptions',
|
|
20
|
+
'Command',
|
|
21
|
+
'CommandOptions',
|
|
22
|
+
'CommanderError',
|
|
23
|
+
'DualOptions',
|
|
24
|
+
'ErrorOptions',
|
|
25
|
+
'ExecutableCommandOptions',
|
|
26
|
+
'Help',
|
|
27
|
+
'HelpConfiguration',
|
|
28
|
+
'HelpContext',
|
|
29
|
+
'HookEvent',
|
|
30
|
+
'HookListener',
|
|
31
|
+
'InvalidArgumentError',
|
|
32
|
+
'InvalidOptionArgumentError',
|
|
33
|
+
'Option',
|
|
34
|
+
'OptionValueSource',
|
|
35
|
+
'OptionValues',
|
|
36
|
+
'OutputConfiguration',
|
|
37
|
+
'OutputContext',
|
|
38
|
+
'ParseOptions',
|
|
39
|
+
'ParseOptionsResult',
|
|
40
|
+
'createArgument',
|
|
41
|
+
'createCommand',
|
|
42
|
+
'createOption',
|
|
43
|
+
'humanReadableArgName',
|
|
44
|
+
'program',
|
|
45
|
+
'useColor',
|
|
46
|
+
],
|
|
47
|
+
'burgee/yargs': [
|
|
48
|
+
'Arguments',
|
|
49
|
+
'ArgumentsCamelCase',
|
|
50
|
+
'Argv',
|
|
51
|
+
'AsyncCompletionFunction',
|
|
52
|
+
'BuilderArguments',
|
|
53
|
+
'BuilderCallback',
|
|
54
|
+
'Choices',
|
|
55
|
+
'CommandBuilder',
|
|
56
|
+
'CommandModule',
|
|
57
|
+
'CompletionCallback',
|
|
58
|
+
'Defined',
|
|
59
|
+
'DetailedArguments',
|
|
60
|
+
'FallbackCompletionFunction',
|
|
61
|
+
'InferredOptionType',
|
|
62
|
+
'InferredOptionTypeInner',
|
|
63
|
+
'InferredOptionTypePrimitive',
|
|
64
|
+
'InferredOptionTypes',
|
|
65
|
+
'MiddlewareFunction',
|
|
66
|
+
'Options',
|
|
67
|
+
'ParseCallback',
|
|
68
|
+
'ParsedCommand',
|
|
69
|
+
'Parser',
|
|
70
|
+
'ParserConfigurationOptions',
|
|
71
|
+
'PlatformShim',
|
|
72
|
+
'PositionalOptions',
|
|
73
|
+
'PositionalOptionsType',
|
|
74
|
+
'PromiseCompletionFunction',
|
|
75
|
+
'RequireDirectoryOptions',
|
|
76
|
+
'SyncCompletionFunction',
|
|
77
|
+
'ToArray',
|
|
78
|
+
'ToNumber',
|
|
79
|
+
'ToString',
|
|
80
|
+
'YError',
|
|
81
|
+
'YargsInstance',
|
|
82
|
+
'applyExtends',
|
|
83
|
+
'argsert',
|
|
84
|
+
'camelCase',
|
|
85
|
+
'decamelize',
|
|
86
|
+
'default',
|
|
87
|
+
'hideBin',
|
|
88
|
+
'isPromise',
|
|
89
|
+
'isYargsInstance',
|
|
90
|
+
'looksLikeNumber',
|
|
91
|
+
'objFilter',
|
|
92
|
+
'parseCommand',
|
|
93
|
+
'platformShim',
|
|
94
|
+
],
|
|
95
|
+
'burgee/yargs/helpers': ['Parser', 'applyExtends', 'hideBin'],
|
|
96
|
+
};
|
|
14
97
|
const WORD = /[A-Za-z0-9_$]/;
|
|
15
98
|
const DIVIDES = new Set([')', ']', '}']);
|
|
16
99
|
function startsRegex(previous) {
|
|
@@ -123,12 +206,25 @@ function push(tokens, text, line) {
|
|
|
123
206
|
if (tokens.pending && text !== QUOTED && text !== ')')
|
|
124
207
|
tokens.nonLiteral.push(line);
|
|
125
208
|
tokens.pending = text === '(' && (tokens.previous === 'import' || tokens.previous === 'require');
|
|
209
|
+
if (text === 'import' || text === 'export')
|
|
210
|
+
tokens.clause = [];
|
|
211
|
+
else if (text === '(' || text === '=' || tokens.previous === 'from')
|
|
212
|
+
tokens.clause = undefined;
|
|
213
|
+
else
|
|
214
|
+
tokens.clause?.push(text);
|
|
126
215
|
tokens.before = tokens.previous;
|
|
127
216
|
tokens.previous = text;
|
|
128
217
|
}
|
|
218
|
+
function siteOf(source, at, tokens) {
|
|
219
|
+
const { open, close, line } = at;
|
|
220
|
+
const site = { specifier: source.slice(open + 1, close - 1), start: open + 1, end: close - 1, line };
|
|
221
|
+
if (tokens.previous === 'from' && tokens.clause !== undefined)
|
|
222
|
+
site.clause = tokens.clause.slice(0, -1);
|
|
223
|
+
return site;
|
|
224
|
+
}
|
|
129
225
|
export function scan(source) {
|
|
130
226
|
const sites = [];
|
|
131
|
-
const tokens = { previous: '', before: '', pending: false, nonLiteral: [] };
|
|
227
|
+
const tokens = { previous: '', before: '', pending: false, nonLiteral: [], clause: undefined };
|
|
132
228
|
let line = 1;
|
|
133
229
|
let i = 0;
|
|
134
230
|
while (i < source.length) {
|
|
@@ -143,7 +239,7 @@ export function scan(source) {
|
|
|
143
239
|
const c = source.charAt(i);
|
|
144
240
|
const quoted = c === "'" || c === '"';
|
|
145
241
|
if (quoted && isSpecifier(tokens.previous, tokens.before))
|
|
146
|
-
sites.push(
|
|
242
|
+
sites.push(siteOf(source, { open: i, close: literal, line }, tokens));
|
|
147
243
|
push(tokens, quoted ? QUOTED : 'lit', line);
|
|
148
244
|
line += newlines(source, i, literal);
|
|
149
245
|
i = literal;
|
|
@@ -169,27 +265,71 @@ function isDeep(specifier) {
|
|
|
169
265
|
return false;
|
|
170
266
|
return HOSTS.some((host) => specifier.startsWith(`${host}/`));
|
|
171
267
|
}
|
|
268
|
+
export function bindingsOf(clause) {
|
|
269
|
+
const typeOnly = clause[0] === 'type' && clause.length > 1;
|
|
270
|
+
const open = clause.indexOf('{');
|
|
271
|
+
const close = clause.indexOf('}', open);
|
|
272
|
+
if (open === -1 || close === -1)
|
|
273
|
+
return { typeOnly, names: [] };
|
|
274
|
+
const names = [];
|
|
275
|
+
let element = [];
|
|
276
|
+
for (const token of [...clause.slice(open + 1, close), ',']) {
|
|
277
|
+
if (token !== ',') {
|
|
278
|
+
element.push(token);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const modifier = element[0] === 'type' && element.length > 1 && element[1] !== 'as';
|
|
282
|
+
const name = modifier ? element[1] : element[0];
|
|
283
|
+
if (name !== undefined && name !== 'default' && name !== QUOTED && WORD.test(name.charAt(0)))
|
|
284
|
+
names.push(name);
|
|
285
|
+
element = [];
|
|
286
|
+
}
|
|
287
|
+
return { typeOnly, names };
|
|
288
|
+
}
|
|
289
|
+
function missingFrom(site, to) {
|
|
290
|
+
const exported = FACADE_EXPORTS[to];
|
|
291
|
+
if (site.clause === undefined || exported === undefined)
|
|
292
|
+
return { typeOnly: false, missing: [] };
|
|
293
|
+
const { typeOnly, names } = bindingsOf(site.clause);
|
|
294
|
+
return { typeOnly, missing: names.filter((name) => !exported.includes(name)) };
|
|
295
|
+
}
|
|
172
296
|
export function rewriteSource(source) {
|
|
173
297
|
if (!mentionsAHost(source))
|
|
174
|
-
return { source, mapped: [], refused: [], relevant: false };
|
|
298
|
+
return { source, mapped: [], refused: [], kept: [], relevant: false };
|
|
175
299
|
const { sites, nonLiteral } = scan(source);
|
|
176
300
|
const hits = sites.filter((s) => MAPPING[s.specifier] !== undefined || isDeep(s.specifier));
|
|
177
301
|
if (hits.length === 0)
|
|
178
|
-
return { source, mapped: [], refused: [], relevant: false };
|
|
302
|
+
return { source, mapped: [], refused: [], kept: [], relevant: false };
|
|
303
|
+
const kept = [];
|
|
304
|
+
const unknown = [];
|
|
305
|
+
const moving = [];
|
|
306
|
+
for (const site of hits) {
|
|
307
|
+
const to = MAPPING[site.specifier];
|
|
308
|
+
if (to === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
const { typeOnly, missing } = missingFrom(site, to);
|
|
311
|
+
if (missing.length === 0)
|
|
312
|
+
moving.push(site);
|
|
313
|
+
else if (typeOnly)
|
|
314
|
+
kept.push({ line: site.line, specifier: site.specifier, names: missing, note: `${to} does not export ${missing.join(', ')}; this type-only import stays on '${site.specifier}', so keep its types installed` });
|
|
315
|
+
else
|
|
316
|
+
unknown.push({ line: site.line, specifier: site.specifier, reason: 'unknown-export', names: missing });
|
|
317
|
+
}
|
|
179
318
|
const refused = [
|
|
180
319
|
...hits.filter((s) => isDeep(s.specifier)).map((s) => ({ line: s.line, specifier: s.specifier, reason: 'deep-import' })),
|
|
181
320
|
...nonLiteral.map((line) => ({ line, specifier: '', reason: 'non-literal-specifier' })),
|
|
321
|
+
...unknown,
|
|
182
322
|
].sort((a, b) => a.line - b.line);
|
|
183
323
|
if (refused.length > 0)
|
|
184
|
-
return { source, mapped: [], refused, relevant: true };
|
|
324
|
+
return { source, mapped: [], refused, kept: [], relevant: true };
|
|
185
325
|
let out = source;
|
|
186
326
|
const mapped = [];
|
|
187
|
-
for (const site of
|
|
327
|
+
for (const site of moving.sort((a, b) => b.start - a.start)) {
|
|
188
328
|
const to = MAPPING[site.specifier];
|
|
189
329
|
out = `${out.slice(0, site.start)}${to}${out.slice(site.end)}`;
|
|
190
330
|
mapped.push({ from: site.specifier, to });
|
|
191
331
|
}
|
|
192
|
-
return { source: out, mapped: mapped.reverse(), refused: [], relevant: true };
|
|
332
|
+
return { source: out, mapped: mapped.reverse(), refused: [], kept, relevant: true };
|
|
193
333
|
}
|
|
194
334
|
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
195
335
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.next', '.output', '.cache', '.vercel']);
|
|
@@ -230,7 +370,7 @@ export class DirtyTreeError extends Error {
|
|
|
230
370
|
this.name = 'DirtyTreeError';
|
|
231
371
|
}
|
|
232
372
|
}
|
|
233
|
-
const EMPTY = { source: '', mapped: [], refused: [], relevant: false };
|
|
373
|
+
const EMPTY = { source: '', mapped: [], refused: [], kept: [], relevant: false };
|
|
234
374
|
async function migrateBatch(dir, batch, write) {
|
|
235
375
|
const sources = await Promise.all(batch.map(async (file) => await readFile(join(dir, file))));
|
|
236
376
|
const results = sources.map((bytes) => (mentionsAHost(bytes) ? rewriteSource(bytes.toString('utf8')) : EMPTY));
|
|
@@ -264,9 +404,10 @@ export async function migrate(options) {
|
|
|
264
404
|
}
|
|
265
405
|
const all = results.flatMap(({ file, result }) => result.mapped.map((m) => ({ ...m, file })));
|
|
266
406
|
const refused = results.flatMap(({ file, result }) => result.refused.map((r) => ({ file, ...r })));
|
|
267
|
-
const
|
|
407
|
+
const kept = results.flatMap(({ file, result }) => result.kept.map((k) => ({ file, ...k })));
|
|
408
|
+
const imported = [...new Set(results.flatMap(({ result }) => (result.relevant ? [...result.mapped.map((m) => m.from), ...result.kept.map((k) => k.specifier)] : [])))];
|
|
268
409
|
const declared = await declaredHosts(dir);
|
|
269
|
-
const stillUsed = new Set(refused.map((r) => r.specifier.split('/')[0] ?? ''));
|
|
410
|
+
const stillUsed = new Set([...refused, ...kept].map((r) => r.specifier.split('/')[0] ?? ''));
|
|
270
411
|
const removable = declared.filter((host) => !stillUsed.has(host));
|
|
271
412
|
const touched = [...new Set(all.map((m) => m.file))];
|
|
272
413
|
return {
|
|
@@ -274,6 +415,7 @@ export async function migrate(options) {
|
|
|
274
415
|
imports: all.length,
|
|
275
416
|
mapped: rollup(all),
|
|
276
417
|
refused,
|
|
418
|
+
kept,
|
|
277
419
|
detected: { declared, imported: [...new Set(imported.map((s) => s.split('/')[0] ?? s))].sort() },
|
|
278
420
|
dependencies: { before: declared, removable, after: declared.length - removable.length },
|
|
279
421
|
graded: gradedFor([...new Set([...declared, ...imported.map((s) => s.split('/')[0] ?? s)])].sort()),
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* N13 / D-116 — `--schema <command> --field <path>`: one field of one command's schema, so an
|
|
8
|
+
* agent under a token budget reads `options.region` instead of the whole document. The path is
|
|
9
|
+
* dotted; a step that does not exist is refused naming the steps that do.
|
|
10
|
+
*
|
|
11
|
+
* With the `--schema` surface it serves: `execute.ts` imports this module only when `--schema`
|
|
12
|
+
* is on the command line (M2), so neither weighs on a run that does not ask for it.
|
|
13
|
+
*/
|
|
14
|
+
import { type Manifest } from './manifest.js';
|
|
15
|
+
export declare function fieldOf(doc: unknown, path: string, command: string): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* `--schema` under a character budget (N13): one command's full schema when a command is
|
|
18
|
+
* named (the drilling), the whole program when it fits, a summary naming every command
|
|
19
|
+
* and how to drill when it does not.
|
|
20
|
+
*/
|
|
21
|
+
export declare function schemaSurface(manifest: Manifest, argv: string[]): Promise<unknown>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { UsageError } from './validate.js';
|
|
2
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
export function fieldOf(doc, path, command) {
|
|
4
|
+
let at = doc;
|
|
5
|
+
const walked = [];
|
|
6
|
+
for (const step of path.split('.')) {
|
|
7
|
+
if (!isRecord(at) || !(step in at)) {
|
|
8
|
+
const here = walked.length === 0 ? `"${command}"` : `"${walked.join('.')}"`;
|
|
9
|
+
const valid = isRecord(at) ? Object.keys(at).join(', ') : 'none — it is a value';
|
|
10
|
+
throw new UsageError(`${here} has no field "${step}"`, `fields here: ${valid}`);
|
|
11
|
+
}
|
|
12
|
+
at = at[step];
|
|
13
|
+
walked.push(step);
|
|
14
|
+
}
|
|
15
|
+
return at;
|
|
16
|
+
}
|
|
17
|
+
const SCHEMA_BUDGET = 48_000;
|
|
18
|
+
export async function schemaSurface(manifest, argv) {
|
|
19
|
+
const { commandSchemaOf, schemaOf, summaryOf } = await import('./schema.js');
|
|
20
|
+
const end = argv.indexOf('--');
|
|
21
|
+
const head = end === -1 ? argv : argv.slice(0, end);
|
|
22
|
+
const at = head.findIndex((a) => a === '--field' || a.startsWith('--field='));
|
|
23
|
+
const field = at === -1 ? undefined : (head[at]?.slice('--field='.length) || head[at + 1]);
|
|
24
|
+
const width = head[at] === '--field' ? 1 : 0;
|
|
25
|
+
const drop = at === -1 ? -1 : at + width;
|
|
26
|
+
const path = head.filter((a, i) => a !== '--schema' && !a.startsWith('--format=') && (i < at || i > drop));
|
|
27
|
+
const { node } = manifest.resolve(path, manifest.rootPath);
|
|
28
|
+
if (field !== undefined) {
|
|
29
|
+
if (node?.run === undefined)
|
|
30
|
+
throw new UsageError('--field drills into one command', 'name the command first: --schema <command> --field <path>');
|
|
31
|
+
return fieldOf(commandSchemaOf(node, manifest.rootPath), field, path.join(' '));
|
|
32
|
+
}
|
|
33
|
+
if (node?.run !== undefined)
|
|
34
|
+
return commandSchemaOf(node, manifest.rootPath);
|
|
35
|
+
const full = schemaOf(manifest);
|
|
36
|
+
const budget = manifest.schemaBudget ?? SCHEMA_BUDGET;
|
|
37
|
+
return JSON.stringify(full).length <= budget ? full : summaryOf(manifest, budget);
|
|
38
|
+
}
|