@clo4/cli 0.0.0-stage → 2.0.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/LICENSE +21 -0
- package/README.md +96 -2
- package/dist/index.d.ts +234 -0
- package/dist/index.js +1229 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +325 -0
- package/dist/testing.js +430 -0
- package/dist/testing.js.map +1 -0
- package/package.json +62 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
// src/util.ts
|
|
2
|
+
function isArray(o) {
|
|
3
|
+
return Array.isArray(o);
|
|
4
|
+
}
|
|
5
|
+
/** Convert the value, if any, to an array with 0 or more elements */
|
|
6
|
+
function makeArray(thing) {
|
|
7
|
+
if (thing === void 0) return [];
|
|
8
|
+
if (isArray(thing)) return thing.slice();
|
|
9
|
+
return [thing];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Like {@link makeArray}, but without copying when the input is already an
|
|
13
|
+
* array. Only safe when the caller treats the result as read-only — use it on
|
|
14
|
+
* the hot path to avoid slicing read-only spec metadata.
|
|
15
|
+
*/
|
|
16
|
+
function asArray(thing) {
|
|
17
|
+
if (thing === void 0) return [];
|
|
18
|
+
if (isArray(thing)) return thing;
|
|
19
|
+
return [thing];
|
|
20
|
+
}
|
|
21
|
+
/** Convert the value to an array with at least one element */
|
|
22
|
+
function makeArray1(thing) {
|
|
23
|
+
if (isArray(thing)) return thing.slice();
|
|
24
|
+
return [thing];
|
|
25
|
+
}
|
|
26
|
+
const nodeProcess = globalThis.process;
|
|
27
|
+
/**
|
|
28
|
+
* Write text to stdout (1) or stderr (2) with a synchronous `write` on the file
|
|
29
|
+
* descriptor.
|
|
30
|
+
*
|
|
31
|
+
* The first `console.log` in a Node process builds the stdout stream, which
|
|
32
|
+
* costs 1-3 ms. That's more than parsing takes, so all of this library's output
|
|
33
|
+
* goes through here instead. Without `node:fs` it falls back to `console`.
|
|
34
|
+
*/
|
|
35
|
+
function writeFd(fd, text) {
|
|
36
|
+
const fs = nodeProcess?.getBuiltinModule?.("node:fs");
|
|
37
|
+
if (!fs) {
|
|
38
|
+
(fd === 1 ? console.log : console.error)(text.endsWith("\n") ? text.slice(0, -1) : text);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const bytes = new TextEncoder().encode(text);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (offset < bytes.length) offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "EAGAIN") (fd === 1 ? nodeProcess?.stdout : nodeProcess?.stderr)?.write(bytes.subarray(offset));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Where this library's output goes. Tests replace `write` to capture output.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
const output = { write: writeFd };
|
|
55
|
+
/** Print a message to stdout, trimming trailing whitespace. */
|
|
56
|
+
function print(text) {
|
|
57
|
+
output.write(1, `${text.trimEnd()}\n`);
|
|
58
|
+
}
|
|
59
|
+
/** Print an error message to stderr with the shared `Error:` prefix. */
|
|
60
|
+
function printActionError(message) {
|
|
61
|
+
output.write(2, `Error: ${message}\n`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns the minimum number of arguments that satisfies the args.
|
|
65
|
+
*
|
|
66
|
+
* Accepts the raw spec value (a single arg, an array, or `undefined`) so the
|
|
67
|
+
* hot path doesn't have to copy it into an array first.
|
|
68
|
+
*/
|
|
69
|
+
function getMinArgs(args) {
|
|
70
|
+
if (args === void 0) return 0;
|
|
71
|
+
if (!isArray(args)) return args.isOptional ? 0 : 1;
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
if (args[i].isOptional) break;
|
|
75
|
+
count++;
|
|
76
|
+
}
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
79
|
+
/** Returns the maximum number of arguments that satisfies the args. */
|
|
80
|
+
function getMaxArgs(args) {
|
|
81
|
+
if (args === void 0) return 0;
|
|
82
|
+
if (!isArray(args)) return args.isVariadic ? Infinity : 1;
|
|
83
|
+
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
84
|
+
return args.length;
|
|
85
|
+
}
|
|
86
|
+
// src/errors.ts
|
|
87
|
+
const repr = JSON.stringify;
|
|
88
|
+
var ParseError = class extends Error {
|
|
89
|
+
context;
|
|
90
|
+
constructor(context, ...args) {
|
|
91
|
+
super(...args);
|
|
92
|
+
this.context = context;
|
|
93
|
+
this.name = this.constructor.name;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var UnknownOption = class extends ParseError {
|
|
97
|
+
index;
|
|
98
|
+
start;
|
|
99
|
+
end;
|
|
100
|
+
option;
|
|
101
|
+
validOptions;
|
|
102
|
+
constructor(found, context) {
|
|
103
|
+
const msg = `${repr(found.option)} looks like a flag, but is unknown in this context`;
|
|
104
|
+
super(context, msg);
|
|
105
|
+
this.option = found.option;
|
|
106
|
+
this.validOptions = found.validOptions;
|
|
107
|
+
this.index = found.index;
|
|
108
|
+
this.start = found.start;
|
|
109
|
+
this.end = found.end;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var InvalidOptionArg = class extends ParseError {
|
|
113
|
+
index;
|
|
114
|
+
start;
|
|
115
|
+
end;
|
|
116
|
+
arg;
|
|
117
|
+
option;
|
|
118
|
+
constructor(found, context) {
|
|
119
|
+
const msg = `Option ${found.option} doesn't take arguments`;
|
|
120
|
+
super(context, msg);
|
|
121
|
+
this.index = found.index;
|
|
122
|
+
this.start = found.start;
|
|
123
|
+
this.end = found.end;
|
|
124
|
+
this.option = found.option;
|
|
125
|
+
this.arg = found.arg;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var TooManyArguments = class extends ParseError {
|
|
129
|
+
index;
|
|
130
|
+
value;
|
|
131
|
+
constructor(found, context) {
|
|
132
|
+
const msg = `Too many arguments, ${repr(found.value)} was unexpected`;
|
|
133
|
+
super(context, msg);
|
|
134
|
+
this.index = found.index;
|
|
135
|
+
this.value = found.value;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var TooFewOptionArguments = class extends ParseError {
|
|
139
|
+
constructor(min, max, context) {
|
|
140
|
+
const msg = min === max ? `Option needs ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Option needs at least ${min} argument${min === 1 ? "" : "s"}` : `Option needs between ${min} and ${max} arguments`;
|
|
141
|
+
super(context, msg);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var TooFewArguments = class extends ParseError {
|
|
145
|
+
constructor(min, max, context) {
|
|
146
|
+
const msg = min === max ? `Expected ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Expected at least ${min} argument${min === 1 ? "" : "s"}` : `Expected between ${min} and ${max} arguments`;
|
|
147
|
+
super(context, msg);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var MissingRequiredOption = class extends ParseError {
|
|
151
|
+
constructor(name, context) {
|
|
152
|
+
const msg = `Option ${name} is required, but wasn't found`;
|
|
153
|
+
super(context, msg);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// src/suggest.ts
|
|
157
|
+
/**
|
|
158
|
+
* "Did you mean" suggestions for unknown commands and options.
|
|
159
|
+
*
|
|
160
|
+
* This only runs after a mistake, but it's loaded on every run, so it's kept
|
|
161
|
+
* small and does nothing until it's called.
|
|
162
|
+
*
|
|
163
|
+
* @module
|
|
164
|
+
*/
|
|
165
|
+
/** How many `-` or `+` characters `name` starts with. */
|
|
166
|
+
function dashCount(name) {
|
|
167
|
+
let i = 0;
|
|
168
|
+
while (i < name.length && (name[i] === "-" || name[i] === "+")) i++;
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
/** Whether every character of `short` appears in `long`, in order. */
|
|
172
|
+
function isAbbreviation(short, long) {
|
|
173
|
+
let i = 0;
|
|
174
|
+
for (let j = 0; j < long.length && i < short.length; j++) if (long[j] === short[i]) i++;
|
|
175
|
+
return i === short.length;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Levenshtein distance where swapping two adjacent characters also counts as
|
|
179
|
+
* one edit (optimal string alignment). Returns `limit + 1` as soon as the
|
|
180
|
+
* distance must be larger than `limit`.
|
|
181
|
+
*/
|
|
182
|
+
function editDistance(a, b, limit) {
|
|
183
|
+
let before = new Uint32Array(b.length + 1);
|
|
184
|
+
let previous = new Uint32Array(b.length + 1);
|
|
185
|
+
let current = new Uint32Array(b.length + 1);
|
|
186
|
+
for (let j = 0; j <= b.length; j++) previous[j] = j;
|
|
187
|
+
for (let i = 1; i <= a.length; i++) {
|
|
188
|
+
current[0] = i;
|
|
189
|
+
let rowMin = i;
|
|
190
|
+
for (let j = 1; j <= b.length; j++) {
|
|
191
|
+
const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
192
|
+
let d = Math.min(previous[j] + 1, current[j - 1] + 1, substitution);
|
|
193
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d = Math.min(d, before[j - 2] + 1);
|
|
194
|
+
current[j] = d;
|
|
195
|
+
if (d < rowMin) rowMin = d;
|
|
196
|
+
}
|
|
197
|
+
if (rowMin > limit) return limit + 1;
|
|
198
|
+
const recycled = before;
|
|
199
|
+
before = previous;
|
|
200
|
+
previous = current;
|
|
201
|
+
current = recycled;
|
|
202
|
+
}
|
|
203
|
+
return previous[b.length];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The choice the user most likely meant by `input`, or `undefined` if none is
|
|
207
|
+
* close enough to be worth suggesting. In order:
|
|
208
|
+
*
|
|
209
|
+
* 1. The only choice that starts with the input (`--verb` for `--verbose`).
|
|
210
|
+
* 2. The only choice that contains every character of the input in order, for
|
|
211
|
+
* inputs of 3 or more characters (`bld` for `build`).
|
|
212
|
+
* 3. The choice with the fewest edits, allowing one edit per three characters
|
|
213
|
+
* of the input. Leading dashes don't count toward the length or the edits.
|
|
214
|
+
*/
|
|
215
|
+
function closestChoice(input, choices) {
|
|
216
|
+
const inputDashes = dashCount(input);
|
|
217
|
+
const bare = input.slice(inputDashes);
|
|
218
|
+
if (bare.length >= 2) {
|
|
219
|
+
let match;
|
|
220
|
+
let count = 0;
|
|
221
|
+
for (const choice of choices) if (choice.startsWith(input)) {
|
|
222
|
+
match = choice;
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
if (count === 1) return match;
|
|
226
|
+
}
|
|
227
|
+
if (bare.length >= 3) {
|
|
228
|
+
let match;
|
|
229
|
+
let count = 0;
|
|
230
|
+
for (const choice of choices) if (isAbbreviation(bare, choice.slice(dashCount(choice)))) {
|
|
231
|
+
match = choice;
|
|
232
|
+
count++;
|
|
233
|
+
}
|
|
234
|
+
if (count === 1) return match;
|
|
235
|
+
}
|
|
236
|
+
let best;
|
|
237
|
+
let limit = Math.floor(bare.length / 3);
|
|
238
|
+
for (const choice of choices) {
|
|
239
|
+
const other = choice.slice(dashCount(choice));
|
|
240
|
+
if (Math.abs(other.length - bare.length) > limit) continue;
|
|
241
|
+
const distance = editDistance(bare, other, limit);
|
|
242
|
+
if (distance <= limit && (best === void 0 || distance < limit)) {
|
|
243
|
+
best = choice;
|
|
244
|
+
limit = distance;
|
|
245
|
+
if (distance === 0) break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return best;
|
|
249
|
+
}
|
|
250
|
+
// src/help.ts
|
|
251
|
+
function getNamed(named, name) {
|
|
252
|
+
return named.find((obj) => makeArray(obj.name).includes(name)) ?? null;
|
|
253
|
+
}
|
|
254
|
+
function getParserDirective(path, key) {
|
|
255
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
256
|
+
const command = path[index];
|
|
257
|
+
if (command?.parserDirectives && key in command.parserDirectives) return command.parserDirectives[key];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function dedent(str) {
|
|
261
|
+
if (str[0] !== "\n") return str;
|
|
262
|
+
const lines = str.split("\n").slice(1, -1);
|
|
263
|
+
const margin = lines[0].length - lines[0].trimStart().length;
|
|
264
|
+
return lines.map((line) => line.slice(margin)).join("\n");
|
|
265
|
+
}
|
|
266
|
+
/** Get the help message used by the built-in help option and command. */
|
|
267
|
+
function getHelp(path, config = {}) {
|
|
268
|
+
const { description = true, usage = true, didYouMean } = config;
|
|
269
|
+
const sections = getSections(path);
|
|
270
|
+
if (didYouMean) sections.didYouMean = closestChoice(didYouMean.input, didYouMean.choices);
|
|
271
|
+
if (!description) sections.description = void 0;
|
|
272
|
+
if (!usage) sections.usage = void 0;
|
|
273
|
+
return formatHelpSections(sections);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Action that prints usage for commands where a subcommand must be provided.
|
|
277
|
+
*/
|
|
278
|
+
const usage = ({ path, args: [command], help }) => {
|
|
279
|
+
if (command === void 0) {
|
|
280
|
+
print(help());
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (command === "") {
|
|
284
|
+
printActionError(`Expected a command, but the value was empty\n\n${help({ description: false })}`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
const subcommands = path.at(-1)?.subcommands?.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
288
|
+
if (subcommands && subcommands.length > 0) {
|
|
289
|
+
if (getParserDirective(path, "subcommandsMatchUniquePrefix")) {
|
|
290
|
+
const possible = subcommands.filter((name) => name.startsWith(command));
|
|
291
|
+
if (possible.length > 0) {
|
|
292
|
+
printActionError(`Ambiguous command '${command}', could be: ${possible.join(", ")}\n\n${help({ description: false })}`);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
printActionError(`Unknown command '${command}'\n\n${help({
|
|
297
|
+
description: false,
|
|
298
|
+
didYouMean: {
|
|
299
|
+
input: command,
|
|
300
|
+
choices: subcommands
|
|
301
|
+
}
|
|
302
|
+
})}`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
printActionError(`Unknown command '${command}'\n\n${help({ description: false })}`);
|
|
306
|
+
return 1;
|
|
307
|
+
};
|
|
308
|
+
/** A subcommand that prints help for its parent or a sibling command. */
|
|
309
|
+
const helpCommand = {
|
|
310
|
+
name: "help",
|
|
311
|
+
description: "Print a help message",
|
|
312
|
+
args: {
|
|
313
|
+
name: "command",
|
|
314
|
+
isOptional: true
|
|
315
|
+
},
|
|
316
|
+
action({ path, args: [commandName], help }) {
|
|
317
|
+
const helpRoot = path.slice(0, -1);
|
|
318
|
+
if (!commandName) {
|
|
319
|
+
print(help({ path: helpRoot }));
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
const parent = path[path.length - 2];
|
|
323
|
+
if (!parent.subcommands) {
|
|
324
|
+
printActionError(`No subcommands, try using 'help' with nothing after it\n\n${help({
|
|
325
|
+
description: false,
|
|
326
|
+
path: helpRoot
|
|
327
|
+
})}`);
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
const command = getNamed(parent.subcommands, commandName);
|
|
331
|
+
if (!command) {
|
|
332
|
+
const subcommands = parent.subcommands.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
333
|
+
if (subcommands.length > 0) {
|
|
334
|
+
printActionError(`There is no subcommand named '${commandName}'\n\n${help({
|
|
335
|
+
path: helpRoot,
|
|
336
|
+
didYouMean: {
|
|
337
|
+
input: commandName,
|
|
338
|
+
choices: subcommands
|
|
339
|
+
}
|
|
340
|
+
})}`);
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
printActionError(`There is no subcommand named '${commandName}'\n\n${help({
|
|
344
|
+
description: false,
|
|
345
|
+
path: helpRoot
|
|
346
|
+
})}`);
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
print(help({ path: [...helpRoot, command] }));
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
/** A persistent help option that prints help for the selected command. */
|
|
353
|
+
const help = {
|
|
354
|
+
name: ["-h", "--help"],
|
|
355
|
+
description: "Print a help message",
|
|
356
|
+
isPersistent: true,
|
|
357
|
+
action({ path }) {
|
|
358
|
+
print(formatHelpSections(getSections(path)));
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
function getSubcommandPathString(commands) {
|
|
363
|
+
return commands.map((command) => getLongestString(makeArray1(command.name))).join(" ");
|
|
364
|
+
}
|
|
365
|
+
function nonNullable(value) {
|
|
366
|
+
return value !== null && value !== void 0;
|
|
367
|
+
}
|
|
368
|
+
function getSections(commands) {
|
|
369
|
+
const command = commands[commands.length - 1];
|
|
370
|
+
const description = command.description;
|
|
371
|
+
const defaultDesc = "No description";
|
|
372
|
+
const subcommands = command.subcommands ? command.subcommands.filter((subcommand) => !subcommand.hidden).map((subcommand) => [makeArray1(subcommand.name).sort((a, b) => a.length - b.length).join(", "), subcommand.description ?? defaultDesc]) : [];
|
|
373
|
+
let persistentOptionObjects = [];
|
|
374
|
+
let optionObjects = [];
|
|
375
|
+
let persistentOptions = [];
|
|
376
|
+
let options = [];
|
|
377
|
+
if (commands.length === 1 && subcommands.length === 0) {
|
|
378
|
+
optionObjects = command.options ? command.options.filter((option) => !option.hidden) : [];
|
|
379
|
+
options = optionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
380
|
+
} else {
|
|
381
|
+
persistentOptionObjects = commands.map((command) => command.options).filter(nonNullable).flat().filter((option) => option.isPersistent && !option.hidden);
|
|
382
|
+
persistentOptions = persistentOptionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
383
|
+
optionObjects = (command.options ?? []).filter((option) => !option.hidden && !option.isPersistent);
|
|
384
|
+
options = optionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
usage: (() => {
|
|
388
|
+
const parts = [getSubcommandPathString(commands)];
|
|
389
|
+
const required = [...optionObjects.filter((option) => option.isRequired), ...persistentOptionObjects.filter((option) => option.isRequired)];
|
|
390
|
+
for (const option of required) parts.push(optionToString(option));
|
|
391
|
+
if (options.length || persistentOptions.length) parts.push("[flags]");
|
|
392
|
+
if (command.requiresSubcommand) parts.push("<command>");
|
|
393
|
+
else {
|
|
394
|
+
const args = makeArray(command.args);
|
|
395
|
+
if (args.length > 0) parts.push(summarizeArguments(args));
|
|
396
|
+
}
|
|
397
|
+
return parts.join(" ");
|
|
398
|
+
})(),
|
|
399
|
+
description,
|
|
400
|
+
persistentOptions,
|
|
401
|
+
options,
|
|
402
|
+
subcommands
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function formatHelpSections(sections) {
|
|
406
|
+
const parts = [];
|
|
407
|
+
const indent = " ";
|
|
408
|
+
if (sections.didYouMean) parts.push(` Did you mean '${sections.didYouMean}'?`);
|
|
409
|
+
if (sections.description) parts.push(dedent(sections.description));
|
|
410
|
+
if (sections.usage) parts.push(`Usage:\n${indent}${sections.usage}`);
|
|
411
|
+
const longestOptionName = Math.max(0, ...sections.options.map(([name]) => name.length), ...sections.persistentOptions.map(([name]) => name.length));
|
|
412
|
+
const minSpacingBetweenNameAndDesc = 2;
|
|
413
|
+
const formatOptions = (options) => {
|
|
414
|
+
const lines = [];
|
|
415
|
+
const newline = indent + " ".repeat(longestOptionName + minSpacingBetweenNameAndDesc);
|
|
416
|
+
for (const [name, desc] of options) {
|
|
417
|
+
const spaces = " ".repeat(longestOptionName - name.length + minSpacingBetweenNameAndDesc);
|
|
418
|
+
const [head, ...tail] = desc.split("\n");
|
|
419
|
+
const description = [head, ...tail.map((line) => newline + line)].join("\n");
|
|
420
|
+
lines.push(`${indent}${name}${spaces}${description}`);
|
|
421
|
+
}
|
|
422
|
+
return lines;
|
|
423
|
+
};
|
|
424
|
+
if (sections.subcommands.length > 0) {
|
|
425
|
+
const lines = ["Commands:"];
|
|
426
|
+
const longestName = Math.max(...sections.subcommands.map(([name]) => name.length));
|
|
427
|
+
const newline = indent + " ".repeat(longestName + minSpacingBetweenNameAndDesc);
|
|
428
|
+
for (const [name, desc] of sections.subcommands) {
|
|
429
|
+
const spaces = " ".repeat(longestName - name.length + minSpacingBetweenNameAndDesc);
|
|
430
|
+
const [head, ...tail] = desc.split("\n\n")[0].split("\n");
|
|
431
|
+
const description = [head, ...tail.map((line) => newline + line)].join("\n");
|
|
432
|
+
lines.push(`${indent}${name}${spaces}${description}`);
|
|
433
|
+
}
|
|
434
|
+
parts.push(lines.join("\n"));
|
|
435
|
+
}
|
|
436
|
+
if (sections.options.length > 0) {
|
|
437
|
+
const lines = ["Flags:", ...formatOptions(sections.options)];
|
|
438
|
+
parts.push(lines.join("\n"));
|
|
439
|
+
}
|
|
440
|
+
if (sections.persistentOptions.length > 0) {
|
|
441
|
+
const lines = ["Global flags:", ...formatOptions(sections.persistentOptions)];
|
|
442
|
+
parts.push(lines.join("\n"));
|
|
443
|
+
}
|
|
444
|
+
return parts.join("\n\n") + "\n";
|
|
445
|
+
}
|
|
446
|
+
function getLongestString(strings) {
|
|
447
|
+
let longest = strings[0];
|
|
448
|
+
for (let i = 1; i < strings.length; i++) {
|
|
449
|
+
const string = strings[i];
|
|
450
|
+
if (string.length > longest.length) longest = string;
|
|
451
|
+
}
|
|
452
|
+
return longest;
|
|
453
|
+
}
|
|
454
|
+
function optionToString(option) {
|
|
455
|
+
const name = makeArray1(option.name).sort((a, b) => a.length - b.length).join(", ");
|
|
456
|
+
const optionArgs = makeArray(option.args);
|
|
457
|
+
if (optionArgs.length === 0) return name;
|
|
458
|
+
if (option.requiresSeparator) {
|
|
459
|
+
const separator = option.requiresSeparator === true ? "=" : option.requiresSeparator;
|
|
460
|
+
if (optionArgs[0].isOptional) return `${name}[${separator}${optionArgs[0].name || "argument"}]`;
|
|
461
|
+
return `${name}${separator}<${optionArgs[0].name || "argument"}>`;
|
|
462
|
+
}
|
|
463
|
+
return `${name} ${summarizeArguments(optionArgs)}`;
|
|
464
|
+
}
|
|
465
|
+
function summarizeArguments(args) {
|
|
466
|
+
return args.map((arg) => argToString(arg)).join(" ");
|
|
467
|
+
}
|
|
468
|
+
function argToString(arg) {
|
|
469
|
+
const name = arg.name || "argument";
|
|
470
|
+
let lhs = "<";
|
|
471
|
+
let rhs = ">";
|
|
472
|
+
let suffix = "";
|
|
473
|
+
if (arg.isOptional) {
|
|
474
|
+
lhs = "[";
|
|
475
|
+
rhs = "]";
|
|
476
|
+
}
|
|
477
|
+
if (arg.isVariadic) suffix = "...";
|
|
478
|
+
return `${lhs}${name}${rhs}${suffix}`;
|
|
479
|
+
}
|
|
480
|
+
// src/parse.ts
|
|
481
|
+
function hasName(names, name) {
|
|
482
|
+
return typeof names === "string" ? names === name : names.includes(name);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Find the option `name` refers to. Local options win over persistent ones, and
|
|
486
|
+
* within each list a later definition wins over an earlier one.
|
|
487
|
+
*/
|
|
488
|
+
function findOption(local, persistent, name) {
|
|
489
|
+
for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
|
|
490
|
+
for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
/** Every name that's valid as an option here, for "did you mean" suggestions. */
|
|
494
|
+
function optionNames(local, persistent) {
|
|
495
|
+
const names = [];
|
|
496
|
+
for (const options of [local, persistent]) {
|
|
497
|
+
const seen = /* @__PURE__ */ new Set();
|
|
498
|
+
for (const option of options) for (const name of asArray(option.name)) if (!seen.has(name)) {
|
|
499
|
+
seen.add(name);
|
|
500
|
+
names.push(name);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return names;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Read a command line against a spec in one pass.
|
|
507
|
+
*
|
|
508
|
+
* This is the only code that decides what each input token is. With `strict`,
|
|
509
|
+
* it throws on the first problem, which is what {@link parse} needs. Without
|
|
510
|
+
* it, it never throws: it skips whatever is invalid and keeps going, so
|
|
511
|
+
* completion can still read a line that's wrong at the moment.
|
|
512
|
+
*
|
|
513
|
+
* This runs once per process, so it's written for a cold start: one loop, few
|
|
514
|
+
* helper functions and plain arrays, because every extra function is compiled
|
|
515
|
+
* the first time it runs.
|
|
516
|
+
*/
|
|
517
|
+
function scan(input, spec, strict) {
|
|
518
|
+
const path = [spec];
|
|
519
|
+
const actions = [];
|
|
520
|
+
const optionActions = [];
|
|
521
|
+
const args = [];
|
|
522
|
+
const options = /* @__PURE__ */ new Map();
|
|
523
|
+
const seenOptions = [];
|
|
524
|
+
let argSeparatorIndex = -1;
|
|
525
|
+
let local = [];
|
|
526
|
+
const persistent = [];
|
|
527
|
+
const localRequired = [];
|
|
528
|
+
const persistentRequired = [];
|
|
529
|
+
let subcommands;
|
|
530
|
+
let commandArgsMin = 0;
|
|
531
|
+
let commandArgsMax = 0;
|
|
532
|
+
let separators = ["="];
|
|
533
|
+
let posixCompliantOptions = true;
|
|
534
|
+
let subcommandsMatchUniquePrefix = false;
|
|
535
|
+
let optionsMustPrecedeArguments = false;
|
|
536
|
+
let hasFoundArg = false;
|
|
537
|
+
let openOption = null;
|
|
538
|
+
let openOptionName = "";
|
|
539
|
+
let valueCount = 0;
|
|
540
|
+
let valueMin = 0;
|
|
541
|
+
let separateValueMax = 0;
|
|
542
|
+
let values = null;
|
|
543
|
+
let valuesStart = 0;
|
|
544
|
+
let valuesMin = 0;
|
|
545
|
+
let valuesMax = 0;
|
|
546
|
+
let requiredSeparator = false;
|
|
547
|
+
const repeatCounts = /* @__PURE__ */ new Map();
|
|
548
|
+
const ctx = () => ({ path });
|
|
549
|
+
const enterCommand = (command) => {
|
|
550
|
+
local = [];
|
|
551
|
+
localRequired.length = 0;
|
|
552
|
+
if (command.options) for (const option of command.options) if (option.isPersistent) {
|
|
553
|
+
persistent.push(option);
|
|
554
|
+
if (option.isRequired) persistentRequired.push(option);
|
|
555
|
+
} else {
|
|
556
|
+
local.push(option);
|
|
557
|
+
if (option.isRequired) localRequired.push(option);
|
|
558
|
+
}
|
|
559
|
+
subcommands = command.subcommands;
|
|
560
|
+
commandArgsMin = getMinArgs(command.args);
|
|
561
|
+
commandArgsMax = getMaxArgs(command.args);
|
|
562
|
+
if (command.action) actions.push(command.action);
|
|
563
|
+
else if (command.requiresSubcommand) actions.push(usage);
|
|
564
|
+
const directives = command.parserDirectives;
|
|
565
|
+
if (directives) {
|
|
566
|
+
if (directives.optionArgSeparators) separators = asArray(directives.optionArgSeparators);
|
|
567
|
+
if (typeof directives.subcommandsMatchUniquePrefix === "boolean") subcommandsMatchUniquePrefix = directives.subcommandsMatchUniquePrefix;
|
|
568
|
+
if (typeof directives.optionsMustPrecedeArguments === "boolean") optionsMustPrecedeArguments = directives.optionsMustPrecedeArguments;
|
|
569
|
+
if (typeof directives.flagsArePosixNoncompliant === "boolean") posixCompliantOptions = !directives.flagsArePosixNoncompliant;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const endOption = () => {
|
|
573
|
+
if (strict && values && values.length - valuesStart < valuesMin) throw new TooFewOptionArguments(valuesMin, valuesMax, ctx());
|
|
574
|
+
values = null;
|
|
575
|
+
};
|
|
576
|
+
const useOption = (option, name) => {
|
|
577
|
+
endOption();
|
|
578
|
+
openOption = option;
|
|
579
|
+
openOptionName = name;
|
|
580
|
+
valueCount = 0;
|
|
581
|
+
valueMin = getMinArgs(option.args);
|
|
582
|
+
separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
|
|
583
|
+
seenOptions.push(option);
|
|
584
|
+
if (option.action) optionActions.push(option.action);
|
|
585
|
+
const names = asArray(option.name);
|
|
586
|
+
let optionValues;
|
|
587
|
+
if (option.isRepeatable) {
|
|
588
|
+
const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
|
|
589
|
+
const existing = options.get(names[0]);
|
|
590
|
+
let isSharedCount = true;
|
|
591
|
+
for (const other of names) if (options.get(other) !== existing) isSharedCount = false;
|
|
592
|
+
let uses = isSharedCount ? existing : void 0;
|
|
593
|
+
if (!uses) {
|
|
594
|
+
uses = [];
|
|
595
|
+
for (const other of names) options.set(other, uses);
|
|
596
|
+
}
|
|
597
|
+
const count = repeatCounts.get(uses) ?? uses.length;
|
|
598
|
+
if (strict && count >= maxRepeat) throw new ParseError(ctx(), "Too many repetitions");
|
|
599
|
+
repeatCounts.set(uses, count + 1);
|
|
600
|
+
if (!option.args) {
|
|
601
|
+
uses.push("");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
optionValues = uses;
|
|
605
|
+
} else {
|
|
606
|
+
optionValues = [];
|
|
607
|
+
for (const other of names) options.set(other, optionValues);
|
|
608
|
+
}
|
|
609
|
+
valuesMin = valueMin;
|
|
610
|
+
valuesMax = getMaxArgs(option.args);
|
|
611
|
+
requiredSeparator = option.requiresSeparator ?? false;
|
|
612
|
+
if (valuesMax > 0) {
|
|
613
|
+
values = optionValues;
|
|
614
|
+
valuesStart = optionValues.length;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const addValue = (index, start, end, value, separator) => {
|
|
618
|
+
valueCount += 1;
|
|
619
|
+
if (!values) {
|
|
620
|
+
if (strict) throw new InvalidOptionArg({
|
|
621
|
+
option: openOptionName,
|
|
622
|
+
arg: value,
|
|
623
|
+
index,
|
|
624
|
+
start,
|
|
625
|
+
end
|
|
626
|
+
}, ctx());
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (strict && typeof requiredSeparator === "string" && separator !== requiredSeparator) throw new ParseError(ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
|
|
630
|
+
values.push(value);
|
|
631
|
+
};
|
|
632
|
+
const addArg = (index, value) => {
|
|
633
|
+
endOption();
|
|
634
|
+
if (strict && !path[path.length - 1].requiresSubcommand && args.length >= commandArgsMax) throw new TooManyArguments({
|
|
635
|
+
value,
|
|
636
|
+
index
|
|
637
|
+
}, ctx());
|
|
638
|
+
args.push(value);
|
|
639
|
+
};
|
|
640
|
+
enterCommand(spec);
|
|
641
|
+
tokens: for (let index = 0; index < input.length; index++) {
|
|
642
|
+
const token = input[index];
|
|
643
|
+
if (token === "--") {
|
|
644
|
+
endOption();
|
|
645
|
+
if (strict && args.length >= commandArgsMax) throw new ParseError(ctx(), "Unexpected argument '--', did you mean to use an option instead?");
|
|
646
|
+
argSeparatorIndex = args.length;
|
|
647
|
+
openOption = null;
|
|
648
|
+
valueCount = valueMin = separateValueMax = 0;
|
|
649
|
+
for (index += 1; index < input.length; index++) addArg(index, input[index]);
|
|
650
|
+
break tokens;
|
|
651
|
+
}
|
|
652
|
+
const hasRoomForValue = valueCount < separateValueMax;
|
|
653
|
+
if (!hasFoundArg && !(valueCount < valueMin || hasRoomForValue && separateValueMax === Infinity) && subcommands) {
|
|
654
|
+
const allowPrefix = subcommandsMatchUniquePrefix && !hasRoomForValue && token !== "";
|
|
655
|
+
let command = null;
|
|
656
|
+
let prefixCount = 0;
|
|
657
|
+
commands: for (const candidate of subcommands) {
|
|
658
|
+
const names = candidate.name;
|
|
659
|
+
if (typeof names === "string") {
|
|
660
|
+
if (names === token) {
|
|
661
|
+
command = candidate;
|
|
662
|
+
prefixCount = -1;
|
|
663
|
+
break commands;
|
|
664
|
+
}
|
|
665
|
+
if (allowPrefix && names.startsWith(token)) {
|
|
666
|
+
command = candidate;
|
|
667
|
+
prefixCount += 1;
|
|
668
|
+
}
|
|
669
|
+
continue commands;
|
|
670
|
+
}
|
|
671
|
+
let prefixMatched = false;
|
|
672
|
+
for (const name of names) {
|
|
673
|
+
if (name === token) {
|
|
674
|
+
command = candidate;
|
|
675
|
+
prefixCount = -1;
|
|
676
|
+
break commands;
|
|
677
|
+
}
|
|
678
|
+
if (allowPrefix && !prefixMatched && name.startsWith(token)) {
|
|
679
|
+
prefixMatched = true;
|
|
680
|
+
command = candidate;
|
|
681
|
+
prefixCount += 1;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (command && (prefixCount === -1 || prefixCount === 1)) {
|
|
686
|
+
endOption();
|
|
687
|
+
openOption = null;
|
|
688
|
+
valueCount = valueMin = separateValueMax = 0;
|
|
689
|
+
path.push(command);
|
|
690
|
+
enterCommand(command);
|
|
691
|
+
continue tokens;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (!(optionsMustPrecedeArguments && hasFoundArg)) {
|
|
695
|
+
const first = token.charCodeAt(0);
|
|
696
|
+
const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
|
|
697
|
+
if (posixCompliantOptions && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
|
|
698
|
+
const leadingChar = token[0];
|
|
699
|
+
const dashOption = findOption(local, persistent, leadingChar);
|
|
700
|
+
if (dashOption?.args && !findOption(local, persistent, token.slice(0, 2))) {
|
|
701
|
+
useOption(dashOption, leadingChar);
|
|
702
|
+
addValue(index, 1, token.length, token.slice(1), "");
|
|
703
|
+
continue tokens;
|
|
704
|
+
}
|
|
705
|
+
for (let char = 1; char < token.length; char++) {
|
|
706
|
+
const optionName = leadingChar + token[char];
|
|
707
|
+
const option = findOption(local, persistent, optionName);
|
|
708
|
+
if (!option) {
|
|
709
|
+
if (char === 1 && valueCount < separateValueMax) {
|
|
710
|
+
addValue(index, 0, token.length, token, "");
|
|
711
|
+
continue tokens;
|
|
712
|
+
}
|
|
713
|
+
if (strict) throw new UnknownOption({
|
|
714
|
+
option: optionName,
|
|
715
|
+
validOptions: optionNames(local, persistent),
|
|
716
|
+
index,
|
|
717
|
+
start: char,
|
|
718
|
+
end: char + 1
|
|
719
|
+
}, ctx());
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
useOption(option, optionName);
|
|
723
|
+
if (option.args && char < token.length - 1) {
|
|
724
|
+
const remainingChars = token.slice(char + 1);
|
|
725
|
+
let sep = "";
|
|
726
|
+
for (const candidate of separators) if (remainingChars.startsWith(candidate)) {
|
|
727
|
+
sep = candidate;
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
const argStart = char + sep.length + 1;
|
|
731
|
+
addValue(index, argStart, token.length, token.slice(argStart), sep);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
continue tokens;
|
|
736
|
+
}
|
|
737
|
+
if (!posixCompliantOptions || token.length > 2 && isDoubleDash) {
|
|
738
|
+
let foundSep = "";
|
|
739
|
+
let foundSepIndex = -1;
|
|
740
|
+
for (const sep of separators) {
|
|
741
|
+
const at = token.indexOf(sep);
|
|
742
|
+
if (at === -1) continue;
|
|
743
|
+
if (foundSepIndex === -1 || at < foundSepIndex || at === foundSepIndex && sep.length > foundSep.length) {
|
|
744
|
+
foundSep = sep;
|
|
745
|
+
foundSepIndex = at;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const optionName = foundSep ? token.slice(0, foundSepIndex) : token;
|
|
749
|
+
const option = findOption(local, persistent, optionName);
|
|
750
|
+
if (option) {
|
|
751
|
+
useOption(option, optionName);
|
|
752
|
+
if (foundSep) {
|
|
753
|
+
const valueStart = foundSepIndex + foundSep.length;
|
|
754
|
+
addValue(index, valueStart, token.length, token.slice(valueStart), foundSep);
|
|
755
|
+
}
|
|
756
|
+
continue tokens;
|
|
757
|
+
}
|
|
758
|
+
if (posixCompliantOptions) {
|
|
759
|
+
if (valueCount < separateValueMax) {
|
|
760
|
+
addValue(index, 0, token.length, token, "");
|
|
761
|
+
continue tokens;
|
|
762
|
+
}
|
|
763
|
+
if (strict) throw new UnknownOption({
|
|
764
|
+
option: optionName,
|
|
765
|
+
validOptions: optionNames(local, persistent),
|
|
766
|
+
index,
|
|
767
|
+
start: 0,
|
|
768
|
+
end: optionName.length
|
|
769
|
+
}, ctx());
|
|
770
|
+
continue tokens;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (valueCount < separateValueMax) {
|
|
775
|
+
addValue(index, 0, token.length, token, "");
|
|
776
|
+
continue tokens;
|
|
777
|
+
}
|
|
778
|
+
hasFoundArg = true;
|
|
779
|
+
addArg(index, token);
|
|
780
|
+
}
|
|
781
|
+
if (strict) {
|
|
782
|
+
const hasOptionAction = optionActions.length > 0;
|
|
783
|
+
if (!hasOptionAction && args.length < commandArgsMin) throw new TooFewArguments(commandArgsMin, commandArgsMax, ctx());
|
|
784
|
+
endOption();
|
|
785
|
+
for (const option of seenOptions) {
|
|
786
|
+
if (!option.dependsOn) continue;
|
|
787
|
+
for (const name of option.dependsOn) if (!options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} requires ${name}, add it to fix this error`);
|
|
788
|
+
}
|
|
789
|
+
for (const option of seenOptions) {
|
|
790
|
+
if (!option.exclusiveOn) continue;
|
|
791
|
+
for (const name of option.exclusiveOn) if (options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} can't be used together with ${name}`);
|
|
792
|
+
}
|
|
793
|
+
if (!hasOptionAction) {
|
|
794
|
+
for (const required of [localRequired, persistentRequired]) for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, ctx());
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const open = openOption;
|
|
798
|
+
return {
|
|
799
|
+
path,
|
|
800
|
+
actions,
|
|
801
|
+
optionActions,
|
|
802
|
+
args,
|
|
803
|
+
argSeparatorIndex,
|
|
804
|
+
options,
|
|
805
|
+
seenOptions,
|
|
806
|
+
localOptions: local,
|
|
807
|
+
persistentOptions: persistent,
|
|
808
|
+
openOption: open && valueCount < separateValueMax ? {
|
|
809
|
+
option: open,
|
|
810
|
+
valueCount
|
|
811
|
+
} : null
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Parse the input tokens using the given CLI spec.
|
|
816
|
+
*
|
|
817
|
+
* This returns a `ParseResult`, which contains the array of
|
|
818
|
+
* subcommands used, the provided options, and the arguments.
|
|
819
|
+
*
|
|
820
|
+
* If parsing fails, this function throws.
|
|
821
|
+
*
|
|
822
|
+
* This function is exposed to make it easier to build a custom runner.
|
|
823
|
+
*/
|
|
824
|
+
function parse(input, spec) {
|
|
825
|
+
return scan(input, spec, true);
|
|
826
|
+
}
|
|
827
|
+
// src/completion.ts
|
|
828
|
+
const SHELLS = [
|
|
829
|
+
"bash",
|
|
830
|
+
"zsh",
|
|
831
|
+
"fish"
|
|
832
|
+
];
|
|
833
|
+
function isShell(value) {
|
|
834
|
+
return SHELLS.includes(value);
|
|
835
|
+
}
|
|
836
|
+
const firstLine = (description) => description ? description.split("\n", 1)[0].trim() : "";
|
|
837
|
+
/** Turn an arbitrary name into a safe shell identifier fragment. */
|
|
838
|
+
const sanitize = (value) => value.replace(/[^A-Za-z0-9]+/g, "_");
|
|
839
|
+
/** Single-quote a string for bash/zsh. */
|
|
840
|
+
function shQuote(value) {
|
|
841
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
842
|
+
}
|
|
843
|
+
/** Single-quote a string for fish (only `\` and `'` are special). */
|
|
844
|
+
function fishQuote(value) {
|
|
845
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Read the line typed so far with the parser in lenient mode, which skips
|
|
849
|
+
* anything invalid instead of throwing.
|
|
850
|
+
*/
|
|
851
|
+
function readLine(spec, before) {
|
|
852
|
+
const scanned = scan(before, spec, false);
|
|
853
|
+
const path = scanned.path;
|
|
854
|
+
const presentNames = /* @__PURE__ */ new Set();
|
|
855
|
+
const countByOption = /* @__PURE__ */ new Map();
|
|
856
|
+
for (const option of scanned.seenOptions) {
|
|
857
|
+
for (const name of makeArray(option.name)) presentNames.add(name);
|
|
858
|
+
countByOption.set(option, (countByOption.get(option) ?? 0) + 1);
|
|
859
|
+
}
|
|
860
|
+
const open = scanned.openOption;
|
|
861
|
+
const pending = open ? {
|
|
862
|
+
option: open.option,
|
|
863
|
+
argIndex: open.valueCount
|
|
864
|
+
} : null;
|
|
865
|
+
const availableOptions = [];
|
|
866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
867
|
+
for (const list of [scanned.localOptions, scanned.persistentOptions]) {
|
|
868
|
+
const byName = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const option of list) for (const name of makeArray(option.name)) byName.set(name, option);
|
|
870
|
+
for (const option of byName.values()) if (!seen.has(option)) {
|
|
871
|
+
seen.add(option);
|
|
872
|
+
availableOptions.push(option);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
path,
|
|
877
|
+
current: path[path.length - 1],
|
|
878
|
+
availableOptions,
|
|
879
|
+
pending,
|
|
880
|
+
positionalIndex: scanned.args.length,
|
|
881
|
+
presentNames,
|
|
882
|
+
countByOption,
|
|
883
|
+
forceArgs: scanned.argSeparatorIndex !== -1,
|
|
884
|
+
sawPositional: scanned.args.length > 0
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/** A directive's value in effect (the deepest command that sets it). */
|
|
888
|
+
function directiveOnPath(path, key) {
|
|
889
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
890
|
+
const value = path[i].parserDirectives?.[key];
|
|
891
|
+
if (typeof value === "boolean") return value;
|
|
892
|
+
}
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
/** The option-arg separators in effect (the deepest command that sets them). */
|
|
896
|
+
function separatorsOnPath(path) {
|
|
897
|
+
let separators = ["="];
|
|
898
|
+
for (const command of path) {
|
|
899
|
+
const configured = command.parserDirectives?.optionArgSeparators;
|
|
900
|
+
if (configured) separators = makeArray(configured);
|
|
901
|
+
}
|
|
902
|
+
return separators;
|
|
903
|
+
}
|
|
904
|
+
/** Enumerate filesystem entries matching `partial` for a path template. */
|
|
905
|
+
async function enumeratePaths(partial, template) {
|
|
906
|
+
const { readdirSync } = await import("node:fs");
|
|
907
|
+
const slash = partial.lastIndexOf("/");
|
|
908
|
+
const dir = slash >= 0 ? partial.slice(0, slash + 1) : "";
|
|
909
|
+
const base = slash >= 0 ? partial.slice(slash + 1) : partial;
|
|
910
|
+
const foldersOnly = template === "folders";
|
|
911
|
+
let entries;
|
|
912
|
+
try {
|
|
913
|
+
entries = readdirSync(dir || ".", { withFileTypes: true });
|
|
914
|
+
} catch {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
const candidates = [];
|
|
918
|
+
for (const entry of entries) {
|
|
919
|
+
if (!entry.name.startsWith(base)) continue;
|
|
920
|
+
const isDir = entry.isDirectory();
|
|
921
|
+
if (foldersOnly && !isDir) continue;
|
|
922
|
+
candidates.push({ value: `${dir}${entry.name}${isDir ? "/" : ""}` });
|
|
923
|
+
}
|
|
924
|
+
return candidates;
|
|
925
|
+
}
|
|
926
|
+
/** All candidate values for a single argument (suggestions/generators/template). */
|
|
927
|
+
async function argValues(arg, tokens, partial) {
|
|
928
|
+
const candidates = [];
|
|
929
|
+
for (const suggestion of arg.suggestions ?? []) candidates.push(typeof suggestion === "string" ? { value: suggestion } : {
|
|
930
|
+
value: makeArray(suggestion.name)[0],
|
|
931
|
+
description: suggestion.description
|
|
932
|
+
});
|
|
933
|
+
for (const generator of makeArray(arg.generators)) {
|
|
934
|
+
if (!generator.custom) continue;
|
|
935
|
+
const produced = await generator.custom([...tokens, partial]);
|
|
936
|
+
for (const suggestion of produced) candidates.push(typeof suggestion === "string" ? { value: suggestion } : {
|
|
937
|
+
value: makeArray(suggestion.name)[0],
|
|
938
|
+
description: suggestion.description
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
|
|
942
|
+
return candidates;
|
|
943
|
+
}
|
|
944
|
+
/** Pick the argument at an index, reusing a final variadic argument. */
|
|
945
|
+
function argAt(args, index) {
|
|
946
|
+
if (args.length === 0) return void 0;
|
|
947
|
+
if (index < args.length) return args[index];
|
|
948
|
+
const last = args[args.length - 1];
|
|
949
|
+
return last?.isVariadic ? last : void 0;
|
|
950
|
+
}
|
|
951
|
+
/** Pick the argument being completed at a positional index (honors variadic). */
|
|
952
|
+
function positionalArg(command, index) {
|
|
953
|
+
return argAt(asArray(command.args), index);
|
|
954
|
+
}
|
|
955
|
+
/** Decide which kinds of candidate the word being completed can use. */
|
|
956
|
+
function wantedCandidates(state, req) {
|
|
957
|
+
const posixNoncompliant = directiveOnPath(state.path, "flagsArePosixNoncompliant");
|
|
958
|
+
const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
|
|
959
|
+
return {
|
|
960
|
+
wantValues: !(req.gateOptions && partialIsOption),
|
|
961
|
+
wantOptions: !req.gateOptions || partialIsOption || posixNoncompliant
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Resolve completion candidates for a partial command line, reading it with the
|
|
966
|
+
* parser's `scan` for context. Dispatches between completing a option's value and
|
|
967
|
+
* completing a fresh token (subcommand / positional / option).
|
|
968
|
+
*/
|
|
969
|
+
async function completeRuntime(req) {
|
|
970
|
+
const state = readLine(req.spec, req.before);
|
|
971
|
+
const separators = separatorsOnPath(state.path);
|
|
972
|
+
const optionValue = await resolveOptionValue(state, req, separators);
|
|
973
|
+
if (optionValue) return optionValue;
|
|
974
|
+
return freshTokenCandidates(state, req, separators);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Candidates when the word being completed is a option's value — either attached
|
|
978
|
+
* (`--option=value`) or the separate token after a option that expects one. Returns
|
|
979
|
+
* null when no option value is being completed.
|
|
980
|
+
*/
|
|
981
|
+
async function resolveOptionValue(state, req, separators) {
|
|
982
|
+
const tokens = [...req.before];
|
|
983
|
+
for (const separator of separators) {
|
|
984
|
+
const at = req.partial.indexOf(separator);
|
|
985
|
+
if (at <= 0) continue;
|
|
986
|
+
const optionName = req.partial.slice(0, at);
|
|
987
|
+
const option = state.availableOptions.find((candidate) => makeArray(candidate.name).includes(optionName));
|
|
988
|
+
const arg = option && asArray(option.args)[0];
|
|
989
|
+
if (option && arg) return (await argValues(arg, tokens, req.partial.slice(at + separator.length))).map((candidate) => ({
|
|
990
|
+
value: `${optionName}${separator}${candidate.value}`,
|
|
991
|
+
description: candidate.description
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
if (state.pending) {
|
|
995
|
+
const { option, argIndex } = state.pending;
|
|
996
|
+
const args = asArray(option.args);
|
|
997
|
+
const arg = argAt(args, argIndex);
|
|
998
|
+
if (argIndex < args.length) return arg ? argValues(arg, tokens, req.partial) : [];
|
|
999
|
+
const { wantValues, wantOptions } = wantedCandidates(state, req);
|
|
1000
|
+
return [...arg && wantValues ? await argValues(arg, tokens, req.partial) : [], ...wantOptions ? allowedOptionCandidates(state, separators) : []];
|
|
1001
|
+
}
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
/** Candidates when completing a fresh token: subcommand / positional / option. */
|
|
1005
|
+
async function freshTokenCandidates(state, req, separators) {
|
|
1006
|
+
const candidates = [];
|
|
1007
|
+
const tokens = [...req.before];
|
|
1008
|
+
const { wantValues, wantOptions } = wantedCandidates(state, req);
|
|
1009
|
+
if (wantValues) {
|
|
1010
|
+
const positional = positionalArg(state.current, state.positionalIndex);
|
|
1011
|
+
if (positional) candidates.push(...await argValues(positional, tokens, req.partial));
|
|
1012
|
+
if (state.forceArgs) return candidates;
|
|
1013
|
+
if (!state.sawPositional) for (const sub of state.current.subcommands ?? []) {
|
|
1014
|
+
if (sub.hidden) continue;
|
|
1015
|
+
const description = firstLine(sub.description);
|
|
1016
|
+
for (const token of makeArray(sub.name)) candidates.push({
|
|
1017
|
+
value: token,
|
|
1018
|
+
description
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
} else if (state.forceArgs) return candidates;
|
|
1022
|
+
if (wantOptions) candidates.push(...allowedOptionCandidates(state, separators));
|
|
1023
|
+
return candidates;
|
|
1024
|
+
}
|
|
1025
|
+
/** Option candidates, unless options must precede arguments and one was seen. */
|
|
1026
|
+
function allowedOptionCandidates(state, separators) {
|
|
1027
|
+
return state.sawPositional && directiveOnPath(state.path, "optionsMustPrecedeArguments") ? [] : optionCandidates(state, separators);
|
|
1028
|
+
}
|
|
1029
|
+
/** Eligible option candidates after relationship filtering. */
|
|
1030
|
+
function optionCandidates(state, separators) {
|
|
1031
|
+
const required = [];
|
|
1032
|
+
const optional = [];
|
|
1033
|
+
for (const option of state.availableOptions) {
|
|
1034
|
+
if (option.hidden) continue;
|
|
1035
|
+
const count = state.countByOption.get(option) ?? 0;
|
|
1036
|
+
if (count > 0) {
|
|
1037
|
+
if (!option.isRepeatable) continue;
|
|
1038
|
+
if (typeof option.isRepeatable === "number" && count >= option.isRepeatable) continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (option.exclusiveOn?.some((name) => state.presentNames.has(name))) continue;
|
|
1041
|
+
if (state.availableOptions.some((other) => other !== option && makeArray(other.name).some((name) => state.presentNames.has(name)) && other.exclusiveOn?.some((name) => makeArray(option.name).includes(name)))) continue;
|
|
1042
|
+
let description = firstLine(option.description);
|
|
1043
|
+
if (option.dependsOn) {
|
|
1044
|
+
const needs = option.dependsOn.join(", ");
|
|
1045
|
+
description = description ? `${description} (requires ${needs})` : `requires ${needs}`;
|
|
1046
|
+
}
|
|
1047
|
+
if (option.isRequired) description = description ? `${description} [required]` : "[required]";
|
|
1048
|
+
const sep = option.requiresSeparator === true ? separators[0] : typeof option.requiresSeparator === "string" ? option.requiresSeparator : void 0;
|
|
1049
|
+
const bucket = option.isRequired ? required : optional;
|
|
1050
|
+
for (const token of makeArray(option.name)) bucket.push({
|
|
1051
|
+
value: sep ? `${token}${sep}` : token,
|
|
1052
|
+
description
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return [...required, ...optional];
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Render the thin shim that forwards the line to `completion complete`. The
|
|
1059
|
+
* binary is invoked as typed (so `./pack` works) and guarded so a missing
|
|
1060
|
+
* command degrades to no completion instead of an error.
|
|
1061
|
+
*/
|
|
1062
|
+
function renderRuntimeShim(spec, shell) {
|
|
1063
|
+
const id = sanitize(spec.name);
|
|
1064
|
+
const name = spec.name;
|
|
1065
|
+
if (shell === "bash") return [
|
|
1066
|
+
`_${id}_complete() {`,
|
|
1067
|
+
` local bin="\${COMP_WORDS[0]}"`,
|
|
1068
|
+
` command -v "$bin" >/dev/null 2>&1 || return`,
|
|
1069
|
+
` local cur="\${COMP_WORDS[COMP_CWORD]}"`,
|
|
1070
|
+
` local out`,
|
|
1071
|
+
` out="$("$bin" completion complete --gate-options -- "\${COMP_WORDS[@]:1:COMP_CWORD-1}" "$cur" 2>/dev/null | cut -f1)"`,
|
|
1072
|
+
` local IFS=$'\\n'`,
|
|
1073
|
+
` COMPREPLY=( $(compgen -W "$out" -- "$cur") )`,
|
|
1074
|
+
`}`,
|
|
1075
|
+
`complete -F _${id}_complete ${shQuote(name)}`
|
|
1076
|
+
].join("\n");
|
|
1077
|
+
if (shell === "zsh") return [
|
|
1078
|
+
`#compdef ${name}`,
|
|
1079
|
+
`_${id}_complete() {`,
|
|
1080
|
+
` local bin="$words[1]"`,
|
|
1081
|
+
` command -v "$bin" >/dev/null 2>&1 || return`,
|
|
1082
|
+
` local -a lines`,
|
|
1083
|
+
` lines=( "\${(@f)$("$bin" completion complete -- "\${words[2,CURRENT-1]}" "\${words[CURRENT]}" 2>/dev/null)}" )`,
|
|
1084
|
+
` local -a disp`,
|
|
1085
|
+
` local line`,
|
|
1086
|
+
` for line in $lines; do disp+=( "\${line//$'\\t'/:}" ); done`,
|
|
1087
|
+
` _describe -t values ${shQuote(name)} disp`,
|
|
1088
|
+
`}`,
|
|
1089
|
+
`_${id}_complete "$@"`
|
|
1090
|
+
].join("\n");
|
|
1091
|
+
return [
|
|
1092
|
+
`function __${id}_complete`,
|
|
1093
|
+
` set -l toks (commandline -opc)`,
|
|
1094
|
+
` set -l cur (commandline -ct)`,
|
|
1095
|
+
` set -l bin $toks[1]`,
|
|
1096
|
+
` command -q -- $bin; or test -x "$bin"; or return`,
|
|
1097
|
+
` $bin completion complete --gate-options -- $toks[2..-1] $cur 2>/dev/null`,
|
|
1098
|
+
`end`,
|
|
1099
|
+
`complete -c ${fishQuote(name)} -f -a '(__${id}_complete)'`
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
}
|
|
1102
|
+
/** Generate a completion script for `shell` from a CLI spec. */
|
|
1103
|
+
function getCompletion(spec, shell) {
|
|
1104
|
+
return renderRuntimeShim(spec, shell);
|
|
1105
|
+
}
|
|
1106
|
+
/** A subcommand that prints a shell completion script for the whole CLI. */
|
|
1107
|
+
const completion = {
|
|
1108
|
+
name: "completion",
|
|
1109
|
+
description: "Print a shell completion script",
|
|
1110
|
+
args: {
|
|
1111
|
+
name: "shell",
|
|
1112
|
+
isOptional: true,
|
|
1113
|
+
suggestions: SHELLS
|
|
1114
|
+
},
|
|
1115
|
+
subcommands: [{
|
|
1116
|
+
name: "complete",
|
|
1117
|
+
description: "Resolve completion candidates for a partial command line",
|
|
1118
|
+
hidden: true,
|
|
1119
|
+
options: [{
|
|
1120
|
+
name: "--gate-options",
|
|
1121
|
+
description: "Only suggest options after a leading dash"
|
|
1122
|
+
}],
|
|
1123
|
+
args: {
|
|
1124
|
+
name: "tokens",
|
|
1125
|
+
isVariadic: true,
|
|
1126
|
+
isOptional: true
|
|
1127
|
+
},
|
|
1128
|
+
async action({ path, args, options }) {
|
|
1129
|
+
const spec = path[0];
|
|
1130
|
+
const partial = args.length > 0 ? args[args.length - 1] : "";
|
|
1131
|
+
const lines = (await completeRuntime({
|
|
1132
|
+
spec,
|
|
1133
|
+
before: args.slice(0, -1),
|
|
1134
|
+
partial,
|
|
1135
|
+
gateOptions: options.has("--gate-options")
|
|
1136
|
+
})).map((candidate) => candidate.description ? `${candidate.value}\t${candidate.description}` : candidate.value);
|
|
1137
|
+
if (lines.length > 0) print(lines.join("\n"));
|
|
1138
|
+
return 0;
|
|
1139
|
+
}
|
|
1140
|
+
}],
|
|
1141
|
+
action({ path: [spec], args: [shell], help }) {
|
|
1142
|
+
if (shell === void 0) {
|
|
1143
|
+
print(`Supported shells: ${SHELLS.join(", ")}`);
|
|
1144
|
+
return 0;
|
|
1145
|
+
}
|
|
1146
|
+
if (!isShell(shell)) {
|
|
1147
|
+
printActionError(`Unsupported shell '${shell}'\n\n${help({
|
|
1148
|
+
description: false,
|
|
1149
|
+
didYouMean: {
|
|
1150
|
+
input: shell,
|
|
1151
|
+
choices: SHELLS
|
|
1152
|
+
}
|
|
1153
|
+
})}`);
|
|
1154
|
+
return 1;
|
|
1155
|
+
}
|
|
1156
|
+
print(getCompletion(spec, shell));
|
|
1157
|
+
return 0;
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
// src/run.ts
|
|
1161
|
+
/**
|
|
1162
|
+
* Parse argv and execute the selected action.
|
|
1163
|
+
*
|
|
1164
|
+
* The final command action runs unless a parsed option has an action. Option
|
|
1165
|
+
* actions take precedence so helpers like `--help` can run without satisfying
|
|
1166
|
+
* the selected command's required arguments.
|
|
1167
|
+
*/
|
|
1168
|
+
async function run(spec, args = defaultArgs()) {
|
|
1169
|
+
let result;
|
|
1170
|
+
try {
|
|
1171
|
+
result = parse(args, spec);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error instanceof UnknownOption) {
|
|
1174
|
+
const helpMessage = getHelp(error.context.path, {
|
|
1175
|
+
description: false,
|
|
1176
|
+
didYouMean: {
|
|
1177
|
+
input: error.option,
|
|
1178
|
+
choices: error.validOptions
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
printError(`${error.message}\n\n${helpMessage}`);
|
|
1182
|
+
} else if (error instanceof ParseError) printError(`${error.message}\n\n${getHelp(error.context.path, { description: false })}`);
|
|
1183
|
+
else printError(`${error}`);
|
|
1184
|
+
return 1;
|
|
1185
|
+
}
|
|
1186
|
+
const action = result.optionActions.at(-1) ?? result.actions.at(-1);
|
|
1187
|
+
if (!action) return 1;
|
|
1188
|
+
const options = result.options;
|
|
1189
|
+
const actionInit = {
|
|
1190
|
+
help: (helpOptions) => getHelp(helpOptions?.path ?? result.path, helpOptions),
|
|
1191
|
+
args: result.args,
|
|
1192
|
+
path: result.path,
|
|
1193
|
+
argSeparatorIndex: result.argSeparatorIndex,
|
|
1194
|
+
options: {
|
|
1195
|
+
options,
|
|
1196
|
+
has: (name) => options.has(name),
|
|
1197
|
+
get: (name) => options.get(name)?.[0],
|
|
1198
|
+
all: (name) => options.get(name),
|
|
1199
|
+
count: (name) => options.get(name)?.length ?? 0
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
let exitCode;
|
|
1203
|
+
try {
|
|
1204
|
+
const actionResult = action(actionInit);
|
|
1205
|
+
exitCode = typeof actionResult === "object" ? await actionResult : actionResult;
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
1208
|
+
return 1;
|
|
1209
|
+
}
|
|
1210
|
+
return Math.min(Math.max(exitCode ?? 0, 0), 255);
|
|
1211
|
+
}
|
|
1212
|
+
/** Print `Error:` and the given values to stderr, separated by spaces. */
|
|
1213
|
+
function printError(...strings) {
|
|
1214
|
+
output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
|
|
1215
|
+
}
|
|
1216
|
+
function defaultArgs() {
|
|
1217
|
+
return globalThis.process?.argv?.slice(2) ?? [];
|
|
1218
|
+
}
|
|
1219
|
+
// src/index.ts
|
|
1220
|
+
var src_default = {
|
|
1221
|
+
completion,
|
|
1222
|
+
help,
|
|
1223
|
+
helpCommand,
|
|
1224
|
+
printError,
|
|
1225
|
+
run
|
|
1226
|
+
};
|
|
1227
|
+
export { completion, src_default as default, help, helpCommand, printError, run };
|
|
1228
|
+
|
|
1229
|
+
//# sourceMappingURL=index.js.map
|