@clo4/cli 1.0.0 → 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/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- // \0rolldown/runtime.js
2
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
3
1
  // src/util.ts
4
2
  function isArray(o) {
5
3
  return Array.isArray(o);
@@ -25,247 +23,65 @@ function makeArray1(thing) {
25
23
  if (isArray(thing)) return thing.slice();
26
24
  return [thing];
27
25
  }
28
- /** Set many keys in a Map to the same value */
29
- function setEach(map, keys, value) {
30
- if (isArray(keys)) for (const key of keys) map.set(key, value);
31
- else map.set(keys, value);
32
- }
33
- function assert(expr, msg = "") {
34
- if (!expr) throw new Error(msg);
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
+ }
35
48
  }
49
+ /**
50
+ * Where this library's output goes. Tests replace `write` to capture output.
51
+ *
52
+ * @internal
53
+ */
54
+ const output = { write: writeFd };
36
55
  /** Print a message to stdout, trimming trailing whitespace. */
37
56
  function print(text) {
38
- console.log(text.trimEnd());
57
+ output.write(1, `${text.trimEnd()}\n`);
39
58
  }
40
59
  /** Print an error message to stderr with the shared `Error:` prefix. */
41
60
  function printActionError(message) {
42
- console.error("Error:", message);
61
+ output.write(2, `Error: ${message}\n`);
43
62
  }
44
- // src/analyze.ts
45
- const $optionArg = (index, start, end, literal, option, separator) => ({
46
- kind: "option-arg",
47
- index,
48
- literal,
49
- start,
50
- end,
51
- option,
52
- separator
53
- });
54
- const $unknownOption = (index, start, end, literal, validOptions) => ({
55
- kind: "unknown-option",
56
- index,
57
- start,
58
- end,
59
- literal,
60
- validOptions
61
- });
62
- const $arg = (index, start, end, literal) => ({
63
- kind: "arg",
64
- index,
65
- start,
66
- end,
67
- literal
68
- });
69
- const $argSeparator = (index, start, end, literal) => ({
70
- kind: "arg-separator",
71
- index,
72
- start,
73
- end,
74
- literal
75
- });
76
- const $subcommand = (index, start, end, literal, subcommand) => {
77
- return {
78
- kind: "subcommand",
79
- index,
80
- start,
81
- end,
82
- literal,
83
- subcommand
84
- };
85
- };
86
- const $option = (index, start, end, literal, option) => {
87
- return {
88
- kind: "option",
89
- index,
90
- start,
91
- end,
92
- literal,
93
- option
94
- };
95
- };
96
63
  /**
97
- * Lex the input, output some tokens
64
+ * Returns the minimum number of arguments that satisfies the args.
98
65
  *
99
- * This generator uses the options and subcommands to categorize the
100
- * input tokens. It will not track or reject arguments - tokens that
101
- * don't match an option or subcommand are yielded as arguments.
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.
102
68
  */
103
- function analyze(input, spec) {
104
- const tokens = [];
105
- if (input.length === 0) {
106
- const localOptions = /* @__PURE__ */ new Map();
107
- const localRequiredOptions = /* @__PURE__ */ new Map();
108
- const persistentOptions = /* @__PURE__ */ new Map();
109
- const persistentRequiredOptions = /* @__PURE__ */ new Map();
110
- if (spec.options) for (const option of spec.options) if (option.isPersistent) {
111
- setEach(persistentOptions, option.name, option);
112
- if (option.isRequired) setEach(persistentRequiredOptions, option.name, option);
113
- } else {
114
- setEach(localOptions, option.name, option);
115
- if (option.isRequired) setEach(localRequiredOptions, option.name, option);
116
- }
117
- return {
118
- finalState: {
119
- localOptions,
120
- persistentOptions,
121
- localRequiredOptions,
122
- persistentRequiredOptions
123
- },
124
- tokens
125
- };
126
- }
127
- let localSubcommands;
128
- const localOptions = /* @__PURE__ */ new Map();
129
- const persistentOptions = /* @__PURE__ */ new Map();
130
- const localRequiredOptions = /* @__PURE__ */ new Map();
131
- const persistentRequiredOptions = /* @__PURE__ */ new Map();
132
- let separators = ["="];
133
- let hasFoundArg = false;
134
- let posixCompliantOptions = true;
135
- let subcommandsMatchUniquePrefix = false;
136
- let optionsMustPrecedeArguments = false;
137
- let hasUsedNonPersistentOption = false;
138
- const getOption = (name) => localOptions.get(name) ?? persistentOptions.get(name) ?? null;
139
- const hasOption = (name) => localOptions.has(name) || persistentOptions.has(name);
140
- const getSubcommand = (name) => {
141
- if (!localSubcommands) return null;
142
- let prefixMatch = null;
143
- let prefixCount = 0;
144
- for (const command of localSubcommands) {
145
- const names = typeof command.name === "string" ? [command.name] : command.name;
146
- for (const cmdName of names) {
147
- if (cmdName === name) return command;
148
- if (subcommandsMatchUniquePrefix && cmdName.startsWith(name)) {
149
- prefixMatch = command;
150
- prefixCount += 1;
151
- break;
152
- }
153
- }
154
- }
155
- return prefixCount === 1 ? prefixMatch : null;
156
- };
157
- const updateCurrentCommand = (command) => {
158
- if (command.options) for (const option of command.options) if (option.isPersistent) {
159
- setEach(persistentOptions, option.name, option);
160
- if (option.isRequired) setEach(persistentRequiredOptions, option.name, option);
161
- } else {
162
- setEach(localOptions, option.name, option);
163
- if (option.isRequired) setEach(localRequiredOptions, option.name, option);
164
- }
165
- localSubcommands = command.subcommands;
166
- const directives = command.parserDirectives;
167
- if (directives?.optionArgSeparators) separators = asArray(directives.optionArgSeparators);
168
- if (directives?.subcommandsMatchUniquePrefix) subcommandsMatchUniquePrefix = directives.subcommandsMatchUniquePrefix;
169
- if (directives?.optionsMustPrecedeArguments) optionsMustPrecedeArguments = directives.optionsMustPrecedeArguments;
170
- if (typeof directives?.flagsArePosixNoncompliant === "boolean") posixCompliantOptions = !directives.flagsArePosixNoncompliant;
171
- };
172
- updateCurrentCommand(spec);
173
- tokens: for (let index = 0; index < input.length; index++) {
174
- const token = input[index];
175
- if (token === "--") {
176
- tokens.push($argSeparator(index, 0, token.length, token));
177
- index += 1;
178
- for (; index < input.length; index++) {
179
- const tok = input[index];
180
- tokens.push($arg(index, 0, tok.length, tok));
181
- }
182
- break tokens;
183
- }
184
- if (!hasFoundArg && !hasUsedNonPersistentOption && localSubcommands) {
185
- const command = getSubcommand(token);
186
- if (command) {
187
- localOptions.clear();
188
- updateCurrentCommand(command);
189
- tokens.push($subcommand(index, 0, token.length, token, command));
190
- continue tokens;
191
- }
192
- }
193
- if (!(optionsMustPrecedeArguments && hasFoundArg)) {
194
- if (posixCompliantOptions && token.length > 1 && (token.startsWith("-") || token.startsWith("+")) && !token.startsWith("--")) {
195
- const leadingChar = token[0];
196
- const dashOption = getOption(leadingChar);
197
- if (dashOption?.args && !hasOption(token.slice(0, 2))) {
198
- tokens.push($option(index, 0, 1, leadingChar, dashOption));
199
- tokens.push($optionArg(index, 1, token.length, token.slice(1), leadingChar, ""));
200
- continue tokens;
201
- }
202
- chars: for (let char = 1; char < token.length; char++) {
203
- const optionName = `${leadingChar}${token[char]}`;
204
- const option = getOption(optionName);
205
- if (!option) {
206
- tokens.push($unknownOption(index, char, char + 1, optionName, [...localOptions.keys(), ...persistentOptions.keys()]));
207
- continue chars;
208
- }
209
- if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
210
- tokens.push($option(index, char, char + 1, optionName, option));
211
- if (option.args && char < token.length - 1) {
212
- const remainingChars = token.slice(char + 1);
213
- const sep = separators.find((sep) => remainingChars.startsWith(sep)) || "";
214
- const argStartOffset = char + sep.length;
215
- tokens.push($optionArg(index, argStartOffset + 1, token.length, token.slice(argStartOffset + 1), optionName, sep));
216
- break;
217
- }
218
- }
219
- continue tokens;
220
- }
221
- if (!posixCompliantOptions || token.length > 2 && token.startsWith("--")) {
222
- let foundSep = null;
223
- let foundSepIndex = -1;
224
- separators: for (const sep of separators) {
225
- const index = token.indexOf(sep);
226
- if (index === -1) continue separators;
227
- foundSep = sep;
228
- foundSepIndex = index;
229
- }
230
- if (foundSep) {
231
- const optionName = token.slice(0, foundSepIndex);
232
- const option = getOption(optionName);
233
- if (option) {
234
- if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
235
- tokens.push($option(index, 0, foundSepIndex, token.slice(0, foundSepIndex), option));
236
- tokens.push($optionArg(index, foundSepIndex + foundSep.length, token.length, token.slice(foundSepIndex + foundSep.length), optionName, foundSep));
237
- continue tokens;
238
- } else if (posixCompliantOptions) {
239
- const optionName = token.slice(0, foundSepIndex);
240
- tokens.push($unknownOption(index, 0, foundSepIndex, optionName, [...localOptions.keys(), ...persistentOptions.keys()]));
241
- tokens.push($optionArg(index, foundSepIndex + foundSep.length, token.length, token.slice(foundSepIndex + foundSep.length), optionName, foundSep));
242
- continue tokens;
243
- }
244
- } else {
245
- const option = getOption(token);
246
- if (option) {
247
- if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
248
- tokens.push($option(index, 0, token.length, token, option));
249
- continue tokens;
250
- } else if (posixCompliantOptions) {
251
- tokens.push($unknownOption(index, 0, token.length, token, [...localOptions.keys(), ...persistentOptions.keys()]));
252
- continue tokens;
253
- }
254
- }
255
- }
256
- }
257
- if (!hasFoundArg) hasFoundArg = true;
258
- tokens.push($arg(index, 0, token.length, token));
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++;
259
76
  }
260
- return {
261
- finalState: {
262
- localOptions,
263
- persistentOptions,
264
- localRequiredOptions,
265
- persistentRequiredOptions
266
- },
267
- tokens
268
- };
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;
269
85
  }
