@optique/core 0.9.8 → 0.9.10

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.
@@ -1,50 +1,13 @@
1
1
  const require_message = require('./message.cjs');
2
2
  const require_usage = require('./usage.cjs');
3
3
  const require_suggestion = require('./suggestion.cjs');
4
+ const require_usage_internals = require('./usage-internals.cjs');
4
5
 
5
6
  //#region src/constructs.ts
6
- /**
7
- * Collects option names and command names that are valid at the current
8
- * parse position by walking the usage tree. Only "leading" candidates
9
- * (those reachable before a required positional argument) are collected.
10
- */
11
- function collectLeadingCandidates(terms, optionNames, commandNames) {
12
- if (!terms || !Array.isArray(terms)) return true;
13
- for (const term of terms) {
14
- if (term.type === "option") {
15
- for (const name of term.names) optionNames.add(name);
16
- return false;
17
- }
18
- if (term.type === "command") {
19
- commandNames.add(term.name);
20
- return false;
21
- }
22
- if (term.type === "argument") return false;
23
- if (term.type === "optional") {
24
- collectLeadingCandidates(term.terms, optionNames, commandNames);
25
- continue;
26
- }
27
- if (term.type === "multiple") {
28
- collectLeadingCandidates(term.terms, optionNames, commandNames);
29
- if (term.min === 0) continue;
30
- return false;
31
- }
32
- if (term.type === "exclusive") {
33
- let allAlternativesSkippable = true;
34
- for (const exclusiveUsage of term.terms) {
35
- const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
36
- allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
37
- }
38
- if (allAlternativesSkippable) continue;
39
- return false;
40
- }
41
- }
42
- return true;
43
- }
44
7
  function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
45
8
  const options = /* @__PURE__ */ new Set();
46
9
  const commands = /* @__PURE__ */ new Set();
47
- for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
10
+ for (const parser of parsers) require_usage_internals.collectLeadingCandidates(parser.usage, options, commands);
48
11
  const candidates = new Set([...options, ...commands]);
49
12
  const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
50
13
  const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
