@optique/core 1.1.0-dev.2087 → 1.1.0-dev.2146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/annotation-state.cjs +26 -26
  2. package/dist/annotation-state.d.cts +133 -1
  3. package/dist/annotation-state.d.ts +133 -1
  4. package/dist/annotations.cjs +2 -2
  5. package/dist/constructs.cjs +873 -73
  6. package/dist/constructs.d.cts +72 -1
  7. package/dist/constructs.d.ts +72 -1
  8. package/dist/constructs.js +808 -9
  9. package/dist/dependency-metadata.cjs +12 -12
  10. package/dist/dependency-metadata.d.cts +34 -3
  11. package/dist/dependency-metadata.d.ts +34 -3
  12. package/dist/dependency-runtime.cjs +37 -13
  13. package/dist/dependency-runtime.d.cts +197 -2
  14. package/dist/dependency-runtime.d.ts +197 -2
  15. package/dist/dependency-runtime.js +22 -1
  16. package/dist/dependency.cjs +7 -7
  17. package/dist/displaywidth.d.cts +12 -0
  18. package/dist/displaywidth.d.ts +12 -0
  19. package/dist/doc.cjs +3 -0
  20. package/dist/doc.js +3 -0
  21. package/dist/execution-context.d.cts +23 -0
  22. package/dist/execution-context.d.ts +23 -0
  23. package/dist/extension.cjs +14 -14
  24. package/dist/facade.cjs +49 -37
  25. package/dist/facade.js +34 -22
  26. package/dist/index.cjs +23 -21
  27. package/dist/index.d.cts +3 -3
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.js +4 -4
  30. package/dist/input-trace.d.cts +2 -1
  31. package/dist/input-trace.d.ts +2 -1
  32. package/dist/internal/annotations.cjs +3 -0
  33. package/dist/internal/annotations.d.cts +47 -5
  34. package/dist/internal/annotations.d.ts +47 -5
  35. package/dist/internal/annotations.js +1 -1
  36. package/dist/internal/command-alias.cjs +16 -0
  37. package/dist/internal/command-alias.js +14 -0
  38. package/dist/internal/dependency.cjs +131 -0
  39. package/dist/internal/dependency.d.cts +311 -2
  40. package/dist/internal/dependency.d.ts +311 -2
  41. package/dist/internal/dependency.js +119 -1
  42. package/dist/internal/parser.cjs +108 -23
  43. package/dist/internal/parser.d.cts +58 -3
  44. package/dist/internal/parser.d.ts +58 -3
  45. package/dist/internal/parser.js +101 -16
  46. package/dist/modifiers.cjs +74 -44
  47. package/dist/modifiers.js +34 -4
  48. package/dist/parser.cjs +11 -11
  49. package/dist/phase2-seed.cjs +2 -2
  50. package/dist/phase2-seed.d.cts +50 -0
  51. package/dist/phase2-seed.d.ts +50 -0
  52. package/dist/primitives.cjs +104 -33
  53. package/dist/primitives.d.cts +10 -0
  54. package/dist/primitives.d.ts +10 -0
  55. package/dist/primitives.js +84 -13
  56. package/dist/suggestion.cjs +72 -2
  57. package/dist/suggestion.d.cts +188 -0
  58. package/dist/suggestion.d.ts +188 -0
  59. package/dist/suggestion.js +71 -3
  60. package/dist/usage-internals.cjs +14 -6
  61. package/dist/usage-internals.js +14 -6
  62. package/dist/usage.cjs +33 -8
  63. package/dist/usage.d.cts +31 -0
  64. package/dist/usage.d.ts +31 -0
  65. package/dist/usage.js +33 -8
  66. package/dist/validate.cjs +1 -0
  67. package/dist/validate.d.cts +99 -0
  68. package/dist/validate.d.ts +99 -0
  69. package/dist/validate.js +1 -1
  70. package/dist/valueparser.cjs +333 -79
  71. package/dist/valueparser.d.cts +197 -1
  72. package/dist/valueparser.d.ts +197 -1
  73. package/dist/valueparser.js +334 -81
  74. package/package.json +19 -4