270
86
  // src/errors.ts
271
87
  const repr = JSON.stringify;
@@ -283,14 +99,14 @@ var UnknownOption = class extends ParseError {
283
99
  end;
284
100
  option;
285
101
  validOptions;
286
- constructor(token, context) {
287
- const msg = `${repr(token.literal)} looks like a flag, but is unknown in this context`;
102
+ constructor(found, context) {
103
+ const msg = `${repr(found.option)} looks like a flag, but is unknown in this context`;
288
104
  super(context, msg);
289
- this.option = token.literal;
290
- this.validOptions = token.validOptions;
291
- this.index = token.index;
292
- this.start = token.start;
293
- this.end = token.end;
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;
294
110
  }
295
111
  };
296
112
  var InvalidOptionArg = class extends ParseError {
@@ -299,24 +115,24 @@ var InvalidOptionArg = class extends ParseError {
299
115
  end;
300
116
  arg;
301
117
  option;
302
- constructor(token, context) {
303
- const msg = `Option ${token.option} doesn't take arguments`;
118
+ constructor(found, context) {
119
+ const msg = `Option ${found.option} doesn't take arguments`;
304
120
  super(context, msg);
305
- this.index = token.index;
306
- this.start = token.start;
307
- this.end = token.end;
308
- this.option = token.option;
309
- this.arg = token.literal;
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;
310
126
  }
311
127
  };
312
128
  var TooManyArguments = class extends ParseError {
313
129
  index;
314
130
  value;
315
- constructor(token, context) {
316
- const msg = `Too many arguments, ${repr(token.literal)} was unexpected`;
131
+ constructor(found, context) {
132
+ const msg = `Too many arguments, ${repr(found.value)} was unexpected`;
317
133
  super(context, msg);
318
- this.index = token.index;
319
- this.value = token.literal;
134
+ this.index = found.index;
135
+ this.value = found.value;
320
136
  }
321
137
  };
322
138
  var TooFewOptionArguments = class extends ParseError {
@@ -337,122 +153,101 @@ var MissingRequiredOption = class extends ParseError {
337
153
  super(context, msg);
338
154
  }
339
155
  };
340
- // src/help.ts
341
- var import_fastest_levenshtein = (/* @__PURE__ */ __commonJSMin(((exports) => {
342
- exports.__esModule = true;
343
- exports.distance = exports.closest = void 0;
344
- var peq = /* @__PURE__ */ new Uint32Array(65536);
345
- var myers_32 = function(a, b) {
346
- var n = a.length;
347
- var m = b.length;
348
- var lst = 1 << n - 1;
349
- var pv = -1;
350
- var mv = 0;
351
- var sc = n;
352
- var i = n;
353
- while (i--) peq[a.charCodeAt(i)] |= 1 << i;
354
- for (i = 0; i < m; i++) {
355
- var eq = peq[b.charCodeAt(i)];
356
- var xv = eq | mv;
357
- eq |= (eq & pv) + pv ^ pv;
358
- mv |= ~(eq | pv);
359
- pv &= eq;
360
- if (mv & lst) sc++;
361
- if (pv & lst) sc--;
362
- mv = mv << 1 | 1;
363
- pv = pv << 1 | ~(xv | mv);
364
- mv &= xv;
365
- }
366
- i = n;
367
- while (i--) peq[a.charCodeAt(i)] = 0;
368
- return sc;
369
- };
370
- var myers_x = function(b, a) {
371
- var n = a.length;
372
- var m = b.length;
373
- var mhc = [];
374
- var phc = [];
375
- var hsize = Math.ceil(n / 32);
376
- var vsize = Math.ceil(m / 32);
377
- for (var i = 0; i < hsize; i++) {
378
- phc[i] = -1;
379
- mhc[i] = 0;
380
- }
381
- var j = 0;
382
- for (; j < vsize - 1; j++) {
383
- var mv_1 = 0;
384
- var pv_1 = -1;
385
- var start_1 = j * 32;
386
- var vlen_1 = Math.min(32, m) + start_1;
387
- for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] |= 1 << k;
388
- for (var i = 0; i < n; i++) {
389
- var eq = peq[a.charCodeAt(i)];
390
- var pb = phc[i / 32 | 0] >>> i & 1;
391
- var mb = mhc[i / 32 | 0] >>> i & 1;
392
- var xv = eq | mv_1;
393
- var xh = ((eq | mb) & pv_1) + pv_1 ^ pv_1 | eq | mb;
394
- var ph = mv_1 | ~(xh | pv_1);
395
- var mh = pv_1 & xh;
396
- if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
397
- if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
398
- ph = ph << 1 | pb;
399
- mh = mh << 1 | mb;
400
- pv_1 = mh | ~(xv | ph);
401
- mv_1 = ph & xv;
402
- }
403
- for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] = 0;
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;
404
196
  }
405
- var mv = 0;
406
- var pv = -1;
407
- var start = j * 32;
408
- var vlen = Math.min(32, m - start) + start;
409
- for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] |= 1 << k;
410
- var score = m;
411
- for (var i = 0; i < n; i++) {
412
- var eq = peq[a.charCodeAt(i)];
413
- var pb = phc[i / 32 | 0] >>> i & 1;
414
- var mb = mhc[i / 32 | 0] >>> i & 1;
415
- var xv = eq | mv;
416
- var xh = ((eq | mb) & pv) + pv ^ pv | eq | mb;
417
- var ph = mv | ~(xh | pv);
418
- var mh = pv & xh;
419
- score += ph >>> m - 1 & 1;
420
- score -= mh >>> m - 1 & 1;
421
- if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
422
- if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
423
- ph = ph << 1 | pb;
424
- mh = mh << 1 | mb;
425
- pv = mh | ~(xv | ph);
426
- mv = ph & xv;
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++;
427
224
  }
428
- for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] = 0;
429
- return score;
430
- };
431
- var distance = function(a, b) {
432
- if (a.length < b.length) {
433
- var tmp = b;
434
- b = a;
435
- a = tmp;
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++;
436
233
  }
437
- if (b.length === 0) return a.length;
438
- if (a.length <= 32) return myers_32(a, b);
439
- return myers_x(a, b);
440
- };
441
- exports.distance = distance;
442
- var closest = function(str, arr) {
443
- var min_distance = Infinity;
444
- var min_index = 0;
445
- for (var i = 0; i < arr.length; i++) {
446
- var dist = distance(str, arr[i]);
447
- if (dist < min_distance) {
448
- min_distance = dist;
449
- min_index = i;
450
- }
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;
451
246
  }
452
- return arr[min_index];
453
- };
454
- exports.closest = closest;
455
- })))();
247
+ }
248
+ return best;
249
+ }
250
+ // src/help.ts
456
251
  function getNamed(named, name) {
457
252
  return named.find((obj) => makeArray(obj.name).includes(name)) ?? null;
458
253
  }
