@optique/core 1.0.0-dev.400 → 1.0.0-dev.407
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/constructs.cjs +13 -43
- package/dist/constructs.js +12 -42
- package/dist/doc.cjs +32 -1
- package/dist/doc.d.cts +22 -0
- package/dist/doc.d.ts +22 -0
- package/dist/doc.js +32 -1
- package/dist/facade.cjs +11 -9
- package/dist/facade.d.cts +15 -1
- package/dist/facade.d.ts +15 -1
- package/dist/facade.js +11 -9
- package/dist/primitives.cjs +9 -4
- package/dist/primitives.js +12 -7
- package/dist/usage-internals.cjs +73 -0
- package/dist/usage-internals.js +71 -0
- package/package.json +1 -1
package/dist/constructs.cjs
CHANGED
|
@@ -3,50 +3,13 @@ const require_dependency = require('./dependency.cjs');
|
|
|
3
3
|
const require_mode_dispatch = require('./mode-dispatch.cjs');
|
|
4
4
|
const require_usage = require('./usage.cjs');
|
|
5
5
|
const require_suggestion = require('./suggestion.cjs');
|
|
6
|
+
const require_usage_internals = require('./usage-internals.cjs');
|
|
6
7
|
|
|
7
8
|
//#region src/constructs.ts
|
|
8
|
-
/**
|
|
9
|
-
* Collects option names and command names that are valid at the current
|
|
10
|
-
* parse position by walking the usage tree. Only "leading" candidates
|
|
11
|
-
* (those reachable before a required positional argument) are collected.
|
|
12
|
-
*/
|
|
13
|
-
function collectLeadingCandidates(terms, optionNames, commandNames) {
|
|
14
|
-
if (!terms || !Array.isArray(terms)) return true;
|
|
15
|
-
for (const term of terms) {
|
|
16
|
-
if (term.type === "option") {
|
|
17
|
-
for (const name of term.names) optionNames.add(name);
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
if (term.type === "command") {
|
|
21
|
-
commandNames.add(term.name);
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
if (term.type === "argument") return false;
|
|
25
|
-
if (term.type === "optional") {
|
|
26
|
-
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (term.type === "multiple") {
|
|
30
|
-
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
31
|
-
if (term.min === 0) continue;
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
if (term.type === "exclusive") {
|
|
35
|
-
let allAlternativesSkippable = true;
|
|
36
|
-
for (const exclusiveUsage of term.terms) {
|
|
37
|
-
const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
|
|
38
|
-
allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
|
|
39
|
-
}
|
|
40
|
-
if (allAlternativesSkippable) continue;
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
9
|
function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
|
|
47
10
|
const options = /* @__PURE__ */ new Set();
|
|
48
11
|
const commands = /* @__PURE__ */ new Set();
|
|
49
|
-
for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
|
|
12
|
+
for (const parser of parsers) require_usage_internals.collectLeadingCandidates(parser.usage, options, commands);
|
|
50
13
|
const candidates = new Set([...options, ...commands]);
|
|
51
14
|
const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
|
|
52
15
|
const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
|
|
@@ -2006,7 +1969,7 @@ function group(label, parser) {
|
|
|
2006
1969
|
complete: (state) => parser.complete(state),
|
|
2007
1970
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
2008
1971
|
getDocFragments: (state, defaultValue) => {
|
|
2009
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1972
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
2010
1973
|
const allEntries = [];
|
|
2011
1974
|
const titledSections = [];
|
|
2012
1975
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
@@ -2016,15 +1979,22 @@ function group(label, parser) {
|
|
|
2016
1979
|
kind: "available",
|
|
2017
1980
|
state: parser.initialState
|
|
2018
1981
|
}, void 0);
|
|
2019
|
-
const
|
|
2020
|
-
const
|
|
2021
|
-
|
|
1982
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1983
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1984
|
+
else if (f.type === "section") {
|
|
1985
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1986
|
+
}
|
|
1987
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1988
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1989
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
2022
1990
|
const labeledSection = applyLabel ? {
|
|
2023
1991
|
title: label,
|
|
2024
1992
|
entries: allEntries
|
|
2025
1993
|
} : { entries: allEntries };
|
|
2026
1994
|
return {
|
|
1995
|
+
brief,
|
|
2027
1996
|
description,
|
|
1997
|
+
footer,
|
|
2028
1998
|
fragments: [...titledSections.map((s) => ({
|
|
2029
1999
|
...s,
|
|
2030
2000
|
type: "section"
|
package/dist/constructs.js
CHANGED
|
@@ -3,46 +3,9 @@ import { DependencyRegistry, dependencyId, isDeferredParseState, isDependencySou
|
|
|
3
3
|
import { dispatchByMode, dispatchIterableByMode } from "./mode-dispatch.js";
|
|
4
4
|
import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
|
|
5
5
|
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
|
|
6
|
+
import { collectLeadingCandidates } from "./usage-internals.js";
|
|
6
7
|
|
|
7
8
|
//#region src/constructs.ts
|
|
8
|
-
/**
|
|
9
|
-
* Collects option names and command names that are valid at the current
|
|
10
|
-
* parse position by walking the usage tree. Only "leading" candidates
|
|
11
|
-
* (those reachable before a required positional argument) are collected.
|
|
12
|
-
*/
|
|
13
|
-
function collectLeadingCandidates(terms, optionNames, commandNames) {
|
|
14
|
-
if (!terms || !Array.isArray(terms)) return true;
|
|
15
|
-
for (const term of terms) {
|
|
16
|
-
if (term.type === "option") {
|
|
17
|
-
for (const name of term.names) optionNames.add(name);
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
if (term.type === "command") {
|
|
21
|
-
commandNames.add(term.name);
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
if (term.type === "argument") return false;
|
|
25
|
-
if (term.type === "optional") {
|
|
26
|
-
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (term.type === "multiple") {
|
|
30
|
-
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
31
|
-
if (term.min === 0) continue;
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
if (term.type === "exclusive") {
|
|
35
|
-
let allAlternativesSkippable = true;
|
|
36
|
-
for (const exclusiveUsage of term.terms) {
|
|
37
|
-
const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
|
|
38
|
-
allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
|
|
39
|
-
}
|
|
40
|
-
if (allAlternativesSkippable) continue;
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
9
|
function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
|
|
47
10
|
const options = /* @__PURE__ */ new Set();
|
|
48
11
|
const commands = /* @__PURE__ */ new Set();
|
|
@@ -2006,7 +1969,7 @@ function group(label, parser) {
|
|
|
2006
1969
|
complete: (state) => parser.complete(state),
|
|
2007
1970
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
2008
1971
|
getDocFragments: (state, defaultValue) => {
|
|
2009
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1972
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
2010
1973
|
const allEntries = [];
|
|
2011
1974
|
const titledSections = [];
|
|
2012
1975
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
@@ -2016,15 +1979,22 @@ function group(label, parser) {
|
|
|
2016
1979
|
kind: "available",
|
|
2017
1980
|
state: parser.initialState
|
|
2018
1981
|
}, void 0);
|
|
2019
|
-
const
|
|
2020
|
-
const
|
|
2021
|
-
|
|
1982
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1983
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1984
|
+
else if (f.type === "section") {
|
|
1985
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1986
|
+
}
|
|
1987
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1988
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1989
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
2022
1990
|
const labeledSection = applyLabel ? {
|
|
2023
1991
|
title: label,
|
|
2024
1992
|
entries: allEntries
|
|
2025
1993
|
} : { entries: allEntries };
|
|
2026
1994
|
return {
|
|
1995
|
+
brief,
|
|
2027
1996
|
description,
|
|
1997
|
+
footer,
|
|
2028
1998
|
fragments: [...titledSections.map((s) => ({
|
|
2029
1999
|
...s,
|
|
2030
2000
|
type: "section"
|
package/dist/doc.cjs
CHANGED
|
@@ -3,6 +3,28 @@ const require_usage = require('./usage.cjs');
|
|
|
3
3
|
|
|
4
4
|
//#region src/doc.ts
|
|
5
5
|
/**
|
|
6
|
+
* Classifies a {@link DocSection} by its content type for use in the
|
|
7
|
+
* default smart sort.
|
|
8
|
+
*
|
|
9
|
+
* @returns `0` for command-only sections, `1` for mixed sections, `2` for
|
|
10
|
+
* option/argument/passthrough-only sections.
|
|
11
|
+
*/
|
|
12
|
+
function classifySection(section) {
|
|
13
|
+
const hasCommand = section.entries.some((e) => e.term.type === "command");
|
|
14
|
+
const hasNonCommand = section.entries.some((e) => e.term.type !== "command");
|
|
15
|
+
if (hasCommand && !hasNonCommand) return 0;
|
|
16
|
+
if (hasCommand && hasNonCommand) return 1;
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The default section comparator: command-only sections come first, then
|
|
21
|
+
* mixed sections, then option/argument-only sections. Sections with the
|
|
22
|
+
* same score preserve their original relative order (stable sort).
|
|
23
|
+
*/
|
|
24
|
+
function defaultSectionOrder(a, b) {
|
|
25
|
+
return classifySection(a) - classifySection(b);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
6
28
|
* Formats a documentation page into a human-readable string.
|
|
7
29
|
*
|
|
8
30
|
* This function takes a structured {@link DocPage} and converts it into
|
|
@@ -64,7 +86,16 @@ function formatDocPage(programName, page, options = {}) {
|
|
|
64
86
|
});
|
|
65
87
|
output += "\n";
|
|
66
88
|
}
|
|
67
|
-
const
|
|
89
|
+
const comparator = options.sectionOrder ?? defaultSectionOrder;
|
|
90
|
+
const sections = page.sections.map((s, i) => ({
|
|
91
|
+
section: s,
|
|
92
|
+
index: i
|
|
93
|
+
})).toSorted((a, b) => {
|
|
94
|
+
const cmp = comparator(a.section, b.section);
|
|
95
|
+
if (cmp !== 0) return cmp;
|
|
96
|
+
const titleCmp = (a.section.title == null ? 0 : 1) - (b.section.title == null ? 0 : 1);
|
|
97
|
+
return titleCmp !== 0 ? titleCmp : a.index - b.index;
|
|
98
|
+
}).map(({ section }) => section);
|
|
68
99
|
for (const section of sections) {
|
|
69
100
|
if (section.entries.length < 1) continue;
|
|
70
101
|
output += "\n";
|
package/dist/doc.d.cts
CHANGED
|
@@ -228,6 +228,28 @@ interface DocPageFormatOptions {
|
|
|
228
228
|
* ```
|
|
229
229
|
*/
|
|
230
230
|
showChoices?: boolean | ShowChoicesOptions;
|
|
231
|
+
/**
|
|
232
|
+
* A custom comparator function to control the order of sections in the
|
|
233
|
+
* help output. When provided, it is used instead of the default smart
|
|
234
|
+
* sort (command-only sections first, then mixed, then option/argument-only
|
|
235
|
+
* sections). Sections that compare equal (return `0`) preserve their
|
|
236
|
+
* original relative order (stable sort).
|
|
237
|
+
*
|
|
238
|
+
* @param a The first section to compare.
|
|
239
|
+
* @param b The second section to compare.
|
|
240
|
+
* @returns A negative number if `a` should appear before `b`, a positive
|
|
241
|
+
* number if `a` should appear after `b`, or `0` if they are equal.
|
|
242
|
+
* @since 1.0.0
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* // Sort sections alphabetically by title
|
|
247
|
+
* {
|
|
248
|
+
* sectionOrder: (a, b) => (a.title ?? "").localeCompare(b.title ?? "")
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
sectionOrder?: (a: DocSection, b: DocSection) => number;
|
|
231
253
|
}
|
|
232
254
|
/**
|
|
233
255
|
* Formats a documentation page into a human-readable string.
|
package/dist/doc.d.ts
CHANGED
|
@@ -228,6 +228,28 @@ interface DocPageFormatOptions {
|
|
|
228
228
|
* ```
|
|
229
229
|
*/
|
|
230
230
|
showChoices?: boolean | ShowChoicesOptions;
|
|
231
|
+
/**
|
|
232
|
+
* A custom comparator function to control the order of sections in the
|
|
233
|
+
* help output. When provided, it is used instead of the default smart
|
|
234
|
+
* sort (command-only sections first, then mixed, then option/argument-only
|
|
235
|
+
* sections). Sections that compare equal (return `0`) preserve their
|
|
236
|
+
* original relative order (stable sort).
|
|
237
|
+
*
|
|
238
|
+
* @param a The first section to compare.
|
|
239
|
+
* @param b The second section to compare.
|
|
240
|
+
* @returns A negative number if `a` should appear before `b`, a positive
|
|
241
|
+
* number if `a` should appear after `b`, or `0` if they are equal.
|
|
242
|
+
* @since 1.0.0
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* // Sort sections alphabetically by title
|
|
247
|
+
* {
|
|
248
|
+
* sectionOrder: (a, b) => (a.title ?? "").localeCompare(b.title ?? "")
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
sectionOrder?: (a: DocSection, b: DocSection) => number;
|
|
231
253
|
}
|
|
232
254
|
/**
|
|
233
255
|
* Formats a documentation page into a human-readable string.
|
package/dist/doc.js
CHANGED
|
@@ -3,6 +3,28 @@ import { formatUsage, formatUsageTerm } from "./usage.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/doc.ts
|
|
5
5
|
/**
|
|
6
|
+
* Classifies a {@link DocSection} by its content type for use in the
|
|
7
|
+
* default smart sort.
|
|
8
|
+
*
|
|
9
|
+
* @returns `0` for command-only sections, `1` for mixed sections, `2` for
|
|
10
|
+
* option/argument/passthrough-only sections.
|
|
11
|
+
*/
|
|
12
|
+
function classifySection(section) {
|
|
13
|
+
const hasCommand = section.entries.some((e) => e.term.type === "command");
|
|
14
|
+
const hasNonCommand = section.entries.some((e) => e.term.type !== "command");
|
|
15
|
+
if (hasCommand && !hasNonCommand) return 0;
|
|
16
|
+
if (hasCommand && hasNonCommand) return 1;
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The default section comparator: command-only sections come first, then
|
|
21
|
+
* mixed sections, then option/argument-only sections. Sections with the
|
|
22
|
+
* same score preserve their original relative order (stable sort).
|
|
23
|
+
*/
|
|
24
|
+
function defaultSectionOrder(a, b) {
|
|
25
|
+
return classifySection(a) - classifySection(b);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
6
28
|
* Formats a documentation page into a human-readable string.
|
|
7
29
|
*
|
|
8
30
|
* This function takes a structured {@link DocPage} and converts it into
|
|
@@ -64,7 +86,16 @@ function formatDocPage(programName, page, options = {}) {
|
|
|
64
86
|
});
|
|
65
87
|
output += "\n";
|
|
66
88
|
}
|
|
67
|
-
const
|
|
89
|
+
const comparator = options.sectionOrder ?? defaultSectionOrder;
|
|
90
|
+
const sections = page.sections.map((s, i) => ({
|
|
91
|
+
section: s,
|
|
92
|
+
index: i
|
|
93
|
+
})).toSorted((a, b) => {
|
|
94
|
+
const cmp = comparator(a.section, b.section);
|
|
95
|
+
if (cmp !== 0) return cmp;
|
|
96
|
+
const titleCmp = (a.section.title == null ? 0 : 1) - (b.section.title == null ? 0 : 1);
|
|
97
|
+
return titleCmp !== 0 ? titleCmp : a.index - b.index;
|
|
98
|
+
}).map(({ section }) => section);
|
|
68
99
|
for (const section of sections) {
|
|
69
100
|
if (section.entries.length < 1) continue;
|
|
70
101
|
output += "\n";
|
package/dist/facade.cjs
CHANGED
|
@@ -369,7 +369,7 @@ function classifyResult(result, args) {
|
|
|
369
369
|
* Handles shell completion requests.
|
|
370
370
|
* @since 0.6.0
|
|
371
371
|
*/
|
|
372
|
-
function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName) {
|
|
372
|
+
function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder) {
|
|
373
373
|
const shellName = completionArgs[0] || "";
|
|
374
374
|
const args = completionArgs.slice(1);
|
|
375
375
|
const callOnError = (code) => {
|
|
@@ -392,7 +392,8 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
392
392
|
const doc = require_parser.getDocPage(completionParser, ["completion"]);
|
|
393
393
|
if (doc) stderr(require_doc.formatDocPage(programName, doc, {
|
|
394
394
|
colors,
|
|
395
|
-
maxWidth
|
|
395
|
+
maxWidth,
|
|
396
|
+
sectionOrder
|
|
396
397
|
}));
|
|
397
398
|
}
|
|
398
399
|
if (parser.$mode === "async") return Promise.resolve(callOnError(1));
|
|
@@ -457,7 +458,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
457
458
|
args = argsOrOptions;
|
|
458
459
|
options = optionsParam ?? {};
|
|
459
460
|
}
|
|
460
|
-
const { colors, maxWidth, showDefault, showChoices, aboveError = "usage", onError = () => {
|
|
461
|
+
const { colors, maxWidth, showDefault, showChoices, sectionOrder, aboveError = "usage", onError = () => {
|
|
461
462
|
throw new RunParserError("Failed to parse command line arguments.");
|
|
462
463
|
}, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
|
|
463
464
|
const helpMode = options.help?.mode ?? "option";
|
|
@@ -500,7 +501,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
500
501
|
} : createCompletionParser(completion, programName, availableShells, completionName, completionHelpVisibility);
|
|
501
502
|
if (options.completion) {
|
|
502
503
|
const hasHelpOption = args.includes("--help");
|
|
503
|
-
if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && ((completionName === "singular" || completionName === "both" ? args[0] === "completion" : false) || (completionName === "plural" || completionName === "both" ? args[0] === "completions" : false)) && !hasHelpOption) return handleCompletion(args.slice(1), programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
504
|
+
if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && ((completionName === "singular" || completionName === "both" ? args[0] === "completion" : false) || (completionName === "plural" || completionName === "both" ? args[0] === "completions" : false)) && !hasHelpOption) return handleCompletion(args.slice(1), programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
504
505
|
if (completionMode === "option" || completionMode === "both") for (let i = 0; i < args.length; i++) {
|
|
505
506
|
const arg = args[i];
|
|
506
507
|
const singularMatch = completionName === "singular" || completionName === "both" ? arg.startsWith("--completion=") : false;
|
|
@@ -508,14 +509,14 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
508
509
|
if (singularMatch || pluralMatch) {
|
|
509
510
|
const shell = arg.slice(arg.indexOf("=") + 1);
|
|
510
511
|
const completionArgs = args.slice(i + 1);
|
|
511
|
-
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
512
|
+
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
512
513
|
} else {
|
|
513
514
|
const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
|
|
514
515
|
const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
|
|
515
516
|
if (singularMatchExact || pluralMatchExact) {
|
|
516
517
|
const shell = i + 1 < args.length ? args[i + 1] : "";
|
|
517
518
|
const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
|
|
518
|
-
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
519
|
+
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
519
520
|
}
|
|
520
521
|
}
|
|
521
522
|
}
|
|
@@ -625,8 +626,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
625
626
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
626
627
|
const augmentedDoc = {
|
|
627
628
|
...doc,
|
|
628
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
629
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
629
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
630
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
630
631
|
examples: isTopLevel && !isMetaCommandHelp ? examples ?? doc.examples : void 0,
|
|
631
632
|
author: isTopLevel && !isMetaCommandHelp ? author ?? doc.author : void 0,
|
|
632
633
|
bugs: isTopLevel && !isMetaCommandHelp ? bugs ?? doc.bugs : void 0,
|
|
@@ -636,7 +637,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
636
637
|
colors,
|
|
637
638
|
maxWidth,
|
|
638
639
|
showDefault,
|
|
639
|
-
showChoices
|
|
640
|
+
showChoices,
|
|
641
|
+
sectionOrder
|
|
640
642
|
}));
|
|
641
643
|
}
|
|
642
644
|
try {
|
package/dist/facade.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message } from "./message.cjs";
|
|
2
|
-
import { ShowChoicesOptions, ShowDefaultOptions } from "./doc.cjs";
|
|
2
|
+
import { DocSection, ShowChoicesOptions, ShowDefaultOptions } from "./doc.cjs";
|
|
3
3
|
import { InferMode, InferValue, Mode, ModeValue, Parser } from "./parser.cjs";
|
|
4
4
|
import { ShellCompletion } from "./completion.cjs";
|
|
5
5
|
import { ParserValuePlaceholder, SourceContext } from "./context.cjs";
|
|
@@ -148,6 +148,20 @@ interface RunOptions<THelp, TError> {
|
|
|
148
148
|
* @since 0.10.0
|
|
149
149
|
*/
|
|
150
150
|
readonly showChoices?: boolean | ShowChoicesOptions;
|
|
151
|
+
/**
|
|
152
|
+
* A custom comparator function to control the order of sections in the
|
|
153
|
+
* help output. When provided, it is used instead of the default smart
|
|
154
|
+
* sort (command-only sections first, then mixed, then option/argument-only
|
|
155
|
+
* sections). Sections that compare equal (return `0`) preserve their
|
|
156
|
+
* original relative order.
|
|
157
|
+
*
|
|
158
|
+
* @param a The first section to compare.
|
|
159
|
+
* @param b The second section to compare.
|
|
160
|
+
* @returns A negative number if `a` should appear before `b`, a positive
|
|
161
|
+
* number if `a` should appear after `b`, or `0` if they are equal.
|
|
162
|
+
* @since 1.0.0
|
|
163
|
+
*/
|
|
164
|
+
readonly sectionOrder?: (a: DocSection, b: DocSection) => number;
|
|
151
165
|
/**
|
|
152
166
|
* Help configuration. When provided, enables help functionality.
|
|
153
167
|
*/
|
package/dist/facade.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message } from "./message.js";
|
|
2
|
-
import { ShowChoicesOptions, ShowDefaultOptions } from "./doc.js";
|
|
2
|
+
import { DocSection, ShowChoicesOptions, ShowDefaultOptions } from "./doc.js";
|
|
3
3
|
import { InferMode, InferValue, Mode, ModeValue, Parser } from "./parser.js";
|
|
4
4
|
import { ShellCompletion } from "./completion.js";
|
|
5
5
|
import { ParserValuePlaceholder, SourceContext } from "./context.js";
|
|
@@ -148,6 +148,20 @@ interface RunOptions<THelp, TError> {
|
|
|
148
148
|
* @since 0.10.0
|
|
149
149
|
*/
|
|
150
150
|
readonly showChoices?: boolean | ShowChoicesOptions;
|
|
151
|
+
/**
|
|
152
|
+
* A custom comparator function to control the order of sections in the
|
|
153
|
+
* help output. When provided, it is used instead of the default smart
|
|
154
|
+
* sort (command-only sections first, then mixed, then option/argument-only
|
|
155
|
+
* sections). Sections that compare equal (return `0`) preserve their
|
|
156
|
+
* original relative order.
|
|
157
|
+
*
|
|
158
|
+
* @param a The first section to compare.
|
|
159
|
+
* @param b The second section to compare.
|
|
160
|
+
* @returns A negative number if `a` should appear before `b`, a positive
|
|
161
|
+
* number if `a` should appear after `b`, or `0` if they are equal.
|
|
162
|
+
* @since 1.0.0
|
|
163
|
+
*/
|
|
164
|
+
readonly sectionOrder?: (a: DocSection, b: DocSection) => number;
|
|
151
165
|
/**
|
|
152
166
|
* Help configuration. When provided, enables help functionality.
|
|
153
167
|
*/
|
package/dist/facade.js
CHANGED
|
@@ -369,7 +369,7 @@ function classifyResult(result, args) {
|
|
|
369
369
|
* Handles shell completion requests.
|
|
370
370
|
* @since 0.6.0
|
|
371
371
|
*/
|
|
372
|
-
function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName) {
|
|
372
|
+
function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder) {
|
|
373
373
|
const shellName = completionArgs[0] || "";
|
|
374
374
|
const args = completionArgs.slice(1);
|
|
375
375
|
const callOnError = (code) => {
|
|
@@ -392,7 +392,8 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
392
392
|
const doc = getDocPage(completionParser, ["completion"]);
|
|
393
393
|
if (doc) stderr(formatDocPage(programName, doc, {
|
|
394
394
|
colors,
|
|
395
|
-
maxWidth
|
|
395
|
+
maxWidth,
|
|
396
|
+
sectionOrder
|
|
396
397
|
}));
|
|
397
398
|
}
|
|
398
399
|
if (parser.$mode === "async") return Promise.resolve(callOnError(1));
|
|
@@ -457,7 +458,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
457
458
|
args = argsOrOptions;
|
|
458
459
|
options = optionsParam ?? {};
|
|
459
460
|
}
|
|
460
|
-
const { colors, maxWidth, showDefault, showChoices, aboveError = "usage", onError = () => {
|
|
461
|
+
const { colors, maxWidth, showDefault, showChoices, sectionOrder, aboveError = "usage", onError = () => {
|
|
461
462
|
throw new RunParserError("Failed to parse command line arguments.");
|
|
462
463
|
}, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
|
|
463
464
|
const helpMode = options.help?.mode ?? "option";
|
|
@@ -500,7 +501,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
500
501
|
} : createCompletionParser(completion, programName, availableShells, completionName, completionHelpVisibility);
|
|
501
502
|
if (options.completion) {
|
|
502
503
|
const hasHelpOption = args.includes("--help");
|
|
503
|
-
if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && ((completionName === "singular" || completionName === "both" ? args[0] === "completion" : false) || (completionName === "plural" || completionName === "both" ? args[0] === "completions" : false)) && !hasHelpOption) return handleCompletion(args.slice(1), programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
504
|
+
if ((completionMode === "command" || completionMode === "both") && args.length >= 1 && ((completionName === "singular" || completionName === "both" ? args[0] === "completion" : false) || (completionName === "plural" || completionName === "both" ? args[0] === "completions" : false)) && !hasHelpOption) return handleCompletion(args.slice(1), programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
504
505
|
if (completionMode === "option" || completionMode === "both") for (let i = 0; i < args.length; i++) {
|
|
505
506
|
const arg = args[i];
|
|
506
507
|
const singularMatch = completionName === "singular" || completionName === "both" ? arg.startsWith("--completion=") : false;
|
|
@@ -508,14 +509,14 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
508
509
|
if (singularMatch || pluralMatch) {
|
|
509
510
|
const shell = arg.slice(arg.indexOf("=") + 1);
|
|
510
511
|
const completionArgs = args.slice(i + 1);
|
|
511
|
-
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
512
|
+
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
512
513
|
} else {
|
|
513
514
|
const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
|
|
514
515
|
const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
|
|
515
516
|
if (singularMatchExact || pluralMatchExact) {
|
|
516
517
|
const shell = i + 1 < args.length ? args[i + 1] : "";
|
|
517
518
|
const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
|
|
518
|
-
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
|
|
519
|
+
return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName, sectionOrder);
|
|
519
520
|
}
|
|
520
521
|
}
|
|
521
522
|
}
|
|
@@ -625,8 +626,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
625
626
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
626
627
|
const augmentedDoc = {
|
|
627
628
|
...doc,
|
|
628
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
629
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
629
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
630
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
630
631
|
examples: isTopLevel && !isMetaCommandHelp ? examples ?? doc.examples : void 0,
|
|
631
632
|
author: isTopLevel && !isMetaCommandHelp ? author ?? doc.author : void 0,
|
|
632
633
|
bugs: isTopLevel && !isMetaCommandHelp ? bugs ?? doc.bugs : void 0,
|
|
@@ -636,7 +637,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
636
637
|
colors,
|
|
637
638
|
maxWidth,
|
|
638
639
|
showDefault,
|
|
639
|
-
showChoices
|
|
640
|
+
showChoices,
|
|
641
|
+
sectionOrder
|
|
640
642
|
}));
|
|
641
643
|
}
|
|
642
644
|
try {
|
package/dist/primitives.cjs
CHANGED
|
@@ -2,6 +2,7 @@ const require_message = require('./message.cjs');
|
|
|
2
2
|
const require_dependency = require('./dependency.cjs');
|
|
3
3
|
const require_usage = require('./usage.cjs');
|
|
4
4
|
const require_suggestion = require('./suggestion.cjs');
|
|
5
|
+
const require_usage_internals = require('./usage-internals.cjs');
|
|
5
6
|
const require_valueparser = require('./valueparser.cjs');
|
|
6
7
|
|
|
7
8
|
//#region src/primitives.ts
|
|
@@ -929,11 +930,10 @@ function command(name, parser, options = {}) {
|
|
|
929
930
|
if (context.state === void 0) {
|
|
930
931
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
931
932
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
933
|
+
const leadingCmds = require_usage_internals.extractLeadingCommandNames(context.usage);
|
|
934
|
+
const suggestions = actual ? require_suggestion.findSimilar(actual, leadingCmds, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
932
935
|
if (options.errors?.notMatched) {
|
|
933
936
|
const errorMessage = options.errors.notMatched;
|
|
934
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
935
|
-
for (const cmdName of require_usage.extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
936
|
-
const suggestions = actual ? require_suggestion.findSimilar(actual, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
937
937
|
return {
|
|
938
938
|
success: false,
|
|
939
939
|
consumed: 0,
|
|
@@ -946,10 +946,15 @@ function command(name, parser, options = {}) {
|
|
|
946
946
|
error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
|
|
947
947
|
};
|
|
948
948
|
const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
|
|
949
|
+
const suggestionMsg = require_suggestion.createSuggestionMessage(suggestions);
|
|
949
950
|
return {
|
|
950
951
|
success: false,
|
|
951
952
|
consumed: 0,
|
|
952
|
-
error:
|
|
953
|
+
error: suggestionMsg.length > 0 ? [
|
|
954
|
+
...baseError,
|
|
955
|
+
require_message.text("\n\n"),
|
|
956
|
+
...suggestionMsg
|
|
957
|
+
] : baseError
|
|
953
958
|
};
|
|
954
959
|
}
|
|
955
960
|
return {
|
package/dist/primitives.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { message, metavar, optionName, optionNames, valueSet } from "./message.js";
|
|
1
|
+
import { message, metavar, optionName, optionNames, text, valueSet } from "./message.js";
|
|
2
2
|
import { createDeferredParseState, createDependencySourceState, createPendingDependencySourceState, dependencyId, getDefaultValuesFunction, getDependencyIds, isDeferredParseState, isDependencySource, isDependencySourceState, isDerivedValueParser, isPendingDependencySourceState, suggestWithDependency } from "./dependency.js";
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar } from "./suggestion.js";
|
|
3
|
+
import { extractOptionNames } from "./usage.js";
|
|
4
|
+
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
|
|
5
|
+
import { extractLeadingCommandNames } from "./usage-internals.js";
|
|
5
6
|
import { isValueParser } from "./valueparser.js";
|
|
6
7
|
|
|
7
8
|
//#region src/primitives.ts
|
|
@@ -929,11 +930,10 @@ function command(name, parser, options = {}) {
|
|
|
929
930
|
if (context.state === void 0) {
|
|
930
931
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
931
932
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
933
|
+
const leadingCmds = extractLeadingCommandNames(context.usage);
|
|
934
|
+
const suggestions = actual ? findSimilar(actual, leadingCmds, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
932
935
|
if (options.errors?.notMatched) {
|
|
933
936
|
const errorMessage = options.errors.notMatched;
|
|
934
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
935
|
-
for (const cmdName of extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
936
|
-
const suggestions = actual ? findSimilar(actual, candidates, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
937
937
|
return {
|
|
938
938
|
success: false,
|
|
939
939
|
consumed: 0,
|
|
@@ -946,10 +946,15 @@ function command(name, parser, options = {}) {
|
|
|
946
946
|
error: message`Expected command ${optionName(name)}, but got end of input.`
|
|
947
947
|
};
|
|
948
948
|
const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
|
|
949
|
+
const suggestionMsg = createSuggestionMessage(suggestions);
|
|
949
950
|
return {
|
|
950
951
|
success: false,
|
|
951
952
|
consumed: 0,
|
|
952
|
-
error:
|
|
953
|
+
error: suggestionMsg.length > 0 ? [
|
|
954
|
+
...baseError,
|
|
955
|
+
text("\n\n"),
|
|
956
|
+
...suggestionMsg
|
|
957
|
+
] : baseError
|
|
953
958
|
};
|
|
954
959
|
}
|
|
955
960
|
return {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/usage-internals.ts
|
|
3
|
+
/**
|
|
4
|
+
* Collects option names and command names that are valid as the *immediate*
|
|
5
|
+
* next token at the current parse position ("leading candidates").
|
|
6
|
+
*
|
|
7
|
+
* Unlike the full-tree extractors in `usage.ts`, this function stops
|
|
8
|
+
* descending into a branch as soon as it hits a required (blocking) term —
|
|
9
|
+
* an option, a command, or a required argument. Optional and zero-or-more
|
|
10
|
+
* terms are traversed but do not block.
|
|
11
|
+
*
|
|
12
|
+
* @param terms The usage terms to inspect.
|
|
13
|
+
* @param optionNames Accumulator for leading option names.
|
|
14
|
+
* @param commandNames Accumulator for leading command names.
|
|
15
|
+
* @returns `true` if every term in `terms` is skippable (i.e., the caller
|
|
16
|
+
* may continue scanning the next sibling term), `false` otherwise.
|
|
17
|
+
*/
|
|
18
|
+
function collectLeadingCandidates(terms, optionNames, commandNames) {
|
|
19
|
+
if (!terms || !Array.isArray(terms)) return true;
|
|
20
|
+
for (const term of terms) {
|
|
21
|
+
if (term.type === "option") {
|
|
22
|
+
for (const name of term.names) optionNames.add(name);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (term.type === "command") {
|
|
26
|
+
commandNames.add(term.name);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (term.type === "argument") return false;
|
|
30
|
+
if (term.type === "optional") {
|
|
31
|
+
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (term.type === "multiple") {
|
|
35
|
+
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
36
|
+
if (term.min === 0) continue;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (term.type === "exclusive") {
|
|
40
|
+
let allSkippable = true;
|
|
41
|
+
for (const branch of term.terms) {
|
|
42
|
+
const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
|
|
43
|
+
allSkippable = allSkippable && branchSkippable;
|
|
44
|
+
}
|
|
45
|
+
if (allSkippable) continue;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns the set of command names that are valid as the *immediate* next
|
|
53
|
+
* token, derived from the leading candidates of `usage`.
|
|
54
|
+
*
|
|
55
|
+
* This is the command-only projection of {@link collectLeadingCandidates}
|
|
56
|
+
* and is used to generate accurate "Did you mean?" suggestions in
|
|
57
|
+
* `command()` error messages — suggestions are scoped to commands actually
|
|
58
|
+
* reachable at the current parse position rather than all commands anywhere
|
|
59
|
+
* in the usage tree.
|
|
60
|
+
*
|
|
61
|
+
* @param usage The usage tree to inspect.
|
|
62
|
+
* @returns A `Set` of command names valid as the next input token.
|
|
63
|
+
*/
|
|
64
|
+
function extractLeadingCommandNames(usage) {
|
|
65
|
+
const options = /* @__PURE__ */ new Set();
|
|
66
|
+
const commands = /* @__PURE__ */ new Set();
|
|
67
|
+
collectLeadingCandidates(usage, options, commands);
|
|
68
|
+
return commands;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
exports.collectLeadingCandidates = collectLeadingCandidates;
|
|
73
|
+
exports.extractLeadingCommandNames = extractLeadingCommandNames;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/usage-internals.ts
|
|
2
|
+
/**
|
|
3
|
+
* Collects option names and command names that are valid as the *immediate*
|
|
4
|
+
* next token at the current parse position ("leading candidates").
|
|
5
|
+
*
|
|
6
|
+
* Unlike the full-tree extractors in `usage.ts`, this function stops
|
|
7
|
+
* descending into a branch as soon as it hits a required (blocking) term —
|
|
8
|
+
* an option, a command, or a required argument. Optional and zero-or-more
|
|
9
|
+
* terms are traversed but do not block.
|
|
10
|
+
*
|
|
11
|
+
* @param terms The usage terms to inspect.
|
|
12
|
+
* @param optionNames Accumulator for leading option names.
|
|
13
|
+
* @param commandNames Accumulator for leading command names.
|
|
14
|
+
* @returns `true` if every term in `terms` is skippable (i.e., the caller
|
|
15
|
+
* may continue scanning the next sibling term), `false` otherwise.
|
|
16
|
+
*/
|
|
17
|
+
function collectLeadingCandidates(terms, optionNames, commandNames) {
|
|
18
|
+
if (!terms || !Array.isArray(terms)) return true;
|
|
19
|
+
for (const term of terms) {
|
|
20
|
+
if (term.type === "option") {
|
|
21
|
+
for (const name of term.names) optionNames.add(name);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (term.type === "command") {
|
|
25
|
+
commandNames.add(term.name);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (term.type === "argument") return false;
|
|
29
|
+
if (term.type === "optional") {
|
|
30
|
+
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (term.type === "multiple") {
|
|
34
|
+
collectLeadingCandidates(term.terms, optionNames, commandNames);
|
|
35
|
+
if (term.min === 0) continue;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (term.type === "exclusive") {
|
|
39
|
+
let allSkippable = true;
|
|
40
|
+
for (const branch of term.terms) {
|
|
41
|
+
const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
|
|
42
|
+
allSkippable = allSkippable && branchSkippable;
|
|
43
|
+
}
|
|
44
|
+
if (allSkippable) continue;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the set of command names that are valid as the *immediate* next
|
|
52
|
+
* token, derived from the leading candidates of `usage`.
|
|
53
|
+
*
|
|
54
|
+
* This is the command-only projection of {@link collectLeadingCandidates}
|
|
55
|
+
* and is used to generate accurate "Did you mean?" suggestions in
|
|
56
|
+
* `command()` error messages — suggestions are scoped to commands actually
|
|
57
|
+
* reachable at the current parse position rather than all commands anywhere
|
|
58
|
+
* in the usage tree.
|
|
59
|
+
*
|
|
60
|
+
* @param usage The usage tree to inspect.
|
|
61
|
+
* @returns A `Set` of command names valid as the next input token.
|
|
62
|
+
*/
|
|
63
|
+
function extractLeadingCommandNames(usage) {
|
|
64
|
+
const options = /* @__PURE__ */ new Set();
|
|
65
|
+
const commands = /* @__PURE__ */ new Set();
|
|
66
|
+
collectLeadingCandidates(usage, options, commands);
|
|
67
|
+
return commands;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { collectLeadingCandidates, extractLeadingCommandNames };
|