@@ -1647,7 +1610,7 @@ function group(label, parser) {
1647
1610
  complete: (state) => parser.complete(state),
1648
1611
  suggest: (context, prefix) => parser.suggest(context, prefix),
1649
1612
  getDocFragments: (state, defaultValue) => {
1650
- const { description, fragments } = parser.getDocFragments(state, defaultValue);
1613
+ const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
1651
1614
  const allEntries = [];
1652
1615
  const titledSections = [];
1653
1616
  for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
@@ -1657,15 +1620,22 @@ function group(label, parser) {
1657
1620
  kind: "available",
1658
1621
  state: parser.initialState
1659
1622
  }, void 0);
1660
- const initialHasCommands = initialFragments.fragments.some((f) => f.type === "entry" && f.term.type === "command" || f.type === "section" && f.entries.some((e) => e.term.type === "command"));
1661
- const currentHasCommands = allEntries.some((e) => e.term.type === "command");
1662
- const applyLabel = !initialHasCommands || currentHasCommands;
1623
+ const initialCommandNames = /* @__PURE__ */ new Set();
1624
+ for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
1625
+ else if (f.type === "section") {
1626
+ for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
1627
+ }
1628
+ const initialHasCommands = initialCommandNames.size > 0;
1629
+ const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
1630
+ const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
1663
1631
  const labeledSection = applyLabel ? {
1664
1632
  title: label,
1665
1633
  entries: allEntries
1666
1634
  } : { entries: allEntries };
1667
1635
  return {
1636
+ brief,
1668
1637
  description,
1638
+ footer,
1669
1639
  fragments: [...titledSections.map((s) => ({
1670
1640
  ...s,
1671
1641
  type: "section"
@@ -1,46 +1,9 @@
1
1
  import { message, optionName, text, values } from "./message.js";
2
2
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
3
3
  import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
4
+ import { collectLeadingCandidates } from "./usage-internals.js";
4
5
 
5
6
  //#region src/constructs.ts
6
- /**
7
- * Collects option names and command names that are valid at the current
8
- * parse position by walking the usage tree. Only "leading" candidates
9
- * (those reachable before a required positional argument) are collected.
10
- */
11
- function collectLeadingCandidates(terms, optionNames, commandNames) {
12
- if (!terms || !Array.isArray(terms)) return true;
13
- for (const term of terms) {
14
- if (term.type === "option") {
15
- for (const name of term.names) optionNames.add(name);
16
- return false;
17
- }
18
- if (term.type === "command") {
19
- commandNames.add(term.name);
20
- return false;
21
- }
22
- if (term.type === "argument") return false;
23
- if (term.type === "optional") {
24
- collectLeadingCandidates(term.terms, optionNames, commandNames);
25
- continue;
26
- }
27
- if (term.type === "multiple") {
28
- collectLeadingCandidates(term.terms, optionNames, commandNames);
29
- if (term.min === 0) continue;
30
- return false;
31
- }
32
- if (term.type === "exclusive") {
33
- let allAlternativesSkippable = true;
34
- for (const exclusiveUsage of term.terms) {
35
- const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
36
- allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
37
- }
38
- if (allAlternativesSkippable) continue;
39
- return false;
40
- }
41
- }
42
- return true;
43
- }
44
7
  function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
45
8
  const options = /* @__PURE__ */ new Set();
46
9
  const commands = /* @__PURE__ */ new Set();
@@ -1647,7 +1610,7 @@ function group(label, parser) {
1647
1610
  complete: (state) => parser.complete(state),
1648
1611
  suggest: (context, prefix) => parser.suggest(context, prefix),
1649
1612
  getDocFragments: (state, defaultValue) => {
1650
- const { description, fragments } = parser.getDocFragments(state, defaultValue);
1613
+ const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
1651
1614
  const allEntries = [];
1652
1615
  const titledSections = [];
1653
1616
  for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
@@ -1657,15 +1620,22 @@ function group(label, parser) {
1657
1620
  kind: "available",
1658
1621
  state: parser.initialState
1659
1622
  }, void 0);
1660
- const initialHasCommands = initialFragments.fragments.some((f) => f.type === "entry" && f.term.type === "command" || f.type === "section" && f.entries.some((e) => e.term.type === "command"));
1661
- const currentHasCommands = allEntries.some((e) => e.term.type === "command");
1662
- const applyLabel = !initialHasCommands || currentHasCommands;
1623
+ const initialCommandNames = /* @__PURE__ */ new Set();
1624
+ for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
1625
+ else if (f.type === "section") {
1626
+ for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
1627
+ }
1628
+ const initialHasCommands = initialCommandNames.size > 0;
1629
+ const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
1630
+ const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
1663
1631
  const labeledSection = applyLabel ? {
1664
1632
  title: label,
1665
1633
  entries: allEntries
1666
1634
  } : { entries: allEntries };
1667
1635
  return {
1636
+ brief,
1668
1637
  description,
1638
+ footer,
1669
1639
  fragments: [...titledSections.map((s) => ({
1670
1640
  ...s,
1671
1641
  type: "section"
package/dist/facade.cjs CHANGED
@@ -577,8 +577,8 @@ function runParser(parser, programName, args, options = {}) {
577
577
  const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
578
578
  const augmentedDoc = {
579
579
  ...doc,
580
- brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
581
- description: shouldOverride ? description ?? doc.description : doc.description ?? description,
580
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief,
581
+ description: shouldOverride ? description ?? doc.description : doc.description,
582
582
  footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
583
583
  };
584
584
  stdout(require_doc.formatDocPage(programName, augmentedDoc, {
package/dist/facade.js CHANGED
@@ -577,8 +577,8 @@ function runParser(parser, programName, args, options = {}) {
577
577
  const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
578
578
  const augmentedDoc = {
579
579
  ...doc,
580
- brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
581
- description: shouldOverride ? description ?? doc.description : doc.description ?? description,
580
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief,
581
+ description: shouldOverride ? description ?? doc.description : doc.description,
582
582
  footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
583
583
  };
584
584
  stdout(formatDocPage(programName, augmentedDoc, {
package/dist/parser.cjs CHANGED
@@ -308,11 +308,11 @@ function getDocPageSyncImpl(parser, args) {
308
308
  state: parser.initialState,
309
309
  usage: parser.usage
310
310
  };
311
- do {
311
+ while (context.buffer.length > 0) {
312
312
  const result = parser.parse(context);
313
313
  if (!result.success) break;
314
314
  context = result.next;
315
- } while (context.buffer.length > 0);
315
+ }
316
316
  return buildDocPage(parser, context, args);
317
317
  }
318
318
  /**
@@ -325,11 +325,11 @@ async function getDocPageAsyncImpl(parser, args) {
325
325
  state: parser.initialState,
326
326
  usage: parser.usage
327
327
  };
328
- do {
328
+ while (context.buffer.length > 0) {
329
329
  const result = await parser.parse(context);
330
330
  if (!result.success) break;
331
331
  context = result.next;
332
- } while (context.buffer.length > 0);
332
+ }
333
333
  return buildDocPage(parser, context, args);
334
334
  }
335
335
  /**
package/dist/parser.js CHANGED
@@ -308,11 +308,11 @@ function getDocPageSyncImpl(parser, args) {
308
308
  state: parser.initialState,
309
309
  usage: parser.usage
310
310
  };
311
- do {
311
+ while (context.buffer.length > 0) {
312
312
  const result = parser.parse(context);
313
313
  if (!result.success) break;
314
314
  context = result.next;
315
- } while (context.buffer.length > 0);
315
+ }
316
316
  return buildDocPage(parser, context, args);
317
317
  }
318
318
  /**
@@ -325,11 +325,11 @@ async function getDocPageAsyncImpl(parser, args) {
325
325
  state: parser.initialState,
326
326
  usage: parser.usage
327
327
  };
328
- do {
328
+ while (context.buffer.length > 0) {
329
329
  const result = await parser.parse(context);
330
330
  if (!result.success) break;
331
331
  context = result.next;
332
- } while (context.buffer.length > 0);
332
+ }
333
333
  return buildDocPage(parser, context, args);
334
334
  }
335
335
  /**
@@ -1,6 +1,7 @@
1
1
  const require_message = require('./message.cjs');
2
2
  const require_usage = require('./usage.cjs');
3
3
  const require_suggestion = require('./suggestion.cjs');
4
+ const require_usage_internals = require('./usage-internals.cjs');
4
5
  const require_valueparser = require('./valueparser.cjs');
5
6
 
6
7
  //#region src/primitives.ts
@@ -752,11 +753,10 @@ function command(name, parser, options = {}) {
752
753
  if (context.state === void 0) {
753
754
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
754
755
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
756
+ const leadingCmds = require_usage_internals.extractLeadingCommandNames(context.usage);
757
+ const suggestions = actual ? require_suggestion.findSimilar(actual, leadingCmds, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
755
758
  if (options.errors?.notMatched) {
756
759
  const errorMessage = options.errors.notMatched;
757
- const candidates = /* @__PURE__ */ new Set();
758
- for (const cmdName of require_usage.extractCommandNames(context.usage)) candidates.add(cmdName);
759
- const suggestions = actual ? require_suggestion.findSimilar(actual, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
760
760
  return {
761
761
  success: false,
762
762
  consumed: 0,
@@ -769,10 +769,15 @@ function command(name, parser, options = {}) {
769
769
  error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
770
770
  };
771
771
  const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
772
+ const suggestionMsg = require_suggestion.createSuggestionMessage(suggestions);
772
773
  return {
773
774
  success: false,
774
775
  consumed: 0,
775
- error: require_suggestion.createErrorWithSuggestions(baseError, actual, context.usage, "command")
776
+ error: suggestionMsg.length > 0 ? [
777
+ ...baseError,
778
+ require_message.text("\n\n"),
779
+ ...suggestionMsg
780
+ ] : baseError
776
781
  };
777
782
  }
778
783
  return {
@@ -1,6 +1,7 @@
1
- import { message, metavar, optionName, optionNames } from "./message.js";
2
- import { extractCommandNames, extractOptionNames } from "./usage.js";
3
- import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar } from "./suggestion.js";
1
+ import { message, metavar, optionName, optionNames, text } from "./message.js";
2
+ import { extractOptionNames } from "./usage.js";
3
+ import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
4
+ import { extractLeadingCommandNames } from "./usage-internals.js";
4
5
  import { isValueParser } from "./valueparser.js";
5
6
 
6
7
  //#region src/primitives.ts
@@ -752,11 +753,10 @@ function command(name, parser, options = {}) {
752
753
  if (context.state === void 0) {
753
754
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
754
755
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
756
+ const leadingCmds = extractLeadingCommandNames(context.usage);
757
+ const suggestions = actual ? findSimilar(actual, leadingCmds, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
755
758
  if (options.errors?.notMatched) {
756
759
  const errorMessage = options.errors.notMatched;
757
- const candidates = /* @__PURE__ */ new Set();
758
- for (const cmdName of extractCommandNames(context.usage)) candidates.add(cmdName);
759
- const suggestions = actual ? findSimilar(actual, candidates, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
760
760
  return {
761
761
  success: false,
762
762
  consumed: 0,
@@ -769,10 +769,15 @@ function command(name, parser, options = {}) {
769
769
  error: message`Expected command ${optionName(name)}, but got end of input.`
770
770
  };
771
771
  const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
772
+ const suggestionMsg = createSuggestionMessage(suggestions);
772
773
  return {
773
774
  success: false,
774
775
  consumed: 0,
775
- error: createErrorWithSuggestions(baseError, actual, context.usage, "command")
776
+ error: suggestionMsg.length > 0 ? [
777
+ ...baseError,
778
+ text("\n\n"),
779
+ ...suggestionMsg
780
+ ] : baseError
776
781
  };
777
782
  }
778
783
  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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",