@@ -472,7 +267,7 @@ function dedent(str) {
472
267
  function getHelp(path, config = {}) {
473
268
  const { description = true, usage = true, didYouMean } = config;
474
269
  const sections = getSections(path);
475
- if (didYouMean) sections.didYouMean = (0, import_fastest_levenshtein.closest)(didYouMean.input, didYouMean.choices);
270
+ if (didYouMean) sections.didYouMean = closestChoice(didYouMean.input, didYouMean.choices);
476
271
  if (!description) sections.description = void 0;
477
272
  if (!usage) sections.usage = void 0;
478
273
  return formatHelpSections(sections);
@@ -683,211 +478,352 @@ function argToString(arg) {
683
478
  return `${lhs}${name}${rhs}${suffix}`;
684
479
  }
685
480
  // src/parse.ts
481
+ function hasName(names, name) {
482
+ return typeof names === "string" ? names === name : names.includes(name);
483
+ }
686
484
  /**
687
- * Returns the minimum number of arguments that satisfies the args.
688
- *
689
- * Accepts the raw spec value (a single arg, an array, or `undefined`) so the
690
- * hot path doesn't have to copy it into an array first.
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.
691
487
  */
692
- function getMinArgs(args) {
693
- if (args === void 0) return 0;
694
- if (!isArray(args)) return args.isOptional ? 0 : 1;
695
- let count = 0;
696
- for (let i = 0; i < args.length; i++) {
697
- if (args[i].isOptional) break;
698
- count++;
699
- }
700
- return count;
701
- }
702
- /** Returns the maximum number of arguments that satisfies the args. */
703
- function getMaxArgs(args) {
704
- if (args === void 0) return 0;
705
- if (!isArray(args)) return args.isVariadic ? Infinity : 1;
706
- for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
707
- return args.length;
708
- }
709
- /** Check if a map has a key or any key from an array */
710
- function has(map, key) {
711
- if (isArray(key)) return map.has(key[0]);
712
- return map.has(key);
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;
713
492
  }
714
- /** Get a value from a map by a key or any key from an array */
715
- function get(map, key) {
716
- if (isArray(key)) return map.get(key[0]);
717
- return map.get(key);
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;
718
504
  }
719
505
  /**
720
- * Parse the input tokens using the given CLI spec.
721
- *
722
- * This returns a `ParseResult`, which contains the array of
723
- * subcommands used, the provided options, and the arguments.
506
+ * Read a command line against a spec in one pass.
724
507
  *
725
- * If parsing fails, this function throws.
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.
726
512
  *
727
- * This function is exposed to make it easier to build a custom runner.
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.
728
516
  */
729
- function parse(input, spec) {
517
+ function scan(input, spec, strict) {
730
518
  const path = [spec];
731
519
  const actions = [];
732
520
  const optionActions = [];
733
- if (spec.action) actions.push(spec.action);
734
- else if (spec.requiresSubcommand) actions.push(usage);
735
- let commandArgsMin = getMinArgs(spec.args);
736
- let commandArgsMax = getMaxArgs(spec.args);
737
- const foundArgs = [];
738
- const foundOptions = /* @__PURE__ */ new Map();
739
- let state = 0;
740
- let optionArgs = null;
741
- let optionArgsMin = 0;
742
- let optionArgsMax = 0;
743
- let requiredSeparator = false;
521
+ const args = [];
522
+ const options = /* @__PURE__ */ new Map();
523
+ const seenOptions = [];
744
524
  let argSeparatorIndex = -1;
745
- const dependsOnOptions = [];
746
- const exclusiveOnOptions = [];
747
- const parseOption = (token) => {
748
- const option = token.option;
749
- if (!option.isRepeatable && has(foundOptions, option.name)) throw new ParseError(ctx(), "Repeated option");
750
- if (option.exclusiveOn) exclusiveOnOptions.push(option);
751
- if (option.dependsOn) dependsOnOptions.push(option);
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);
752
584
  if (option.action) optionActions.push(option.action);
585
+ const names = asArray(option.name);
586
+ let optionValues;
753
587
  if (option.isRepeatable) {
754
588
  const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
755
- let arr = get(foundOptions, option.name);
756
- if (!arr) {
757
- arr = [];
758
- setEach(foundOptions, option.name, arr);
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);
759
596
  }
760
- if (arr.length >= maxRepeat) throw new ParseError(ctx(), "Too many repetitions");
761
- arr.push("");
762
- return;
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);
763
608
  }
764
- optionArgsMin = getMinArgs(option.args);
765
- optionArgsMax = getMaxArgs(option.args);
766
- optionArgs = [];
767
- setEach(foundOptions, option.name, optionArgs);
768
- if (optionArgsMax === 0) {
769
- optionArgs = null;
770
- state = 0;
771
- return;
609
+ valuesMin = valueMin;
610
+ valuesMax = getMaxArgs(option.args);
611
+ requiredSeparator = option.requiresSeparator ?? false;
612
+ if (valuesMax > 0) {
613
+ values = optionValues;
614
+ valuesStart = optionValues.length;
772
615
  }
773
- if (option.requiresSeparator) {
774
- state = 2;
775
- requiredSeparator = option.requiresSeparator;
776
- } else state = 1;
777
616
  };
778
- const parseArg = (token) => {
779
- if (!path[path.length - 1].requiresSubcommand && foundArgs.length >= commandArgsMax) throw new TooManyArguments(token, ctx());
780
- foundArgs.push(token.literal);
781
- };
782
- const parseArgSeparator = () => {
783
- if (foundArgs.length >= commandArgsMax) throw new ParseError(ctx(), "Unexpected argument '--', did you mean to use an option instead?");
784
- argSeparatorIndex = foundArgs.length;
785
- state = 0;
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);
786
631
  };
787
- const parseSubcommand = (token) => {
788
- path.push(token.subcommand);
789
- commandArgsMin = getMinArgs(token.subcommand.args);
790
- commandArgsMax = getMaxArgs(token.subcommand.args);
791
- if (token.subcommand.action) actions.push(token.subcommand.action);
792
- else if (token.subcommand.requiresSubcommand) actions.push(usage);
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);
793
639
  };
