burgee 0.7.1 → 0.9.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 +1 -0
- package/dist/check.d.ts +22 -0
- package/dist/check.js +35 -0
- package/dist/cli.d.ts +31 -3
- package/dist/cli.js +36 -7
- package/dist/commander/argument.js +0 -3
- package/dist/commander/command.d.ts +7 -3
- package/dist/commander/command.js +54 -187
- package/dist/commander/error.js +0 -2
- package/dist/commander/help.js +0 -17
- package/dist/commander/option.js +0 -14
- package/dist/compat.d.ts +30 -0
- package/dist/compat.js +4 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +2 -0
- package/dist/definition.d.ts +24 -6
- package/dist/definition.js +18 -14
- package/dist/execute.d.ts +1 -0
- package/dist/execute.js +45 -27
- package/dist/exit-code.d.ts +17 -1
- package/dist/exit-code.js +1 -0
- package/dist/help-entry.d.ts +2 -0
- package/dist/help-entry.js +1 -0
- package/dist/help.d.ts +12 -0
- package/dist/help.js +6 -0
- package/dist/index.d.ts +32 -7
- package/dist/index.js +1 -7
- package/dist/mcp-entry.d.ts +2 -0
- package/dist/mcp-entry.js +1 -0
- package/dist/mcp.d.ts +33 -13
- package/dist/mcp.js +4 -3
- package/dist/meow/parse.d.ts +21 -0
- package/dist/meow/parse.js +43 -0
- package/dist/meow/present.d.ts +35 -0
- package/dist/meow/present.js +58 -0
- package/dist/meow/types.d.ts +47 -0
- package/dist/meow/types.js +3 -0
- package/dist/meow/validate.d.ts +35 -0
- package/dist/meow/validate.js +144 -0
- package/dist/meow.d.ts +6 -0
- package/dist/meow.js +146 -0
- package/dist/migrate.d.ts +142 -0
- package/dist/migrate.js +284 -0
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +3 -0
- package/dist/schema-entry.d.ts +3 -0
- package/dist/schema-entry.js +2 -0
- package/dist/schema.json +1 -1
- package/dist/testing-helpers.js +3 -1
- package/dist/validate.d.ts +15 -0
- package/dist/validate.js +10 -0
- package/dist/yargs/burgee.js +0 -14
- package/dist/yargs/cliui.js +0 -53
- package/dist/yargs/command.js +0 -7
- package/dist/yargs/completion.js +0 -5
- package/dist/yargs/factory.js +7 -57
- package/dist/yargs/middleware.js +0 -5
- package/dist/yargs/shim.js +0 -20
- package/dist/yargs/usage.js +0 -8
- package/dist/yargs/utils.js +0 -11
- package/dist/yargs/validation.js +0 -6
- package/dist/yargs/y18n.js +0 -6
- package/package.json +32 -7
package/dist/meow.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ExitCode } from './exit-code.js';
|
|
2
|
+
import { aliasMap, splitAtCommand, typeofDefault } from './meow/parse.js';
|
|
3
|
+
import { buildHelp, normalizePackage, readPackageUp, setProcessTitle } from './meow/present.js';
|
|
4
|
+
import { own } from './meow/types.js';
|
|
5
|
+
import { checkChoices, checkInput, checkRequired, checkSetOnce, checkUnknown, requireImportMeta, validateCommands, validateFlags } from './meow/validate.js';
|
|
6
|
+
import { host } from './runtime.js';
|
|
7
|
+
import parser, { camelCase } from './yargs-parser.js';
|
|
8
|
+
function meow(helpText, options = {}) {
|
|
9
|
+
const opts = typeof helpText === 'string' ? { help: helpText, ...options } : helpText;
|
|
10
|
+
if (typeof helpText !== 'string' && Object.keys(options).length > 0)
|
|
11
|
+
Object.assign(opts, options);
|
|
12
|
+
if (opts.input !== undefined && typeof opts.input !== 'string' && !Array.isArray(opts.input) && typeof opts.input !== 'object') {
|
|
13
|
+
throw new TypeError('The `input` option must be a string or an object.');
|
|
14
|
+
}
|
|
15
|
+
const flagSpecs = (opts.flags ?? {});
|
|
16
|
+
if (typeof flagSpecs !== 'object' || flagSpecs === null || Array.isArray(flagSpecs)) {
|
|
17
|
+
throw new TypeError('The `flags` option must be an object.');
|
|
18
|
+
}
|
|
19
|
+
if (typeof opts.input === 'object' && opts.input !== null) {
|
|
20
|
+
const required = opts.input.isRequired;
|
|
21
|
+
if (required !== undefined && typeof required !== 'boolean' && typeof required !== 'function') {
|
|
22
|
+
throw new TypeError('The `input.isRequired` option must be a boolean or a function.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
validateFlags(flagSpecs);
|
|
26
|
+
if (opts.pkg === undefined)
|
|
27
|
+
requireImportMeta(opts.importMeta);
|
|
28
|
+
const pkg = normalizePackage(opts.pkg ?? readPackageUp(opts.importMeta));
|
|
29
|
+
validateCommands(opts.commands);
|
|
30
|
+
const rawArgv = [...(opts.argv ?? host.argv.slice(2))];
|
|
31
|
+
const booleans = [];
|
|
32
|
+
const strings = [];
|
|
33
|
+
const numbers = [];
|
|
34
|
+
const defaults = {};
|
|
35
|
+
const arrays = [];
|
|
36
|
+
for (const [name, spec] of Object.entries(flagSpecs)) {
|
|
37
|
+
if (name === '--')
|
|
38
|
+
continue;
|
|
39
|
+
const type = spec.type ?? (spec.default === undefined ? undefined : typeofDefault(spec.default));
|
|
40
|
+
if (type === 'boolean')
|
|
41
|
+
booleans.push(name);
|
|
42
|
+
else if (type === 'number')
|
|
43
|
+
numbers.push(name);
|
|
44
|
+
else if (type === 'string')
|
|
45
|
+
strings.push(name);
|
|
46
|
+
if (spec.isMultiple === true)
|
|
47
|
+
arrays.push(name);
|
|
48
|
+
if (spec.default !== undefined)
|
|
49
|
+
own(defaults, name, spec.default);
|
|
50
|
+
else if (type === 'boolean' && 'booleanDefault' in opts) {
|
|
51
|
+
if (opts.booleanDefault !== undefined)
|
|
52
|
+
own(defaults, name, opts.booleanDefault);
|
|
53
|
+
}
|
|
54
|
+
else if (type === 'boolean')
|
|
55
|
+
own(defaults, name, false);
|
|
56
|
+
if (spec.isMultiple === true && spec.default === undefined) {
|
|
57
|
+
own(defaults, name, type === 'boolean' && !('booleanDefault' in opts) ? [false] : []);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const parserOptions = {
|
|
61
|
+
alias: aliasMap(flagSpecs),
|
|
62
|
+
array: arrays,
|
|
63
|
+
boolean: booleans,
|
|
64
|
+
string: strings,
|
|
65
|
+
number: numbers,
|
|
66
|
+
default: defaults,
|
|
67
|
+
configuration: {
|
|
68
|
+
'camel-case-expansion': true,
|
|
69
|
+
'greedy-arrays': false,
|
|
70
|
+
'parse-numbers': opts.inferType === true,
|
|
71
|
+
'parse-positional-numbers': opts.inferType === true,
|
|
72
|
+
'populate--': flagSpecs['--'] !== undefined,
|
|
73
|
+
'strip-aliased': false,
|
|
74
|
+
'strip-dashed': true,
|
|
75
|
+
'unknown-options-as-args': false,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const split = opts.commands === undefined ? undefined : splitAtCommand(rawArgv, parserOptions, opts);
|
|
79
|
+
const argv = split?.parent ?? rawArgv;
|
|
80
|
+
const parsed = parser.detailed(argv, parserOptions);
|
|
81
|
+
const { _: positional, ...rest } = parsed.argv;
|
|
82
|
+
const inputType = typeof opts.input === 'object' && opts.input !== null ? opts.input.type : opts.input;
|
|
83
|
+
const coerce = (v) => {
|
|
84
|
+
if (inputType === 'number')
|
|
85
|
+
return Number(v);
|
|
86
|
+
if (inputType === 'string')
|
|
87
|
+
return String(v);
|
|
88
|
+
return opts.inferType === true ? v : String(v);
|
|
89
|
+
};
|
|
90
|
+
const input = (split === undefined ? positional.map(coerce) : split.input);
|
|
91
|
+
const command = split?.command;
|
|
92
|
+
const dropped = new Set();
|
|
93
|
+
for (const [name, spec] of Object.entries(flagSpecs)) {
|
|
94
|
+
if (typeof spec.shortFlag === 'string')
|
|
95
|
+
dropped.add(spec.shortFlag);
|
|
96
|
+
if (typeof spec.alias === 'string')
|
|
97
|
+
dropped.add(spec.alias);
|
|
98
|
+
for (const extra of spec.aliases ?? [])
|
|
99
|
+
dropped.add(extra);
|
|
100
|
+
void name;
|
|
101
|
+
}
|
|
102
|
+
const flags = {};
|
|
103
|
+
const unnormalizedFlags = { ...rest };
|
|
104
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
105
|
+
if (key === '--' || dropped.has(key))
|
|
106
|
+
continue;
|
|
107
|
+
Object.defineProperty(flags, camelCase(key), { value, writable: true, enumerable: true, configurable: true });
|
|
108
|
+
}
|
|
109
|
+
if (flagSpecs['--'] !== undefined)
|
|
110
|
+
flags['--'] = rest['--'] ?? [];
|
|
111
|
+
const help = buildHelp(opts, pkg);
|
|
112
|
+
const declared = opts.version === undefined ? undefined : String(opts.version);
|
|
113
|
+
const fromPkg = typeof pkg['version'] === 'string' && pkg['version'] !== '' ? pkg['version'] : undefined;
|
|
114
|
+
const version = declared ?? fromPkg ?? 'No version found';
|
|
115
|
+
const showHelp = (code = ExitCode.USAGE) => {
|
|
116
|
+
host.stdout.write(help === '' ? '\n' : `${help}\n`);
|
|
117
|
+
return host.exit(code);
|
|
118
|
+
};
|
|
119
|
+
const showVersion = () => {
|
|
120
|
+
host.stdout.write(`${version}\n`);
|
|
121
|
+
host.exit(ExitCode.OK);
|
|
122
|
+
};
|
|
123
|
+
setProcessTitle(pkg);
|
|
124
|
+
const result = { input, flags, unnormalizedFlags, pkg, help, version, showHelp, showVersion };
|
|
125
|
+
if (opts.commands !== undefined && command !== undefined)
|
|
126
|
+
result.command = command;
|
|
127
|
+
if (split?.unknownCommand !== undefined) {
|
|
128
|
+
host.stderr.write(`Unknown command: ${split.unknownCommand}\nAvailable commands: ${opts.commands?.join(', ') ?? ''}\n${help}\n`);
|
|
129
|
+
host.exit(ExitCode.USAGE);
|
|
130
|
+
}
|
|
131
|
+
if (input.length === 0) {
|
|
132
|
+
if (opts.autoHelp !== false && flagSpecs['help'] === undefined && flags['help'] === true)
|
|
133
|
+
showHelp(ExitCode.OK);
|
|
134
|
+
if (opts.autoVersion !== false && flagSpecs['version'] === undefined && flags['version'] === true)
|
|
135
|
+
showVersion();
|
|
136
|
+
}
|
|
137
|
+
checkSetOnce(flagSpecs, rest);
|
|
138
|
+
checkInput(opts, input, flags);
|
|
139
|
+
checkChoices(flagSpecs, flags);
|
|
140
|
+
checkRequired(flagSpecs, flags, input);
|
|
141
|
+
if (opts.allowUnknownFlags === false)
|
|
142
|
+
checkUnknown(flagSpecs, rest, argv, opts);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
const meowFacade = meow;
|
|
146
|
+
export default meowFacade;
|
|
@@ -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 {};
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ambientRuntime, run } from 'bellpull';
|
|
5
|
+
import { GRADED } from './compat.js';
|
|
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'];
|
|
14
|
+
const WORD = /[A-Za-z0-9_$]/;
|
|
15
|
+
const DIVIDES = new Set([')', ']', '}']);
|
|
16
|
+
function startsRegex(previous) {
|
|
17
|
+
if (previous === '')
|
|
18
|
+
return true;
|
|
19
|
+
if (DIVIDES.has(previous))
|
|
20
|
+
return false;
|
|
21
|
+
return !WORD.test(previous.charAt(previous.length - 1));
|
|
22
|
+
}
|
|
23
|
+
function endOfString(source, open) {
|
|
24
|
+
const quote = source.charAt(open);
|
|
25
|
+
for (let i = open + 1; i < source.length; i += 1) {
|
|
26
|
+
const c = source.charAt(i);
|
|
27
|
+
if (c === '\\') {
|
|
28
|
+
i += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === quote || c === '\n')
|
|
32
|
+
return i + 1;
|
|
33
|
+
}
|
|
34
|
+
return source.length;
|
|
35
|
+
}
|
|
36
|
+
function endOfTemplate(source, open) {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
for (let i = open + 1; i < source.length; i += 1) {
|
|
39
|
+
const c = source.charAt(i);
|
|
40
|
+
if (c === '\\') {
|
|
41
|
+
i += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === '$' && source.charAt(i + 1) === '{') {
|
|
45
|
+
depth += 1;
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === '}' && depth > 0) {
|
|
50
|
+
depth -= 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (c === '`' && depth === 0)
|
|
54
|
+
return i + 1;
|
|
55
|
+
}
|
|
56
|
+
return source.length;
|
|
57
|
+
}
|
|
58
|
+
function endOfRegex(source, open) {
|
|
59
|
+
let inClass = false;
|
|
60
|
+
for (let i = open + 1; i < source.length; i += 1) {
|
|
61
|
+
const c = source.charAt(i);
|
|
62
|
+
if (c === '\\') {
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (c === '[' || c === ']') {
|
|
67
|
+
inClass = c === '[';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === '\n')
|
|
71
|
+
return i;
|
|
72
|
+
if (c === '/' && !inClass)
|
|
73
|
+
return i + 1;
|
|
74
|
+
}
|
|
75
|
+
return source.length;
|
|
76
|
+
}
|
|
77
|
+
function newlines(source, from, to) {
|
|
78
|
+
let count = 0;
|
|
79
|
+
for (let i = from; i < to; i += 1)
|
|
80
|
+
if (source.charAt(i) === '\n')
|
|
81
|
+
count += 1;
|
|
82
|
+
return count;
|
|
83
|
+
}
|
|
84
|
+
function isSpecifier(previous, before) {
|
|
85
|
+
if (previous === 'from' || previous === 'import')
|
|
86
|
+
return true;
|
|
87
|
+
return previous === '(' && (before === 'import' || before === 'require');
|
|
88
|
+
}
|
|
89
|
+
function endOfTrivia(source, at) {
|
|
90
|
+
const c = source.charAt(at);
|
|
91
|
+
if (c === ' ' || c === '\t' || c === '\r' || c === '\n' || c === ';')
|
|
92
|
+
return at + 1;
|
|
93
|
+
if (c !== '/')
|
|
94
|
+
return at;
|
|
95
|
+
const next = source.charAt(at + 1);
|
|
96
|
+
if (next === '/') {
|
|
97
|
+
const end = source.indexOf('\n', at);
|
|
98
|
+
return end === -1 ? source.length : end;
|
|
99
|
+
}
|
|
100
|
+
if (next !== '*')
|
|
101
|
+
return at;
|
|
102
|
+
const end = source.indexOf('*/', at + 2);
|
|
103
|
+
return end === -1 ? source.length : end + 2;
|
|
104
|
+
}
|
|
105
|
+
function endOfLiteral(source, at, previous) {
|
|
106
|
+
const c = source.charAt(at);
|
|
107
|
+
if (c === "'" || c === '"')
|
|
108
|
+
return endOfString(source, at);
|
|
109
|
+
if (c === '`')
|
|
110
|
+
return endOfTemplate(source, at);
|
|
111
|
+
if (c === '/' && startsRegex(previous))
|
|
112
|
+
return endOfRegex(source, at);
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
function endOfWord(source, at) {
|
|
116
|
+
let i = at;
|
|
117
|
+
while (i < source.length && WORD.test(source.charAt(i)))
|
|
118
|
+
i += 1;
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
const QUOTED = 'str';
|
|
122
|
+
function push(tokens, text, line) {
|
|
123
|
+
if (tokens.pending && text !== QUOTED && text !== ')')
|
|
124
|
+
tokens.nonLiteral.push(line);
|
|
125
|
+
tokens.pending = text === '(' && (tokens.previous === 'import' || tokens.previous === 'require');
|
|
126
|
+
tokens.before = tokens.previous;
|
|
127
|
+
tokens.previous = text;
|
|
128
|
+
}
|
|
129
|
+
export function scan(source) {
|
|
130
|
+
const sites = [];
|
|
131
|
+
const tokens = { previous: '', before: '', pending: false, nonLiteral: [] };
|
|
132
|
+
let line = 1;
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < source.length) {
|
|
135
|
+
const trivia = endOfTrivia(source, i);
|
|
136
|
+
if (trivia !== i) {
|
|
137
|
+
line += newlines(source, i, trivia);
|
|
138
|
+
i = trivia;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const literal = endOfLiteral(source, i, tokens.previous);
|
|
142
|
+
if (literal !== -1) {
|
|
143
|
+
const c = source.charAt(i);
|
|
144
|
+
const quoted = c === "'" || c === '"';
|
|
145
|
+
if (quoted && isSpecifier(tokens.previous, tokens.before))
|
|
146
|
+
sites.push({ specifier: source.slice(i + 1, literal - 1), start: i + 1, end: literal - 1, line });
|
|
147
|
+
push(tokens, quoted ? QUOTED : 'lit', line);
|
|
148
|
+
line += newlines(source, i, literal);
|
|
149
|
+
i = literal;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const word = endOfWord(source, i);
|
|
153
|
+
if (word !== i) {
|
|
154
|
+
push(tokens, source.slice(i, word), line);
|
|
155
|
+
i = word;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
push(tokens, source.charAt(i), line);
|
|
159
|
+
i += 1;
|
|
160
|
+
}
|
|
161
|
+
return { sites, nonLiteral: tokens.nonLiteral };
|
|
162
|
+
}
|
|
163
|
+
const NEEDLE_BYTES = HOSTS.map((host) => Buffer.from(host));
|
|
164
|
+
export function mentionsAHost(source) {
|
|
165
|
+
return typeof source === 'string' ? HOSTS.some((host) => source.includes(host)) : NEEDLE_BYTES.some((bytes) => source.includes(bytes));
|
|
166
|
+
}
|
|
167
|
+
function isDeep(specifier) {
|
|
168
|
+
if (MAPPING[specifier] !== undefined)
|
|
169
|
+
return false;
|
|
170
|
+
return HOSTS.some((host) => specifier.startsWith(`${host}/`));
|
|
171
|
+
}
|
|
172
|
+
export function rewriteSource(source) {
|
|
173
|
+
if (!mentionsAHost(source))
|
|
174
|
+
return { source, mapped: [], refused: [], relevant: false };
|
|
175
|
+
const { sites, nonLiteral } = scan(source);
|
|
176
|
+
const hits = sites.filter((s) => MAPPING[s.specifier] !== undefined || isDeep(s.specifier));
|
|
177
|
+
if (hits.length === 0)
|
|
178
|
+
return { source, mapped: [], refused: [], relevant: false };
|
|
179
|
+
const refused = [
|
|
180
|
+
...hits.filter((s) => isDeep(s.specifier)).map((s) => ({ line: s.line, specifier: s.specifier, reason: 'deep-import' })),
|
|
181
|
+
...nonLiteral.map((line) => ({ line, specifier: '', reason: 'non-literal-specifier' })),
|
|
182
|
+
].sort((a, b) => a.line - b.line);
|
|
183
|
+
if (refused.length > 0)
|
|
184
|
+
return { source, mapped: [], refused, relevant: true };
|
|
185
|
+
let out = source;
|
|
186
|
+
const mapped = [];
|
|
187
|
+
for (const site of [...hits].sort((a, b) => b.start - a.start)) {
|
|
188
|
+
const to = MAPPING[site.specifier];
|
|
189
|
+
out = `${out.slice(0, site.start)}${to}${out.slice(site.end)}`;
|
|
190
|
+
mapped.push({ from: site.specifier, to });
|
|
191
|
+
}
|
|
192
|
+
return { source: out, mapped: mapped.reverse(), refused: [], relevant: true };
|
|
193
|
+
}
|
|
194
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
195
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.next', '.output', '.cache', '.vercel']);
|
|
196
|
+
export function sourceFiles(dir, at = '', found = []) {
|
|
197
|
+
for (const entry of readdirSync(join(dir, at), { withFileTypes: true })) {
|
|
198
|
+
const path = at === '' ? entry.name : `${at}/${entry.name}`;
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
if (!SKIP.has(entry.name))
|
|
201
|
+
sourceFiles(dir, path, found);
|
|
202
|
+
}
|
|
203
|
+
else if (SOURCE_EXTENSIONS.has(entry.name.slice(entry.name.lastIndexOf('.'))))
|
|
204
|
+
found.push(path);
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
async function declaredHosts(dir) {
|
|
209
|
+
try {
|
|
210
|
+
const raw = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
|
|
211
|
+
const declared = { ...raw.dependencies, ...raw.devDependencies };
|
|
212
|
+
return HOSTS.filter((host) => declared[host] !== undefined);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export async function workingTree(dir) {
|
|
219
|
+
const result = await run('git', ['-C', dir, 'status', '--porcelain'], { runtime: ambientRuntime(), stdio: 'pipe' });
|
|
220
|
+
if (!result.ok)
|
|
221
|
+
return undefined;
|
|
222
|
+
return result.stdout.split('\n').filter((line) => line !== '');
|
|
223
|
+
}
|
|
224
|
+
export class DirtyTreeError extends Error {
|
|
225
|
+
entries;
|
|
226
|
+
fix = 'commit or stash your changes, or pass --force';
|
|
227
|
+
constructor(entries) {
|
|
228
|
+
super(`the git tree has ${entries.length} uncommitted change${entries.length === 1 ? '' : 's'}`);
|
|
229
|
+
this.entries = entries;
|
|
230
|
+
this.name = 'DirtyTreeError';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const EMPTY = { source: '', mapped: [], refused: [], relevant: false };
|
|
234
|
+
async function migrateBatch(dir, batch, write) {
|
|
235
|
+
const sources = await Promise.all(batch.map(async (file) => await readFile(join(dir, file))));
|
|
236
|
+
const results = sources.map((bytes) => (mentionsAHost(bytes) ? rewriteSource(bytes.toString('utf8')) : EMPTY));
|
|
237
|
+
if (write)
|
|
238
|
+
await Promise.all(results.map(async (r, i) => (r.mapped.length === 0 ? undefined : await writeFile(join(dir, batch[i]), r.source))));
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
function rollup(all) {
|
|
242
|
+
return Object.entries(MAPPING)
|
|
243
|
+
.map(([from, to]) => {
|
|
244
|
+
const hits = all.filter((m) => m.from === from);
|
|
245
|
+
return { from, to, imports: hits.length, files: new Set(hits.map((m) => m.file)).size };
|
|
246
|
+
})
|
|
247
|
+
.filter((row) => row.imports > 0);
|
|
248
|
+
}
|
|
249
|
+
function gradedFor(hosts) {
|
|
250
|
+
return hosts.filter((host) => GRADED[host] !== undefined).map((host) => ({ host, ...GRADED[host] }));
|
|
251
|
+
}
|
|
252
|
+
const BATCH = 256;
|
|
253
|
+
export async function migrate(options) {
|
|
254
|
+
const { dir, dryRun = false, force = false } = options;
|
|
255
|
+
const entries = await (options.status ?? workingTree)(dir);
|
|
256
|
+
if (!dryRun && !force && entries !== undefined && entries.length > 0)
|
|
257
|
+
throw new DirtyTreeError(entries);
|
|
258
|
+
const files = sourceFiles(dir);
|
|
259
|
+
const results = [];
|
|
260
|
+
for (let i = 0; i < files.length; i += BATCH) {
|
|
261
|
+
const batch = files.slice(i, i + BATCH);
|
|
262
|
+
const done = await migrateBatch(dir, batch, !dryRun);
|
|
263
|
+
results.push(...done.map((result, k) => ({ file: batch[k], result })));
|
|
264
|
+
}
|
|
265
|
+
const all = results.flatMap(({ file, result }) => result.mapped.map((m) => ({ ...m, file })));
|
|
266
|
+
const refused = results.flatMap(({ file, result }) => result.refused.map((r) => ({ file, ...r })));
|
|
267
|
+
const imported = [...new Set(results.flatMap(({ result }) => (result.relevant ? result.mapped.map((m) => m.from) : [])))];
|
|
268
|
+
const declared = await declaredHosts(dir);
|
|
269
|
+
const stillUsed = new Set(refused.map((r) => r.specifier.split('/')[0] ?? ''));
|
|
270
|
+
const removable = declared.filter((host) => !stillUsed.has(host));
|
|
271
|
+
const touched = [...new Set(all.map((m) => m.file))];
|
|
272
|
+
return {
|
|
273
|
+
files: touched.length,
|
|
274
|
+
imports: all.length,
|
|
275
|
+
mapped: rollup(all),
|
|
276
|
+
refused,
|
|
277
|
+
detected: { declared, imported: [...new Set(imported.map((s) => s.split('/')[0] ?? s))].sort() },
|
|
278
|
+
dependencies: { before: declared, removable, after: declared.length - removable.length },
|
|
279
|
+
graded: gradedFor([...new Set([...declared, ...imported.map((s) => s.split('/')[0] ?? s)])].sort()),
|
|
280
|
+
dryRun,
|
|
281
|
+
changed: !dryRun && touched.length > 0,
|
|
282
|
+
exitCode: refused.length > 0 ? ExitCode.RUNTIME : ExitCode.OK,
|
|
283
|
+
};
|
|
284
|
+
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface Plugin {
|
|
|
19
19
|
};
|
|
20
20
|
enforce?: 'pre' | 'post';
|
|
21
21
|
}
|
|
22
|
-
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT';
|
|
22
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
|
|
23
23
|
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
24
24
|
export declare class PluginError extends Error {
|
|
25
25
|
readonly code: PluginErrorCode;
|
package/dist/plugin.js
CHANGED
|
@@ -68,7 +68,7 @@ function checkCommands(commands, name, taken) {
|
|
|
68
68
|
}
|
|
69
69
|
seen.add(path);
|
|
70
70
|
try {
|
|
71
|
-
checkCommand(path,
|
|
71
|
+
checkCommand(path, node);
|
|
72
72
|
}
|
|
73
73
|
catch (error) {
|
|
74
74
|
throw schema(`${at}: ${error.message}`, 'a plugin command is declared exactly as a first-party one');
|
package/dist/runtime.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export declare const host: {
|
|
|
66
66
|
readonly stdin: NodeJS.ReadStream;
|
|
67
67
|
readonly stdout: NodeJS.WriteStream;
|
|
68
68
|
readonly stderr: NodeJS.WriteStream;
|
|
69
|
+
/** What `ps` shows. meow renames the process after the binary it is; nothing else sets it. */
|
|
70
|
+
setTitle(value: string): void;
|
|
69
71
|
/**
|
|
70
72
|
* The terminal's width, or undefined when there is no terminal to ask. Guarded on
|
|
71
73
|
* `process` itself because cliui's upstream is guarded there: `getWindowWidth` is reached
|
package/dist/runtime.js
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** `burgee/schema` — the schema surface and the manifest it reads, by itself. See `index.ts`. */
|
|
2
|
+
export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
|
|
3
|
+
export { Manifest } from './manifest.js';
|