@@ -1,5 +1,5 @@
1
1
  import { message, optionName, text } from "./message.js";
2
- import { extractCommandNames, extractOptionNames } from "./usage.js";
2
+ import { extractCommandNames, extractOptionNames, isSuggestionHidden } from "./usage.js";
3
3
 
4
4
  //#region src/suggestion.ts
5
5
  /**
@@ -141,6 +141,73 @@ function createSuggestionMessage(suggestions) {
141
141
  return messageParts;
142
142
  }
143
143
  /**
144
+ * Expands command alias suggestions so an alias typo can point at both the
145
+ * canonical command and the alias that matched.
146
+ *
147
+ * @param usage Usage terms that define command aliases.
148
+ * @param suggestions Candidate suggestions returned by {@link findSimilar}.
149
+ * @returns Suggestions with alias hits expanded to canonical name + alias.
150
+ * @internal
151
+ */
152
+ function expandCommandAliasSuggestions(usage, suggestions) {
153
+ if (suggestions.length === 0) return suggestions;
154
+ const commandAliasTargets = collectCommandAliasTargets(usage);
155
+ const expanded = [];
156
+ const seen = /* @__PURE__ */ new Set();
157
+ for (const suggestion of suggestions) {
158
+ const targets = commandAliasTargets.get(suggestion) ?? [suggestion];
159
+ for (const target of targets) {
160
+ if (seen.has(target)) continue;
161
+ seen.add(target);
162
+ expanded.push(target);
163
+ }
164
+ }
165
+ return expanded;
166
+ }
167
+ function collectCommandAliasTargets(usage) {
168
+ const targets = /* @__PURE__ */ new Map();
169
+ function traverse(terms) {
170
+ if (!terms || !Array.isArray(terms)) return true;
171
+ for (const term of terms) {
172
+ if (term.type === "option") continue;
173
+ if (term.type === "argument") return false;
174
+ if (term.type === "command") {
175
+ if (isSuggestionHidden(term.hidden)) return false;
176
+ if (!targets.has(term.name)) targets.set(term.name, [term.name]);
177
+ for (const alias of term.aliases ?? []) if (!targets.has(alias)) targets.set(alias, [term.name, alias]);
178
+ for (const alias of term.hiddenAliases ?? []) if (!targets.has(alias)) targets.set(alias, [term.name]);
179
+ return false;
180
+ }
181
+ if (term.type === "optional") {
182
+ traverse(term.terms);
183
+ continue;
184
+ }
185
+ if (term.type === "multiple") {
186
+ const termsSkippable = traverse(term.terms);
187
+ if (term.min === 0 || termsSkippable) continue;
188
+ return false;
189
+ }
190
+ if (term.type === "sequence") {
191
+ if (traverse(term.terms)) continue;
192
+ return false;
193
+ }
194
+ if (term.type === "exclusive") {
195
+ let anySkippable = false;
196
+ for (const branch of term.terms) {
197
+ const branchSkippable = traverse(branch);
198
+ anySkippable = anySkippable || branchSkippable;
199
+ }
200
+ if (anySkippable) continue;
201
+ return false;
202
+ }
203
+ return false;
204
+ }
205
+ return true;
206
+ }
207
+ traverse(usage);
208
+ return targets;
209
+ }
210
+ /**
144
211
  * Creates an error message with suggestions for similar options or commands.
145
212
  *
146
213
  * This is a convenience function that combines the functionality of
@@ -175,7 +242,8 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
175
242
  if (type === "option" || type === "both") for (const name of extractOptionNames(usage)) candidates.add(name);
176
243
  if (type === "command" || type === "both") for (const name of extractCommandNames(usage)) candidates.add(name);
177
244
  const suggestions = findSimilar(invalidInput, candidates, DEFAULT_FIND_SIMILAR_OPTIONS);
178
- const suggestionMsg = customFormatter ? customFormatter(suggestions) : createSuggestionMessage(suggestions);
245
+ const displaySuggestions = type === "option" ? suggestions : expandCommandAliasSuggestions(usage, suggestions);
246
+ const suggestionMsg = customFormatter ? customFormatter(displaySuggestions) : createSuggestionMessage(displaySuggestions);
179
247
  return suggestionMsg.length > 0 ? [
180
248
  ...baseError,
181
249
  text("\n\n"),
@@ -244,4 +312,4 @@ function deduplicateSuggestions(suggestions) {
244
312
  }
245
313
 
246
314
  //#endregion
247
- export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar };
315
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
@@ -16,31 +16,39 @@ const require_usage = require('./usage.cjs');
16
16
  * @returns `true` if every term in `terms` is skippable (i.e., the caller
17
17
  * may continue scanning the next sibling term), `false` otherwise.
18
18
  */