794
- const ctx = () => ({ path });
795
- const { finalState, tokens } = analyze(input, spec);
796
- for (const token of tokens) switch (state) {
797
- case 0:
798
- switch (token.kind) {
799
- case "subcommand":
800
- parseSubcommand(token);
801
- break;
802
- case "arg":
803
- parseArg(token);
804
- break;
805
- case "option":
806
- parseOption(token);
807
- break;
808
- case "unknown-option": throw new UnknownOption(token, ctx());
809
- case "option-arg": throw new InvalidOptionArg(token, ctx());
810
- case "arg-separator":
811
- parseArgSeparator();
812
- break;
813
- default: throw new ParseError(ctx(), "Unreachable");
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
+ }
814
684
  }
815
- break;
816
- case 1:
817
- assert(Array.isArray(optionArgs), "Invalid state, must have an array to store option arguments");
818
- switch (token.kind) {
819
- case "option-arg":
820
- case "arg":
821
- if (optionArgs.length < optionArgsMax) optionArgs.push(token.literal);
822
- else {
823
- optionArgs = null;
824
- state = 0;
825
- parseArg(token);
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;
826
721
  }
827
- break;
828
- case "option":
829
- if (optionArgs.length < optionArgsMin) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
830
- parseOption(token);
831
- break;
832
- case "unknown-option":
833
- if (optionArgs.length < optionArgsMax) optionArgs.push(token.literal);
834
- else throw new UnknownOption(token, ctx());
835
- break;
836
- case "arg-separator":
837
- if (optionArgs.length < optionArgsMin) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
838
- parseArgSeparator();
839
- break;
840
- case "subcommand": throw new ParseError(ctx(), "Unexpected token");
841
- default: throw new ParseError(ctx(), "Unreachable");
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;
842
736
  }
843
- break;
844
- case 2:
845
- assert(Array.isArray(optionArgs), "Invalid state, must have an array to store option arguments");
846
- assert(requiredSeparator, "Invalid state, a separator must be set");
847
- switch (token.kind) {
848
- case "option-arg":
849
- if (typeof requiredSeparator === "string" && token.separator !== requiredSeparator) throw new ParseError(ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${token.separator}'`);
850
- optionArgs.push(token.literal);
851
- state = 0;
852
- break;
853
- case "arg":
854
- if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
855
- optionArgs = null;
856
- state = 0;
857
- parseArg(token);
858
- break;
859
- case "option":
860
- if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
861
- parseOption(token);
862
- break;
863
- case "unknown-option": throw new UnknownOption(token, ctx());
864
- case "arg-separator": throw new ParseError(ctx(), "Unexpected token");
865
- case "subcommand":
866
- if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
867
- parseSubcommand(token);
868
- state = 0;
869
- break;
870
- default: throw new ParseError(ctx(), "Unreachable");
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
+ }
871
772
  }
773
+ }
774
+ if (valueCount < separateValueMax) {
775
+ addValue(index, 0, token.length, token, "");
776
+ continue tokens;
777
+ }
778
+ hasFoundArg = true;
779
+ addArg(index, token);
872
780
  }
873
- const hasOptionAction = optionActions.length > 0;
874
- if (!hasOptionAction && foundArgs.length < commandArgsMin) throw new TooFewArguments(commandArgsMin, commandArgsMax, ctx());
875
- if (optionArgs && optionArgs.length < optionArgsMin) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
876
- for (const option of dependsOnOptions) for (const name of option.dependsOn) if (!foundOptions.has(name)) throw new ParseError(ctx(), `${option.name} requires ${name}, add it to fix this error`);
877
- for (const option of exclusiveOnOptions) for (const name of option.exclusiveOn) if (foundOptions.has(name)) throw new ParseError(ctx(), `${option.name} can't be used together with ${name}`);
878
- if (!hasOptionAction) {
879
- for (const name of finalState.localRequiredOptions.keys()) if (!foundOptions.has(name)) throw new MissingRequiredOption(name, ctx());
880
- for (const name of finalState.persistentRequiredOptions.keys()) if (!foundOptions.has(name)) throw new MissingRequiredOption(name, ctx());
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
+ }
881
796
  }
797
+ const open = openOption;
882
798
  return {
883
799
  path,
884
800
  actions,
885
801
  optionActions,
802
+ args,
886
803
  argSeparatorIndex,
887
- args: foundArgs,
888
- options: foundOptions
804
+ options,
805
+ seenOptions,
806
+ localOptions: local,
807
+ persistentOptions: persistent,
808
+ openOption: open && valueCount < separateValueMax ? {
809
+ option: open,
810
+ valueCount
811
+ } : null
889
812
  };
890
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
+ }
891
827
  // src/completion.ts