19
- function collectLeadingCandidates(terms, optionNames, commandNames) {
19
+ function collectLeadingCandidates(terms, optionNames, commandNames, includeHidden = false) {
20
20
  if (!terms || !Array.isArray(terms)) return true;
21
21
  for (const term of terms) {
22
22
  if (term.type === "option") {
23
- if (!require_usage.isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
23
+ if (includeHidden || !require_usage.isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
24
24
  return false;
25
25
  }
26
26
  if (term.type === "command") {
27
- if (!require_usage.isSuggestionHidden(term.hidden)) commandNames.add(term.name);
27
+ if (includeHidden || !require_usage.isSuggestionHidden(term.hidden)) {
28
+ commandNames.add(term.name);
29
+ for (const alias of term.aliases ?? []) commandNames.add(alias);
30
+ for (const alias of term.hiddenAliases ?? []) commandNames.add(alias);
31
+ }
28
32
  return false;
29
33
  }
30
34
  if (term.type === "argument") return false;
31
35
  if (term.type === "optional") {
32
- collectLeadingCandidates(term.terms, optionNames, commandNames);
36
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
33
37
  continue;
34
38
  }
35
39
  if (term.type === "multiple") {
36
- collectLeadingCandidates(term.terms, optionNames, commandNames);
40
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
37
41
  if (term.min === 0) continue;
38
42
  return false;
39
43
  }
44
+ if (term.type === "sequence") {
45
+ if (collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden)) continue;
46
+ return false;
47
+ }
40
48
  if (term.type === "exclusive") {
41
49
  let allSkippable = true;
42
50
  for (const branch of term.terms) {
43
- const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
51
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames, includeHidden);
44
52
  allSkippable = allSkippable && branchSkippable;
45
53
  }
46
54
  if (allSkippable) continue;
@@ -16,31 +16,39 @@ import { isSuggestionHidden } from "./usage.js";
16
16
  * @returns `true` if every term in `terms` is skippable (i.e., the caller
17
17
  * may continue scanning the next sibling term), `false` otherwise.
18
18
  */
19
- function collectLeadingCandidates(terms, optionNames, commandNames) {
19
+ function collectLeadingCandidates(terms, optionNames, commandNames, includeHidden = false) {
20
20
  if (!terms || !Array.isArray(terms)) return true;
21
21
  for (const term of terms) {
22
22
  if (term.type === "option") {
23
- if (!isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
23
+ if (includeHidden || !isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
24
24
  return false;
25
25
  }
26
26
  if (term.type === "command") {
27
- if (!isSuggestionHidden(term.hidden)) commandNames.add(term.name);
27
+ if (includeHidden || !isSuggestionHidden(term.hidden)) {
28
+ commandNames.add(term.name);
29
+ for (const alias of term.aliases ?? []) commandNames.add(alias);
30
+ for (const alias of term.hiddenAliases ?? []) commandNames.add(alias);
31
+ }
28
32
  return false;
29
33
  }
30
34
  if (term.type === "argument") return false;
31
35
  if (term.type === "optional") {
32
- collectLeadingCandidates(term.terms, optionNames, commandNames);
36
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
33
37
  continue;
34
38
  }
35
39
  if (term.type === "multiple") {
36
- collectLeadingCandidates(term.terms, optionNames, commandNames);
40
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
37
41
  if (term.min === 0) continue;
38
42
  return false;
39
43
  }
44
+ if (term.type === "sequence") {
45
+ if (collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden)) continue;
46
+ return false;
47
+ }
40
48
  if (term.type === "exclusive") {
41
49
  let allSkippable = true;
42
50
  for (const branch of term.terms) {
43
- const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
51
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames, includeHidden);
44
52
  allSkippable = allSkippable && branchSkippable;
45
53
  }
46
54
  if (allSkippable) continue;
package/dist/usage.cjs CHANGED
@@ -63,7 +63,7 @@ function extractOptionNames(usage, includeHidden) {
63
63
  for (const term of terms) if (term.type === "option") {
64
64
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
65
65
  for (const name of term.names) names.add(name);
66
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
66
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
67
67
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
68
68
  }
69
69
  traverseUsage(usage);
@@ -98,7 +98,9 @@ function extractCommandNames(usage, includeHidden) {
98
98
  for (const term of terms) if (term.type === "command") {
99
99
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
100
100
  names.add(term.name);
101
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
101
+ for (const alias of term.aliases ?? []) names.add(alias);
102
+ if (includeHidden) for (const alias of term.hiddenAliases ?? []) names.add(alias);
103
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
102
104
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
103
105
  }
104
106
  traverseUsage(usage);
@@ -121,7 +123,7 @@ function extractLiteralValues(usage) {
121
123
  function traverseUsage(terms) {
122
124
  if (!terms || !Array.isArray(terms)) return;
123
125
  for (const term of terms) if (term.type === "literal") values.add(term.value);
124
- else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
126
+ else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
125
127
  else if (term.type === "exclusive") for (const branch of term.terms) traverseUsage(branch);
126
128
  }
127
129
  traverseUsage(usage);
@@ -155,7 +157,7 @@ function extractArgumentMetavars(usage) {
155
157
  for (const term of terms) if (term.type === "argument") {
156
158
  if (isSuggestionHidden(term.hidden)) continue;
157
159
  metavars.add(term.metavar);
158
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
160
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
159
161
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
160
162
  }
161
163
  traverseUsage(usage);
@@ -282,6 +284,10 @@ function normalizeUsageTerm(term) {
282
284
  terms: normalizeUsage(term.terms),
283
285
  min: term.min
284
286
  };
287
+ else if (term.type === "sequence") return {
288
+ type: "sequence",
289
+ terms: term.terms.map(normalizeUsageTerm).filter(isNonDegenerateTerm)
290
+ };
285
291
  else if (term.type === "exclusive") {
286
292
  const terms = [];
287
293
  for (const usage of term.terms) {
@@ -314,7 +320,7 @@ function isNonDegenerateTerm(term) {
314
320
  if (term.type === "option") return term.names.length > 0;
315
321
  if (term.type === "command") return term.name !== "";
316
322
  if (term.type === "argument") return term.metavar.length > 0;
317
- if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive") return term.terms.length > 0;
323
+ if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive" || term.type === "sequence") return term.terms.length > 0;
318
324
  return true;
319
325
  }
320
326
  function containsMalformedLeaf(usage) {
@@ -322,7 +328,7 @@ function containsMalformedLeaf(usage) {
322
328
  if (term.type === "option" && term.names.length === 0) return true;
323
329
  if (term.type === "command" && term.name === "") return true;
324
330
  if (term.type === "argument" && term.metavar.length === 0) return true;
325
- if (term.type === "optional" || term.type === "multiple") {
331
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
326
332
  if (containsMalformedLeaf(term.terms)) return true;
327
333
  }
328
334
  if (term.type === "exclusive") {
@@ -350,9 +356,15 @@ function cloneUsageTerm(term) {
350
356
  names: [...term.names]
351
357
  };
352
358
  case "command": {
353
- if (term.usageLine == null || typeof term.usageLine === "function") return { ...term };
359
+ if (term.usageLine == null || typeof term.usageLine === "function") return {
360
+ ...term,
361
+ ...term.aliases != null ? { aliases: [...term.aliases] } : {},
362
+ ...term.hiddenAliases != null ? { hiddenAliases: [...term.hiddenAliases] } : {}
363
+ };
354
364
  return {
355
365
  ...term,
366
+ ...term.aliases != null ? { aliases: [...term.aliases] } : {},
367
+ ...term.hiddenAliases != null ? { hiddenAliases: [...term.hiddenAliases] } : {},
356
368
  usageLine: term.usageLine.map(cloneUsageTerm)
357
369
  };
358
370
  }
@@ -369,6 +381,10 @@ function cloneUsageTerm(term) {
369
381
  type: "exclusive",
370
382
  terms: term.terms.map((u) => u.map(cloneUsageTerm))
371
383
  };
384
+ case "sequence": return {
385
+ type: "sequence",
386
+ terms: term.terms.map(cloneUsageTerm)
387
+ };
372
388
  case "literal":
373
389
  case "passthrough":
374
390
  case "ellipsis": return { ...term };
@@ -421,6 +437,14 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
421
437
  });
422
438
  continue;
423
439
  }
440
+ if (term.type === "sequence") {
441
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
442
+ if (filtered.length > 0) terms.push({
443
+ type: "sequence",
444
+ terms: filtered
445
+ });
446
+ continue;
447
+ }
424
448
  terms.push(term);
425
449
  }
426
450
  return terms;
@@ -541,7 +565,8 @@ function* formatUsageTermInternal(term, options) {
541
565
  text: options?.colors ? `\x1b[2m)\x1b[0m` : ")",
542
566
  width: 1
543
567
  };
544
- } else if (term.type === "multiple") {
568
+ } else if (term.type === "sequence") yield* formatUsageTerms(term.terms, options);
569
+ else if (term.type === "multiple") {
545
570
  if (term.min < 1) yield {
546
571
  text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
547
572
  width: 1
package/dist/usage.d.cts CHANGED
@@ -104,6 +104,20 @@ type UsageTerm =
104
104
  * in the command-line usage.
105
105
  */
106
106
  readonly name: string;
107
+ /**
108
+ * Additional command names that invoke the same parser.
109
+ * These aliases participate in parsing, completion, and typo
110
+ * suggestions, but are not rendered in usage or documentation output.
111
+ * @since 1.1.0
112
+ */
113
+ readonly aliases?: readonly string[];
114
+ /**
115
+ * Additional command names that invoke the same parser but are not
116
+ * rendered or suggested. They are still available to parsers and
117
+ * suggestion matchers so alias typos can resolve to the canonical command.
118
+ * @since 1.1.0
119
+ */
120
+ readonly hiddenAliases?: readonly string[];
107
121
  /**
108
122
  * Optional usage line override for this command's own help page.
109
123
  * This affects help/documentation rendering only.
@@ -164,6 +178,23 @@ type UsageTerm =
164
178
  */
165
179
  readonly terms: readonly Usage[];
166
180
  }
181
+ /**
182
+ * A sequence term, which preserves the declaration order of its child
183
+ * terms through usage normalization.
184
+ *
185
+ * This is used by ordered parser combinators where argument/command/option
186
+ * order is part of the accepted grammar.
187
+ * @since 1.1.0
188
+ */ | {
189
+ /**
190
+ * The type of the term, which is always `"sequence"` for this term.
191
+ */
192
+ readonly type: "sequence";
193
+ /**
194
+ * Terms that must be displayed in the given order.
195
+ */
196
+ readonly terms: Usage;
197
+ }
167
198
  /**
168
199
  * A literal term, which represents a fixed string value in the command-line
169
200
  * usage. Unlike metavars which are placeholders for user-provided values,
package/dist/usage.d.ts CHANGED
@@ -104,6 +104,20 @@ type UsageTerm =
104
104
  * in the command-line usage.
105
105
  */
106
106
  readonly name: string;
107
+ /**
108
+ * Additional command names that invoke the same parser.
109
+ * These aliases participate in parsing, completion, and typo
110
+ * suggestions, but are not rendered in usage or documentation output.
111
+ * @since 1.1.0
112
+ */
113
+ readonly aliases?: readonly string[];
114
+ /**
115
+ * Additional command names that invoke the same parser but are not
116
+ * rendered or suggested. They are still available to parsers and
117
+ * suggestion matchers so alias typos can resolve to the canonical command.
118
+ * @since 1.1.0
119
+ */
120
+ readonly hiddenAliases?: readonly string[];
107
121
  /**
108
122
  * Optional usage line override for this command's own help page.
109
123
  * This affects help/documentation rendering only.
@@ -164,6 +178,23 @@ type UsageTerm =
164
178
  */
165
179
  readonly terms: readonly Usage[];
166
180
  }
181
+ /**
182
+ * A sequence term, which preserves the declaration order of its child
183
+ * terms through usage normalization.
184
+ *
185
+ * This is used by ordered parser combinators where argument/command/option
186
+ * order is part of the accepted grammar.
187
+ * @since 1.1.0
188
+ */ | {
189
+ /**
190
+ * The type of the term, which is always `"sequence"` for this term.
191
+ */
192
+ readonly type: "sequence";
193
+ /**
194
+ * Terms that must be displayed in the given order.
195
+ */
196
+ readonly terms: Usage;
197
+ }
167
198
  /**
168
199
  * A literal term, which represents a fixed string value in the command-line
169
200
  * usage. Unlike metavars which are placeholders for user-provided values,
package/dist/usage.js CHANGED
@@ -63,7 +63,7 @@ function extractOptionNames(usage, includeHidden) {
63
63
  for (const term of terms) if (term.type === "option") {
64
64
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
65
65
  for (const name of term.names) names.add(name);
66
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
66
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
67
67
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
68
68
  }
69
69
  traverseUsage(usage);
@@ -98,7 +98,9 @@ function extractCommandNames(usage, includeHidden) {
98
98
  for (const term of terms) if (term.type === "command") {
99
99
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
100
100
  names.add(term.name);
101
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
101
+ for (const alias of term.aliases ?? []) names.add(alias);
102
+ if (includeHidden) for (const alias of term.hiddenAliases ?? []) names.add(alias);
103
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
102
104
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
103
105
  }
104
106
  traverseUsage(usage);
@@ -121,7 +123,7 @@ function extractLiteralValues(usage) {
121
123
  function traverseUsage(terms) {
122
124
  if (!terms || !Array.isArray(terms)) return;
123
125
  for (const term of terms) if (term.type === "literal") values.add(term.value);
124
- else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
126
+ else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
125
127
  else if (term.type === "exclusive") for (const branch of term.terms) traverseUsage(branch);
126
128
  }
127
129
  traverseUsage(usage);
@@ -155,7 +157,7 @@ function extractArgumentMetavars(usage) {
155
157
  for (const term of terms) if (term.type === "argument") {
156
158
  if (isSuggestionHidden(term.hidden)) continue;
157
159
  metavars.add(term.metavar);
158
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
160
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
159
161
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
160
162
  }
161
163
  traverseUsage(usage);
@@ -282,6 +284,10 @@ function normalizeUsageTerm(term) {
282
284
  terms: normalizeUsage(term.terms),
283
285
  min: term.min
284
286
  };
287
+ else if (term.type === "sequence") return {
288
+ type: "sequence",
289
+ terms: term.terms.map(normalizeUsageTerm).filter(isNonDegenerateTerm)
290
+ };
285
291
  else if (term.type === "exclusive") {
286
292
  const terms = [];
287
293
  for (const usage of term.terms) {
@@ -314,7 +320,7 @@ function isNonDegenerateTerm(term) {
314
320
  if (term.type === "option") return term.names.length > 0;
315
321
  if (term.type === "command") return term.name !== "";
316
322
  if (term.type === "argument") return term.metavar.length > 0;
317
- if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive") return term.terms.length > 0;
323
+ if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive" || term.type === "sequence") return term.terms.length > 0;
318
324
  return true;
319
325
  }
320
326
  function containsMalformedLeaf(usage) {
@@ -322,7 +328,7 @@ function containsMalformedLeaf(usage) {
322
328
  if (term.type === "option" && term.names.length === 0) return true;
323
329
  if (term.type === "command" && term.name === "") return true;
324
330
  if (term.type === "argument" && term.metavar.length === 0) return true;
325
- if (term.type === "optional" || term.type === "multiple") {
331
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
326
332
  if (containsMalformedLeaf(term.terms)) return true;
327
333
  }
328
334
  if (term.type === "exclusive") {
@@ -350,9 +356,15 @@ function cloneUsageTerm(term) {
350
356
  names: [...term.names]
351
357
  };
352
358
  case "command": {
353
- if (term.usageLine == null || typeof term.usageLine === "function") return { ...term };
359
+ if (term.usageLine == null || typeof term.usageLine === "function") return {
360
+ ...term,
361
+ ...term.aliases != null ? { aliases: [...term.aliases] } : {},
362
+ ...term.hiddenAliases != null ? { hiddenAliases: [...term.hiddenAliases] } : {}
363
+ };
354
364
  return {
355
365
  ...term,
366
+ ...term.aliases != null ? { aliases: [...term.aliases] } : {},
367
+ ...term.hiddenAliases != null ? { hiddenAliases: [...term.hiddenAliases] } : {},
356
368
  usageLine: term.usageLine.map(cloneUsageTerm)
357
369
  };
358
370
  }
@@ -369,6 +381,10 @@ function cloneUsageTerm(term) {
369
381
  type: "exclusive",
370
382
  terms: term.terms.map((u) => u.map(cloneUsageTerm))
371
383
  };
384
+ case "sequence": return {
385
+ type: "sequence",
386
+ terms: term.terms.map(cloneUsageTerm)
387
+ };
372
388
  case "literal":
373
389
  case "passthrough":
374
390
  case "ellipsis": return { ...term };
@@ -421,6 +437,14 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
421
437
  });
422
438
  continue;
423
439
  }
440
+ if (term.type === "sequence") {
441
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
442
+ if (filtered.length > 0) terms.push({
443
+ type: "sequence",
444
+ terms: filtered
445
+ });
446
+ continue;
447
+ }
424
448
  terms.push(term);
425
449
  }
426
450
  return terms;
@@ -541,7 +565,8 @@ function* formatUsageTermInternal(term, options) {
541
565
  text: options?.colors ? `\x1b[2m)\x1b[0m` : ")",
542
566
  width: 1
543
567
  };
544
- } else if (term.type === "multiple") {
568
+ } else if (term.type === "sequence") yield* formatUsageTerms(term.terms, options);
569
+ else if (term.type === "multiple") {
545
570
  if (term.min < 1) yield {
546
571
  text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
547
572
  width: 1
package/dist/validate.cjs CHANGED
@@ -162,6 +162,7 @@ function validateContextIds(contexts) {
162
162
  }
163
163
 
164
164
  //#endregion
165
+ exports.escapeControlChars = escapeControlChars;
165
166
  exports.validateCommandNames = validateCommandNames;
166
167
  exports.validateContextIds = validateContextIds;
167
168
  exports.validateLabel = validateLabel;
@@ -0,0 +1,99 @@
1
+ //#region src/validate.d.ts
2
+ /**
3
+ * Escapes control characters in a string for readable error messages.
4
+ *
5
+ * @param value The string to escape.
6
+ * @returns The escaped string with control characters replaced by escape
7
+ * sequences.
8
+ */
9
+ declare function escapeControlChars(value: string): string;
10
+ /**
11
+ * Validates option names at runtime.
12
+ *
13
+ * @param names The option names to validate.
14
+ * @param label A human-readable label for error messages (e.g.,
15
+ * `"Option"`, `"Flag"`, `"Help option"`).
16
+ * @throws {TypeError} If the names array is empty, or any name is empty,
17
+ * lacks a valid prefix, or contains whitespace or control characters.
18
+ */
19
+ declare function validateOptionNames(names: readonly string[], label: string): void;
20
+ /**
21
+ * Validates command names at runtime.
22
+ *
23
+ * @param names The command names to validate.
24
+ * @param label A human-readable label for error messages (e.g.,
25
+ * `"Help command"`).
26
+ * @throws {TypeError} If the names array is empty, or any name is empty,
27
+ * whitespace-only, or contains whitespace or control characters.
28
+ */
29
+ declare function validateCommandNames(names: readonly string[], label: string): void;
30
+ /**
31
+ * A meta entry describes one active meta feature for collision checking.
32
+ *
33
+ * The tuple elements are:
34
+ *
35
+ * 1. `kind` — `"command"` if this meta feature matches at `args[0]` only,
36
+ * or `"option"` if a lenient scanner matches the name anywhere in `argv`.
37
+ * 2. `label` — human-readable label for error messages (e.g., `"help option"`).
38
+ * 3. `names` — the configured name(s) for this meta feature.
39
+ * 4. `prefixMatch` — when `true`, the runtime also intercepts tokens
40
+ * starting with `name=` (e.g., `--completion=bash`). Only the
41
+ * completion option uses this form; help/version use exact matching.
42
+ *
43
+ * @since 1.0.0
44
+ */
45
+ type MetaEntry = readonly [kind: "command" | "option", label: string, names: readonly string[], prefixMatch?: boolean];
46
+ /**
47
+ * Validates that there are no name collisions among active meta features
48
+ * (help, version, completion).
49
+ *
50
+ * User parser names are accepted even when they overlap with meta names.
51
+ * Runtime parsing resolves those cases parser-first so ordinary parser data
52
+ * can shadow built-in meta behavior.
53
+ *
54
+ * Meta-vs-meta collisions are always checked in a unified namespace,
55
+ * because a meta command named `"--help"` and a meta option named
56
+ * `"--help"` both compete for the same token.
57
+ *
58
+ * @param metaEntries Active meta feature entries annotated with their kind.
59
+ * @throws {TypeError} If any meta/meta collision or duplicate is detected.
60
+ * @since 1.0.0
61
+ */
62
+ declare function validateMetaNameCollisions(metaEntries: readonly MetaEntry[]): void;
63
+ /**
64
+ * Validates a program name at runtime.
65
+ *
66
+ * Program names may contain spaces (e.g., file paths), but must not be empty,
67
+ * whitespace-only, or contain control characters.
68
+ *
69
+ * @param programName The program name to validate.
70
+ * @throws {TypeError} If the value is not a string, is empty,
71
+ * whitespace-only, or contains control characters.
72
+ */
73
+ declare function validateProgramName(programName: string): void;
74
+ /**
75
+ * Validates a label at runtime.
76
+ *
77
+ * Labels are used as section titles in documentation output. They may contain
78
+ * spaces (e.g., "Connection options"), but must not be empty, whitespace-only,
79
+ * or contain control characters.
80
+ *
81
+ * @param label The label to validate.
82
+ * @throws {TypeError} If the label is not a string, is empty,
83
+ * whitespace-only, or contains control characters.
84
+ * @since 1.0.0
85
+ */
86
+ declare function validateLabel(label: string): void;
87
+ /**
88
+ * Validates that all source contexts have unique
89
+ * {@link import("./context.ts").SourceContext.id | id} values.
90
+ *
91
+ * @param contexts The source contexts to validate.
92
+ * @throws {TypeError} If two or more contexts share the same id.
93
+ * @since 1.0.0
94
+ */
95
+ declare function validateContextIds(contexts: readonly {
96
+ readonly id: symbol;
97
+ }[]): void;
98
+ //#endregion
99
+ export { MetaEntry, escapeControlChars, validateCommandNames, validateContextIds, validateLabel, validateMetaNameCollisions, validateOptionNames, validateProgramName };