892
828
  const SHELLS = [
893
829
  "bash",
@@ -900,97 +836,6 @@ function isShell(value) {
900
836
  const firstLine = (description) => description ? description.split("\n", 1)[0].trim() : "";
901
837
  /** Turn an arbitrary name into a safe shell identifier fragment. */
902
838
  const sanitize = (value) => value.replace(/[^A-Za-z0-9]+/g, "_");
903
- function encodeLocation(location) {
904
- const bytes = new TextEncoder().encode(JSON.stringify(location));
905
- let binary = "";
906
- for (const byte of bytes) binary += String.fromCharCode(byte);
907
- return btoa(binary);
908
- }
909
- function decodeLocation(encoded) {
910
- const binary = atob(encoded);
911
- const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
912
- return JSON.parse(new TextDecoder().decode(bytes));
913
- }
914
- /** Build {@link ArgComplete} for one argument at a known spec location. */
915
- function argCompleteFor(arg, location) {
916
- const staticValues = (arg.suggestions ?? []).map((suggestion) => typeof suggestion === "string" ? suggestion : makeArray(suggestion.name)[0]);
917
- const genIndex = makeArray(arg.generators).findIndex((generator) => generator.custom);
918
- const customLocation = genIndex >= 0 ? encodeLocation({
919
- ...location,
920
- genIndex
921
- }) : void 0;
922
- if (!arg.template && staticValues.length === 0 && !customLocation) return;
923
- return {
924
- template: arg.template,
925
- staticValues,
926
- customLocation
927
- };
928
- }
929
- /** Completion for a option's first argument, if it has any metadata. */
930
- function optionArgComplete(option, path) {
931
- const arg = asArray(option.args)[0];
932
- if (!arg) return void 0;
933
- return argCompleteFor(arg, {
934
- path,
935
- option: makeArray(option.name)[0],
936
- argIndex: 0
937
- });
938
- }
939
- /** Completion for a command's first positional arg that has metadata. */
940
- function positionalComplete(command, path) {
941
- const args = asArray(command.args);
942
- for (let index = 0; index < args.length; index += 1) {
943
- const ac = argCompleteFor(args[index], {
944
- path,
945
- argIndex: index
946
- });
947
- if (ac) return ac;
948
- }
949
- }
950
- function toOptionInfo(option, path) {
951
- return {
952
- tokens: makeArray(option.name),
953
- description: firstLine(option.description),
954
- takesArg: asArray(option.args).length > 0,
955
- argComplete: optionArgComplete(option, path)
956
- };
957
- }
958
- /**
959
- * Flatten the spec into a list of command nodes. Hidden options and subcommands
960
- * are excluded; persistent options are propagated to descendants.
961
- */
962
- function walk(spec) {
963
- const nodes = [];
964
- const recurse = (command, key, path, inheritedPersistent) => {
965
- const ownOptions = (command.options ?? []).filter((option) => !option.hidden);
966
- const children = (command.subcommands ?? []).filter((sub) => !sub.hidden);
967
- const node = {
968
- key,
969
- path,
970
- subcommands: [],
971
- options: [...inheritedPersistent, ...ownOptions].map((option) => toOptionInfo(option, path)),
972
- positional: positionalComplete(command, path),
973
- transitions: []
974
- };
975
- const childPersistent = [...inheritedPersistent, ...ownOptions.filter((option) => option.isPersistent)];
976
- for (const child of children) {
977
- const tokens = makeArray(child.name);
978
- const childKey = `${key}_${sanitize(tokens[0])}`;
979
- node.subcommands.push({
980
- tokens,
981
- description: firstLine(child.description)
982
- });
983
- for (const token of tokens) node.transitions.push({
984
- token,
985
- childKey
986
- });
987
- recurse(child, childKey, [...path, tokens[0]], childPersistent);
988
- }
989
- nodes.push(node);
990
- };
991
- recurse(spec, "cmd", [], []);
992
- return nodes;
993
- }
994
839
  /** Single-quote a string for bash/zsh. */
995
840
  function shQuote(value) {
996
841
  return `'${value.replaceAll("'", "'\\''")}'`;
@@ -999,318 +844,53 @@ function shQuote(value) {
999
844
  function fishQuote(value) {
1000
845
  return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
1001
846
  }
1002
- /** Every candidate token offered at a node (subcommand aliases + option aliases). */
1003
- function candidateTokens(node) {
1004
- const tokens = [];
1005
- for (const sub of node.subcommands) tokens.push(...sub.tokens);
1006
- for (const option of node.options) tokens.push(...option.tokens);
1007
- return tokens;
1008
- }
1009
- function renderTransitionCases(nodes, indent) {
1010
- const lines = [];
1011
- for (const node of nodes) for (const { token, childKey } of node.transitions) lines.push(`${indent}${shQuote(`${node.key}::${token}`)}) cmd=${shQuote(childKey)} ;;`);
1012
- return lines;
1013
- }
1014
847
  /**
1015
- * bash candidate lines for an arg. `append` keeps existing COMPREPLY (for a
1016
- * positional, which coexists with subcommands/options); otherwise it replaces.
1017
- * The generator callback uses the typed command and is guarded for resilience.
848
+ * Read the line typed so far with the parser in lenient mode, which skips
849
+ * anything invalid instead of throwing.
1018
850
  */
1019
- function bashArgLines(ac, indent, append) {
1020
- const lines = [];
1021
- const op = append ? "COMPREPLY+=" : "COMPREPLY=";
1022
- const hasWords = ac.staticValues.length > 0 || ac.customLocation !== void 0;
1023
- if (hasWords) {
1024
- lines.push(`${indent}local words=${ac.staticValues.length > 0 ? shQuote(ac.staticValues.join(" ")) : "\"\""}`);
1025
- if (ac.customLocation) {
1026
- lines.push(`${indent}if command -v "\${COMP_WORDS[0]}" >/dev/null 2>&1; then`);
1027
- lines.push(`${indent} words="$words $("\${COMP_WORDS[0]}" completion generate-suggestions ${shQuote(ac.customLocation)} -- "\${COMP_WORDS[@]:1}" 2>/dev/null)"`);
1028
- lines.push(`${indent}fi`);
1029
- }
1030
- lines.push(`${indent}${op}( $(compgen -W "$words" -- "$cur") )`);
1031
- }
1032
- if (ac.template) {
1033
- const option = ac.template === "folders" ? "-d" : "-f";
1034
- const fileOp = hasWords || append ? "COMPREPLY+=" : "COMPREPLY=";
1035
- lines.push(`${indent}${fileOp}( $(compgen ${option} -- "$cur") )`);
1036
- }
1037
- return lines;
1038
- }
1039
- function renderBash(spec, nodes) {
1040
- const id = sanitize(spec.name);
1041
- const lines = [];
1042
- lines.push(`_${id}() {`);
1043
- lines.push(` local cur="\${COMP_WORDS[COMP_CWORD]}"`);
1044
- lines.push(` local prev="\${COMP_WORDS[COMP_CWORD-1]}"`);
1045
- lines.push(` local cmd="cmd" i w`);
1046
- lines.push(` for (( i=1; i < COMP_CWORD; i++ )); do`);
1047
- lines.push(` w="\${COMP_WORDS[i]}"`);
1048
- lines.push(` case "$w" in -* ) continue ;; esac`);
1049
- lines.push(` case "\${cmd}::\${w}" in`);
1050
- lines.push(...renderTransitionCases(nodes, " "));
1051
- lines.push(` esac`);
1052
- lines.push(` done`);
1053
- const argCases = [];
1054
- for (const node of nodes) for (const option of node.options) {
1055
- if (!option.argComplete) continue;
1056
- for (const token of option.tokens) {
1057
- argCases.push(` ${shQuote(`${node.key}::${token}`)})`);
1058
- argCases.push(...bashArgLines(option.argComplete, " ", false));
1059
- argCases.push(` return ;;`);
1060
- }
1061
- }
1062
- if (argCases.length > 0) {
1063
- lines.push(` case "\${cmd}::\${prev}" in`);
1064
- lines.push(...argCases);
1065
- lines.push(` esac`);
1066
- }
1067
- lines.push(` local opts=""`);
1068
- lines.push(` case "$cmd" in`);
1069
- for (const node of nodes) lines.push(` ${shQuote(node.key)}) opts=${shQuote(candidateTokens(node).join(" "))} ;;`);
1070
- lines.push(` esac`);
1071
- lines.push(` COMPREPLY=( $(compgen -W "$opts" -- "$cur") )`);
1072
- const posCases = [];
1073
- for (const node of nodes) {
1074
- if (!node.positional) continue;
1075
- posCases.push(` ${shQuote(node.key)})`);
1076
- posCases.push(...bashArgLines(node.positional, " ", true));
1077
- posCases.push(` ;;`);
1078
- }
1079
- if (posCases.length > 0) {
1080
- lines.push(` case "$cmd" in`);
1081
- lines.push(...posCases);
1082
- lines.push(` esac`);
1083
- }
1084
- lines.push(`}`);
1085
- lines.push(`complete -F _${id} ${shQuote(spec.name)}`);
1086
- return lines.join("\n");
1087
- }
1088
- /** zsh candidate lines for an arg (used for both option values and positionals). */
1089
- function zshArgLines(ac, indent) {
1090
- const lines = [];
1091
- if (ac.staticValues.length > 0 || ac.customLocation !== void 0) {
1092
- lines.push(`${indent}local -a vals`);
1093
- lines.push(`${indent}vals=( ${ac.staticValues.map(shQuote).join(" ")} )`);
1094
- if (ac.customLocation) {
1095
- lines.push(`${indent}if command -v "$words[1]" >/dev/null 2>&1; then`);
1096
- lines.push(`${indent} vals+=( \${(f)"$("$words[1]" completion generate-suggestions ${shQuote(ac.customLocation)} -- \${words[2,-1]} 2>/dev/null)"} )`);
1097
- lines.push(`${indent}fi`);
1098
- }
1099
- lines.push(`${indent}compadd -a vals`);
1100
- }
1101
- if (ac.template) lines.push(`${indent}${ac.template === "folders" ? "_files -/" : "_files"}`);
1102
- return lines;
1103
- }
1104
- function renderZsh(spec, nodes) {
1105
- const id = sanitize(spec.name);
1106
- const lines = [];
1107
- lines.push(`#compdef ${spec.name}`);
1108
- lines.push(`_${id}() {`);
1109
- lines.push(` local cmd=cmd i w prev`);
1110
- lines.push(` for (( i=2; i < CURRENT; i++ )); do`);
1111
- lines.push(` w="\${words[i]}"`);
1112
- lines.push(` case "$w" in -* ) continue ;; esac`);
1113
- lines.push(` case "\${cmd}::\${w}" in`);
1114
- lines.push(...renderTransitionCases(nodes, " "));
1115
- lines.push(` esac`);
1116
- lines.push(` done`);
1117
- lines.push(` prev="\${words[CURRENT-1]}"`);
1118
- const argCases = [];
1119
- for (const node of nodes) for (const option of node.options) {
1120
- if (!option.argComplete) continue;
1121
- for (const token of option.tokens) {
1122
- argCases.push(` ${shQuote(`${node.key}::${token}`)})`);
1123
- argCases.push(...zshArgLines(option.argComplete, " "));
1124
- argCases.push(` return ;;`);
1125
- }
1126
- }
1127
- if (argCases.length > 0) {
1128
- lines.push(` case "\${cmd}::\${prev}" in`);
1129
- lines.push(...argCases);
1130
- lines.push(` esac`);
1131
- }
1132
- lines.push(` local -a opts`);
1133
- lines.push(` case "$cmd" in`);
1134
- for (const node of nodes) {
1135
- const entries = [];
1136
- for (const sub of node.subcommands) for (const token of sub.tokens) entries.push(shQuote(`${token}:${sub.description}`));
1137
- for (const option of node.options) for (const token of option.tokens) entries.push(shQuote(`${token}:${option.description}`));
1138
- lines.push(` ${shQuote(node.key)}) opts=( ${entries.join(" ")} ) ;;`);
1139
- }
1140
- lines.push(` esac`);
1141
- lines.push(` _describe -t values ${shQuote(spec.name)} opts`);
1142
- const posCases = [];
1143
- for (const node of nodes) {
1144
- if (!node.positional) continue;
1145
- posCases.push(` ${shQuote(node.key)})`);
1146
- posCases.push(...zshArgLines(node.positional, " "));
1147
- posCases.push(` ;;`);
1148
- }
1149
- if (posCases.length > 0) {
1150
- lines.push(` case "$cmd" in`);
1151
- lines.push(...posCases);
1152
- lines.push(` esac`);
1153
- }
1154
- lines.push(`}`);
1155
- lines.push(`_${id} "$@"`);
1156
- return lines.join("\n");
1157
- }
1158
- /** Build the fish `complete` option selectors (`-s x`, `-l long`, `-o old`). */
1159
- function fishOptionSelectors(tokens) {
1160
- const parts = [];
1161
- for (const token of tokens) if (token.startsWith("--")) parts.push(`-l ${fishQuote(token.slice(2))}`);
1162
- else if (token.startsWith("-") && token.length === 2) parts.push(`-s ${fishQuote(token.slice(1))}`);
1163
- else if (token.startsWith("-") && token.length > 2) parts.push(`-o ${fishQuote(token.slice(1))}`);
1164
- else parts.push(`-a ${fishQuote(token)}`);
1165
- return parts.join(" ");
1166
- }
1167
- /** A guarded fish command substitution that runs a generator by location. */
1168
- function fishGuardedCallback(location) {
1169
- return `(set -l c (commandline -opc)[1]; if command -q -- $c; or test -x "$c"; $c completion generate-suggestions ${location} -- (commandline -opc) 2>/dev/null; end)`;
1170
- }
1171
- /** The `-a` candidate bits and whether the shell's file completion is wanted. */
1172
- function fishValueBits(ac) {
1173
- const parts = [...ac.staticValues];
1174
- if (ac.template === "folders" || Array.isArray(ac.template)) parts.push("(__fish_complete_directories)");
1175
- if (ac.customLocation) parts.push(fishGuardedCallback(ac.customLocation));
1176
- return {
1177
- aBits: parts.length > 0 ? ` -a ${fishQuote(parts.join(" "))}` : "",
1178
- wantsFiles: ac.template === "filepaths" || Array.isArray(ac.template)
1179
- };
1180
- }
1181
- /**
1182
- * The suffix after a option's selectors. Uses `-x` (exclusive: requires a value
1183
- * and suppresses fish's file fallback) when the arg has its own candidates and
1184
- * isn't a filepaths template; `-r` lets files through when they're wanted.
1185
- */
1186
- function fishOptionArgSuffix(option) {
1187
- if (!option.takesArg) return "";
1188
- if (!option.argComplete) return " -r";
1189
- const { aBits, wantsFiles } = fishValueBits(option.argComplete);
1190
- return `${wantsFiles ? " -r" : " -x"}${aBits}`;
1191
- }
1192
- function renderFish(spec, nodes) {
1193
- const id = sanitize(spec.name);
1194
- const name = spec.name;
1195
- const lines = [];
1196
- lines.push(`function __fish_${id}_at`);
1197
- lines.push(` set -l cmd cmd`);
1198
- lines.push(` set -l toks (commandline -opc)`);
1199
- lines.push(` for w in $toks[2..-1]`);
1200
- lines.push(` if string match -qr '^-' -- $w`);
1201
- lines.push(` continue`);
1202
- lines.push(` end`);
1203
- lines.push(` switch "$cmd $w"`);
1204
- for (const node of nodes) for (const { token, childKey } of node.transitions) {
1205
- lines.push(` case ${fishQuote(`${node.key} ${token}`)}`);
1206
- lines.push(` set cmd ${fishQuote(childKey)}`);
1207
- }
1208
- lines.push(` end`);
1209
- lines.push(` end`);
1210
- lines.push(` test "$cmd" = "$argv[1]"`);
1211
- lines.push(`end`);
1212
- lines.push("");
1213
- lines.push(`complete -c ${fishQuote(name)} -f`);
1214
- for (const node of nodes) {
1215
- const guard = `-n ${fishQuote(`__fish_${id}_at ${node.key}`)}`;
1216
- for (const sub of node.subcommands) for (const token of sub.tokens) {
1217
- const desc = sub.description ? ` -d ${fishQuote(sub.description)}` : "";
1218
- lines.push(`complete -c ${fishQuote(name)} ${guard} -a ${fishQuote(token)}${desc}`);
1219
- }
1220
- for (const option of node.options) {
1221
- const desc = option.description ? ` -d ${fishQuote(option.description)}` : "";
1222
- const suffix = fishOptionArgSuffix(option);
1223
- lines.push(`complete -c ${fishQuote(name)} ${guard} ${fishOptionSelectors(option.tokens)}${suffix}${desc}`);
1224
- }
1225
- if (node.positional) {
1226
- const { aBits, wantsFiles } = fishValueBits(node.positional);
1227
- lines.push(`complete -c ${fishQuote(name)} ${guard}${aBits}${wantsFiles ? " -F" : ""}`);
1228
- }
1229
- }
1230
- return lines.join("\n");
1231
- }
1232
- function analyzeState(spec, before) {
1233
- const { tokens, finalState } = analyze(before, spec);
1234
- const path = [spec];
1235
- let current = spec;
1236
- let pending = null;
1237
- let pendingFilled = 0;
1238
- let pendingMax = 0;
1239
- let positionalIndex = 0;
1240
- let forceArgs = false;
1241
- let sawPositional = false;
1242
- let usedNonPersistentOption = false;
851
+ function readLine(spec, before) {
852
+ const scanned = scan(before, spec, false);
853
+ const path = scanned.path;
1243
854
  const presentNames = /* @__PURE__ */ new Set();
1244
855
  const countByOption = /* @__PURE__ */ new Map();
1245
- for (const token of tokens) switch (token.kind) {
1246
- case "subcommand":
1247
- current = token.subcommand;
1248
- path.push(current);
1249
- positionalIndex = 0;
1250
- sawPositional = false;
1251
- pending = null;
1252
- break;
1253
- case "option": {
1254
- const option = token.option;
1255
- for (const name of makeArray(option.name)) presentNames.add(name);
1256
- countByOption.set(option, (countByOption.get(option) ?? 0) + 1);
1257
- if (!option.isPersistent) usedNonPersistentOption = true;
1258
- const max = getMaxArgs(option.args);
1259
- if (max > 0 && !option.requiresSeparator) {
1260
- pending = {
1261
- option,
1262
- argIndex: 0
1263
- };
1264
- pendingFilled = 0;
1265
- pendingMax = max;
1266
- } else pending = null;
1267
- break;
1268
- }
1269
- case "option-arg":
1270
- pending = null;
1271
- break;
1272
- case "arg":
1273
- if (pending && pendingFilled < pendingMax) {
1274
- pendingFilled += 1;
1275
- pending = {
1276
- option: pending.option,
1277
- argIndex: pendingFilled
1278
- };
1279
- if (pendingFilled >= pendingMax) pending = null;
1280
- } else {
1281
- positionalIndex += 1;
1282
- sawPositional = true;
1283
- pending = null;
1284
- }
1285
- break;
1286
- case "arg-separator":
1287
- forceArgs = true;
1288
- pending = null;
1289
- break;
1290
- case "unknown-option": pending = null;
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);
1291
859
  }
860
+ const open = scanned.openOption;
861
+ const pending = open ? {
862
+ option: open.option,
863
+ argIndex: open.valueCount
864
+ } : null;
1292
865
  const availableOptions = [];
1293
866
  const seen = /* @__PURE__ */ new Set();
1294
- for (const map of [finalState.localOptions, finalState.persistentOptions]) for (const option of map.values()) if (!seen.has(option)) {
1295
- seen.add(option);
1296
- availableOptions.push(option);
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
+ }
1297
874
  }
1298
875
  return {
1299
876
  path,
1300
- current,
877
+ current: path[path.length - 1],
1301
878
  availableOptions,
1302
879
  pending,
1303
- positionalIndex,
880
+ positionalIndex: scanned.args.length,
1304
881
  presentNames,
1305
882
  countByOption,
1306
- forceArgs,
1307
- sawPositional,
1308
- usedNonPersistentOption
883
+ forceArgs: scanned.argSeparatorIndex !== -1,
884
+ sawPositional: scanned.args.length > 0
1309
885
  };
1310
886
  }
1311
- /** Does any command on the path enable a sticky parser directive? */
887
+ /** A directive's value in effect (the deepest command that sets it). */
1312
888
  function directiveOnPath(path, key) {
1313
- return path.some((command) => command.parserDirectives?.[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;
1314
894
  }
1315
895
  /** The option-arg separators in effect (the deepest command that sets them). */
1316
896
  function separatorsOnPath(path) {
@@ -1361,21 +941,33 @@ async function argValues(arg, tokens, partial) {
1361
941
  if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
1362
942
  return candidates;
1363
943
  }
1364
- /** Pick the argument being completed at a positional index (honors variadic). */
1365
- function positionalArg(command, index) {
1366
- const args = asArray(command.args);
944
+ /** Pick the argument at an index, reusing a final variadic argument. */
945
+ function argAt(args, index) {
1367
946
  if (args.length === 0) return void 0;
1368
947
  if (index < args.length) return args[index];
1369
948
  const last = args[args.length - 1];
1370
949
  return last?.isVariadic ? last : void 0;
1371
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
+ }
1372
964
  /**
1373
- * Resolve completion candidates for a partial command line, reusing the parser
1374
- * (`analyze`) for context. Dispatches between completing a option's value and
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
1375
967
  * completing a fresh token (subcommand / positional / option).
1376
968
  */
1377
969
  async function completeRuntime(req) {
1378
- const state = analyzeState(req.spec, req.before);
970
+ const state = readLine(req.spec, req.before);
1379
971
  const separators = separatorsOnPath(state.path);
1380
972
  const optionValue = await resolveOptionValue(state, req, separators);
1381
973
  if (optionValue) return optionValue;
@@ -1400,8 +992,12 @@ async function resolveOptionValue(state, req, separators) {
1400
992
  }));
1401
993
  }
1402
994
  if (state.pending) {
1403
- const arg = asArray(state.pending.option.args)[state.pending.argIndex];
1404
- return arg ? argValues(arg, tokens, req.partial) : [];
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) : []];
1405
1001
  }
1406
1002
  return null;
1407
1003
  }
@@ -1409,15 +1005,12 @@ async function resolveOptionValue(state, req, separators) {
1409
1005
  async function freshTokenCandidates(state, req, separators) {
1410
1006
  const candidates = [];
1411
1007
  const tokens = [...req.before];
1412
- const posixNoncompliant = directiveOnPath(state.path, "flagsArePosixNoncompliant");
1413
- const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
1414
- const wantValues = !(req.gateOptions && partialIsOption);
1415
- const wantOptions = !req.gateOptions || partialIsOption || posixNoncompliant;
1008
+ const { wantValues, wantOptions } = wantedCandidates(state, req);
1416
1009
  if (wantValues) {
1417
1010
  const positional = positionalArg(state.current, state.positionalIndex);
1418
1011
  if (positional) candidates.push(...await argValues(positional, tokens, req.partial));
1419
1012
  if (state.forceArgs) return candidates;
1420
- if (!state.sawPositional && !state.usedNonPersistentOption) for (const sub of state.current.subcommands ?? []) {
1013
+ if (!state.sawPositional) for (const sub of state.current.subcommands ?? []) {
1421
1014
  if (sub.hidden) continue;
1422
1015
  const description = firstLine(sub.description);
1423
1016
  for (const token of makeArray(sub.name)) candidates.push({
@@ -1426,10 +1019,13 @@ async function freshTokenCandidates(state, req, separators) {
1426
1019
  });
1427
1020
  }
1428
1021
  } else if (state.forceArgs) return candidates;
1429
- const optionsBlocked = state.sawPositional && directiveOnPath(state.path, "optionsMustPrecedeArguments");
1430
- if (wantOptions && !optionsBlocked) candidates.push(...optionCandidates(state, separators));
1022
+ if (wantOptions) candidates.push(...allowedOptionCandidates(state, separators));
1431
1023
  return candidates;
1432
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
+ }
1433
1029
  /** Eligible option candidates after relationship filtering. */
1434
1030
  function optionCandidates(state, separators) {
1435
1031
  const required = [];
@@ -1504,49 +1100,8 @@ function renderRuntimeShim(spec, shell) {
1504
1100
  ].join("\n");
1505
1101
  }
1506
1102
  /** Generate a completion script for `shell` from a CLI spec. */
1507
- function getCompletion(spec, shell, mode = "runtime") {
1508
- if (mode === "runtime") return renderRuntimeShim(spec, shell);
1509
- const nodes = walk(spec);
1510
- switch (shell) {
1511
- case "bash": return renderBash(spec, nodes);
1512
- case "zsh": return renderZsh(spec, nodes);
1513
- case "fish": return renderFish(spec, nodes);
1514
- }
1515
- }
1516
- /** Resolve the argument a {@link Location} points at, or null if it's stale. */
1517
- function resolveArg(spec, location) {
1518
- let command = spec;
1519
- let ancestorPersistent = [];
1520
- for (const segment of location.path) {
1521
- ancestorPersistent = [...ancestorPersistent, ...(command.options ?? []).filter((option) => option.isPersistent)];
1522
- const child = (command.subcommands ?? []).find((sub) => makeArray(sub.name).includes(segment));
1523
- if (!child) return null;
1524
- command = child;
1525
- }
1526
- let args;
1527
- if (location.option !== void 0) {
1528
- const option = [...ancestorPersistent, ...command.options ?? []].find((candidate) => makeArray(candidate.name).includes(location.option));
1529
- if (!option) return null;
1530
- args = asArray(option.args);
1531
- } else args = asArray(command.args);
1532
- return args[location.argIndex] ?? null;
1533
- }
1534
- /**
1535
- * Run the generator a base64 location points at and return its suggestions as
1536
- * insertable strings, or null if the location is invalid.
1537
- */
1538
- async function generateSuggestions(spec, encoded, tokens) {
1539
- let location;
1540
- try {
1541
- location = decodeLocation(encoded);
1542
- } catch {
1543
- return null;
1544
- }
1545
- const arg = resolveArg(spec, location);
1546
- if (!arg) return null;
1547
- const generator = makeArray(arg.generators)[location.genIndex];
1548
- if (!generator?.custom) return null;
1549
- return (await generator.custom(tokens)).map((suggestion) => typeof suggestion === "string" ? suggestion : makeArray(suggestion.name)[0]);
1103
+ function getCompletion(spec, shell) {
1104
+ return renderRuntimeShim(spec, shell);
1550
1105
  }
1551
1106
  /** A subcommand that prints a shell completion script for the whole CLI. */
1552
1107
  const completion = {
@@ -1557,15 +1112,6 @@ const completion = {
1557
1112
  isOptional: true,
1558
1113
  suggestions: SHELLS
1559
1114
  },
1560
- options: [{
1561
- name: "--runtime",
1562
- description: "Resolve candidates by invoking the CLI (default)",
1563
- exclusiveOn: ["--shell"]
1564
- }, {
1565
- name: "--shell",
1566
- description: "Emit a self-contained script with no per-Tab CLI call",
1567
- exclusiveOn: ["--runtime"]
1568
- }],
1569
1115
  subcommands: [{
1570
1116
  name: "complete",
1571
1117
  description: "Resolve completion candidates for a partial command line",
@@ -1591,31 +1137,8 @@ const completion = {
1591
1137
  if (lines.length > 0) print(lines.join("\n"));
1592
1138
  return 0;
1593
1139
  }
1594
- }, {
1595
- name: "generate-suggestions",
1596
- description: "Print dynamic suggestions for a completion generator",
1597
- hidden: true,
1598
- args: [{ name: "location" }, {
1599
- name: "tokens",
1600
- isVariadic: true,
1601
- isOptional: true
1602
- }],
1603
- async action({ path, args: [encoded, ...tokens] }) {
1604
- const spec = path[0];
1605
- if (encoded === void 0) {
1606
- printActionError("Missing generator location");
1607
- return 1;
1608
- }
1609
- const suggestions = await generateSuggestions(spec, encoded, tokens);
1610
- if (suggestions === null) {
1611
- printActionError(`Unknown generator location '${encoded}'`);
1612
- return 1;
1613
- }
1614
- if (suggestions.length > 0) print(suggestions.join("\n"));
1615
- return 0;
1616
- }
1617
1140
  }],
1618
- action({ path: [spec], args: [shell], options, help }) {
1141
+ action({ path: [spec], args: [shell], help }) {
1619
1142
  if (shell === void 0) {
1620
1143
  print(`Supported shells: ${SHELLS.join(", ")}`);
1621
1144
  return 0;
@@ -1630,7 +1153,7 @@ const completion = {
1630
1153
  })}`);
1631
1154
  return 1;
1632
1155
  }
1633
- print(getCompletion(spec, shell, options.has("--shell") ? "shell" : "runtime"));
1156
+ print(getCompletion(spec, shell));
1634
1157
  return 0;
1635
1158
  }
1636
1159
  };
@@ -1686,8 +1209,9 @@ async function run(spec, args = defaultArgs()) {
1686
1209
  }
1687
1210
  return Math.min(Math.max(exitCode ?? 0, 0), 255);
1688
1211
  }
1212
+ /** Print `Error:` and the given values to stderr, separated by spaces. */
1689
1213
  function printError(...strings) {
1690
- console.error("Error:", ...strings);
1214
+ output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
1691
1215
  }
1692
1216
  function defaultArgs() {
1693
1217
  return globalThis.process?.argv?.slice(2) ?? [];