@optique/core 0.7.0-dev.127 → 0.7.0-dev.128

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/README.md CHANGED
@@ -16,7 +16,7 @@ you usually won't use it directly in browsers.
16
16
 
17
17
 
18
18
  When to use @optique/core
19
- ------------------------
19
+ -------------------------
20
20
 
21
21
  Use *@optique/core* instead when:
22
22
 
@@ -1,5 +1,6 @@
1
1
  const require_message = require('./message.cjs');
2
2
  const require_usage = require('./usage.cjs');
3
+ const require_suggestion = require('./suggestion.cjs');
3
4
 
4
5
  //#region src/constructs.ts
5
6
  /**
@@ -42,7 +43,8 @@ function or(...args) {
42
43
  error: context.buffer.length < 1 ? options?.errors?.noMatch ?? require_message.message`No matching option or command found.` : (() => {
43
44
  const token = context.buffer[0];
44
45
  const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
45
- return options?.errors?.unexpectedInput != null ? typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput : defaultMsg;
46
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
47
+ return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
46
48
  })()
47
49
  };
48
50
  const orderedParsers = parsers.map((p, i) => [p, i]);
@@ -177,7 +179,8 @@ function longestMatch(...args) {
177
179
  error: context.buffer.length < 1 ? options?.errors?.noMatch ?? require_message.message`No matching option or command found.` : (() => {
178
180
  const token = context.buffer[0];
179
181
  const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
180
- return options?.errors?.unexpectedInput != null ? typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput : defaultMsg;
182
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
183
+ return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
181
184
  })()
182
185
  };
183
186
  for (let i = 0; i < parsers.length; i++) {
@@ -299,7 +302,9 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
299
302
  error: context.buffer.length > 0 ? (() => {
300
303
  const token = context.buffer[0];
301
304
  const customMessage = options.errors?.unexpectedInput;
302
- return customMessage ? typeof customMessage === "function" ? customMessage(token) : customMessage : require_message.message`Unexpected option or argument: ${token}.`;
305
+ if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
306
+ const baseError = require_message.message`Unexpected option or argument: ${token}.`;
307
+ return require_suggestion.createErrorWithSuggestions(baseError, token, context.usage, "both");
303
308
  })() : options.errors?.endOfInput ?? require_message.message`Expected an option or argument, but got end of input.`
304
309
  };
305
310
  let currentContext = context;
@@ -1,5 +1,6 @@
1
1
  import { message, optionName, values } from "./message.js";
2
2
  import { extractOptionNames } from "./usage.js";
3
+ import { createErrorWithSuggestions } from "./suggestion.js";
3
4
 
4
5
  //#region src/constructs.ts
5
6
  /**
@@ -42,7 +43,8 @@ function or(...args) {
42
43
  error: context.buffer.length < 1 ? options?.errors?.noMatch ?? message`No matching option or command found.` : (() => {
43
44
  const token = context.buffer[0];
44
45
  const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
45
- return options?.errors?.unexpectedInput != null ? typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput : defaultMsg;
46
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
47
+ return createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
46
48
  })()
47
49
  };
48
50
  const orderedParsers = parsers.map((p, i) => [p, i]);
@@ -177,7 +179,8 @@ function longestMatch(...args) {
177
179
  error: context.buffer.length < 1 ? options?.errors?.noMatch ?? message`No matching option or command found.` : (() => {
178
180
  const token = context.buffer[0];
179
181
  const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
180
- return options?.errors?.unexpectedInput != null ? typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput : defaultMsg;
182
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
183
+ return createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
181
184
  })()
182
185
  };
183
186
  for (let i = 0; i < parsers.length; i++) {
@@ -299,7 +302,9 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
299
302
  error: context.buffer.length > 0 ? (() => {
300
303
  const token = context.buffer[0];
301
304
  const customMessage = options.errors?.unexpectedInput;
302
- return customMessage ? typeof customMessage === "function" ? customMessage(token) : customMessage : message`Unexpected option or argument: ${token}.`;
305
+ if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
306
+ const baseError = message`Unexpected option or argument: ${token}.`;
307
+ return createErrorWithSuggestions(baseError, token, context.usage, "both");
303
308
  })() : options.errors?.endOfInput ?? message`Expected an option or argument, but got end of input.`
304
309
  };
305
310
  let currentContext = context;
package/dist/index.cjs CHANGED
@@ -19,6 +19,7 @@ exports.commandLine = require_message.commandLine;
19
19
  exports.concat = require_constructs.concat;
20
20
  exports.constant = require_primitives.constant;
21
21
  exports.envVar = require_message.envVar;
22
+ exports.extractCommandNames = require_usage.extractCommandNames;
22
23
  exports.extractOptionNames = require_usage.extractOptionNames;
23
24
  exports.fish = require_completion.fish;
24
25
  exports.flag = require_primitives.flag;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Message, MessageFormatOptions, MessageTerm, commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.cjs";
2
- import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.cjs";
2
+ import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.cjs";
3
3
  import { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowDefaultOptions, formatDocPage } from "./doc.cjs";
4
4
  import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, StringOptions, UrlOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, choice, float, integer, isValueParser, locale, string, url, uuid } from "./valueparser.cjs";
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.cjs";
@@ -8,4 +8,4 @@ import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Sugg
8
8
  import { LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
9
9
  import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.cjs";
10
10
  import { RunError, RunOptions, run } from "./facade.cjs";
11
- export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
11
+ export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Message, MessageFormatOptions, MessageTerm, commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.js";
2
- import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
2
+ import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
3
3
  import { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowDefaultOptions, formatDocPage } from "./doc.js";
4
4
  import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, StringOptions, UrlOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, choice, float, integer, isValueParser, locale, string, url, uuid } from "./valueparser.js";
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.js";
@@ -8,4 +8,4 @@ import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Sugg
8
8
  import { LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
9
9
  import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.js";
10
10
  import { RunError, RunOptions, run } from "./facade.js";
11
- export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
11
+ export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.js";
2
- import { extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
2
+ import { extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
3
3
  import { concat, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
4
4
  import { formatDocPage } from "./doc.js";
5
5
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
@@ -9,4 +9,4 @@ import { argument, command, constant, flag, option } from "./primitives.js";
9
9
  import { getDocPage, parse, suggest } from "./parser.js";
10
10
  import { RunError, run } from "./facade.js";
11
11
 
12
- export { RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
12
+ export { RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
package/dist/message.cjs CHANGED
@@ -155,13 +155,41 @@ function formatMessage(msg, options = {}) {
155
155
  const resetSequence = `\x1b[0m${resetSuffix}`;
156
156
  function* stream() {
157
157
  const wordPattern = /\s*\S+\s*/g;
158
- for (const term of msg) if (term.type === "text") while (true) {
159
- const match = wordPattern.exec(term.text);
160
- if (match == null) break;
161
- yield {
162
- text: match[0],
163
- width: match[0].length
158
+ for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
159
+ const paragraphs = term.text.split(/\n\n+/);
160
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
161
+ if (paragraphIndex > 0) yield {
162
+ text: "\n",
163
+ width: -1
164
+ };
165
+ const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
166
+ wordPattern.lastIndex = 0;
167
+ while (true) {
168
+ const match = wordPattern.exec(paragraph);
169
+ if (match == null) break;
170
+ yield {
171
+ text: match[0],
172
+ width: match[0].length
173
+ };
174
+ }
175
+ }
176
+ } else {
177
+ const normalizedText = term.text.replace(/\n/g, " ");
178
+ if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
179
+ text: " ",
180
+ width: 1
164
181
  };
182
+ else {
183
+ wordPattern.lastIndex = 0;
184
+ while (true) {
185
+ const match = wordPattern.exec(normalizedText);
186
+ if (match == null) break;
187
+ yield {
188
+ text: match[0],
189
+ width: match[0].length
190
+ };
191
+ }
192
+ }
165
193
  }
166
194
  else if (term.type === "optionName") {
167
195
  const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
@@ -223,6 +251,11 @@ function formatMessage(msg, options = {}) {
223
251
  let output = "";
224
252
  let totalWidth = 0;
225
253
  for (const { text: text$1, width } of stream()) {
254
+ if (width === -1) {
255
+ output += text$1;
256
+ totalWidth = 0;
257
+ continue;
258
+ }
226
259
  if (options.maxWidth != null && totalWidth + width > options.maxWidth) {
227
260
  output += "\n";
228
261
  totalWidth = 0;
package/dist/message.js CHANGED
@@ -154,13 +154,41 @@ function formatMessage(msg, options = {}) {
154
154
  const resetSequence = `\x1b[0m${resetSuffix}`;
155
155
  function* stream() {
156
156
  const wordPattern = /\s*\S+\s*/g;
157
- for (const term of msg) if (term.type === "text") while (true) {
158
- const match = wordPattern.exec(term.text);
159
- if (match == null) break;
160
- yield {
161
- text: match[0],
162
- width: match[0].length
157
+ for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
158
+ const paragraphs = term.text.split(/\n\n+/);
159
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
160
+ if (paragraphIndex > 0) yield {
161
+ text: "\n",
162
+ width: -1
163
+ };
164
+ const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
165
+ wordPattern.lastIndex = 0;
166
+ while (true) {
167
+ const match = wordPattern.exec(paragraph);
168
+ if (match == null) break;
169
+ yield {
170
+ text: match[0],
171
+ width: match[0].length
172
+ };
173
+ }
174
+ }
175
+ } else {
176
+ const normalizedText = term.text.replace(/\n/g, " ");
177
+ if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
178
+ text: " ",
179
+ width: 1
163
180
  };
181
+ else {
182
+ wordPattern.lastIndex = 0;
183
+ while (true) {
184
+ const match = wordPattern.exec(normalizedText);
185
+ if (match == null) break;
186
+ yield {
187
+ text: match[0],
188
+ width: match[0].length
189
+ };
190
+ }
191
+ }
164
192
  }
165
193
  else if (term.type === "optionName") {
166
194
  const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
@@ -222,6 +250,11 @@ function formatMessage(msg, options = {}) {
222
250
  let output = "";
223
251
  let totalWidth = 0;
224
252
  for (const { text: text$1, width } of stream()) {
253
+ if (width === -1) {
254
+ output += text$1;
255
+ totalWidth = 0;
256
+ continue;
257
+ }
225
258
  if (options.maxWidth != null && totalWidth + width > options.maxWidth) {
226
259
  output += "\n";
227
260
  totalWidth = 0;
package/dist/parser.cjs CHANGED
@@ -23,7 +23,8 @@ function parse(parser, args) {
23
23
  let context = {
24
24
  buffer: args,
25
25
  optionsTerminated: false,
26
- state: parser.initialState
26
+ state: parser.initialState,
27
+ usage: parser.usage
27
28
  };
28
29
  do {
29
30
  const result = parser.parse(context);
@@ -81,7 +82,8 @@ function suggest(parser, args) {
81
82
  let context = {
82
83
  buffer: allButLast,
83
84
  optionsTerminated: false,
84
- state: parser.initialState
85
+ state: parser.initialState,
86
+ usage: parser.usage
85
87
  };
86
88
  while (context.buffer.length > 0) {
87
89
  const result = parser.parse(context);
@@ -129,7 +131,8 @@ function getDocPage(parser, args = []) {
129
131
  let context = {
130
132
  buffer: args,
131
133
  optionsTerminated: false,
132
- state: parser.initialState
134
+ state: parser.initialState,
135
+ usage: parser.usage
133
136
  };
134
137
  do {
135
138
  const result = parser.parse(context);
package/dist/parser.d.cts CHANGED
@@ -125,6 +125,14 @@ interface ParserContext<TState> {
125
125
  * that no further options should be processed.
126
126
  */
127
127
  readonly optionsTerminated: boolean;
128
+ /**
129
+ * Usage information for the entire parser tree.
130
+ * Used to provide better error messages with suggestions for typos.
131
+ * When a parser encounters an invalid option or command, it can use
132
+ * this information to suggest similar valid options.
133
+ * @since 0.7.0
134
+ */
135
+ readonly usage: Usage;
128
136
  }
129
137
  /**
130
138
  * Represents a suggestion for command-line completion or guidance.
package/dist/parser.d.ts CHANGED
@@ -125,6 +125,14 @@ interface ParserContext<TState> {
125
125
  * that no further options should be processed.
126
126
  */
127
127
  readonly optionsTerminated: boolean;
128
+ /**
129
+ * Usage information for the entire parser tree.
130
+ * Used to provide better error messages with suggestions for typos.
131
+ * When a parser encounters an invalid option or command, it can use
132
+ * this information to suggest similar valid options.
133
+ * @since 0.7.0
134
+ */
135
+ readonly usage: Usage;
128
136
  }
129
137
  /**
130
138
  * Represents a suggestion for command-line completion or guidance.
package/dist/parser.js CHANGED
@@ -23,7 +23,8 @@ function parse(parser, args) {
23
23
  let context = {
24
24
  buffer: args,
25
25
  optionsTerminated: false,
26
- state: parser.initialState
26
+ state: parser.initialState,
27
+ usage: parser.usage
27
28
  };
28
29
  do {
29
30
  const result = parser.parse(context);
@@ -81,7 +82,8 @@ function suggest(parser, args) {
81
82
  let context = {
82
83
  buffer: allButLast,
83
84
  optionsTerminated: false,
84
- state: parser.initialState
85
+ state: parser.initialState,
86
+ usage: parser.usage
85
87
  };
86
88
  while (context.buffer.length > 0) {
87
89
  const result = parser.parse(context);
@@ -129,7 +131,8 @@ function getDocPage(parser, args = []) {
129
131
  let context = {
130
132
  buffer: args,
131
133
  optionsTerminated: false,
132
- state: parser.initialState
134
+ state: parser.initialState,
135
+ usage: parser.usage
133
136
  };
134
137
  do {
135
138
  const result = parser.parse(context);
@@ -1,4 +1,5 @@
1
1
  const require_message = require('./message.cjs');
2
+ const require_suggestion = require('./suggestion.cjs');
2
3
  const require_valueparser = require('./valueparser.cjs');
3
4
 
4
5
  //#region src/primitives.ts
@@ -182,10 +183,12 @@ function option(...args) {
182
183
  };
183
184
  }
184
185
  }
186
+ const invalidOption = context.buffer[0];
187
+ const baseError = require_message.message`No matched option for ${require_message.optionName(invalidOption)}.`;
185
188
  return {
186
189
  success: false,
187
190
  consumed: 0,
188
- error: require_message.message`No matched option for ${require_message.optionName(context.buffer[0])}.`
191
+ error: require_suggestion.createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
189
192
  };
190
193
  },
191
194
  complete(state) {
@@ -386,10 +389,12 @@ function flag(...args) {
386
389
  consumed: [context.buffer[0].slice(0, 2)]
387
390
  };
388
391
  }
392
+ const invalidOption = context.buffer[0];
393
+ const baseError = require_message.message`No matched option for ${require_message.optionName(invalidOption)}.`;
389
394
  return {
390
395
  success: false,
391
396
  consumed: 0,
392
- error: require_message.message`No matched option for ${require_message.optionName(context.buffer[0])}.`
397
+ error: require_suggestion.createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
393
398
  };
394
399
  },
395
400
  complete(state) {
@@ -565,11 +570,24 @@ function command(name, parser, options = {}) {
565
570
  if (context.state === void 0) {
566
571
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
567
572
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
568
- const errorMessage = options.errors?.notMatched ?? require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual ?? "end of input"}.`;
573
+ if (options.errors?.notMatched) {
574
+ const errorMessage = options.errors.notMatched;
575
+ return {
576
+ success: false,
577
+ consumed: 0,
578
+ error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
579
+ };
580
+ }
581
+ if (actual == null) return {
582
+ success: false,
583
+ consumed: 0,
584
+ error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
585
+ };
586
+ const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
569
587
  return {
570
588
  success: false,
571
589
  consumed: 0,
572
- error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
590
+ error: require_suggestion.createErrorWithSuggestions(baseError, actual, context.usage, "command")
573
591
  };
574
592
  }
575
593
  return {
@@ -1,4 +1,5 @@
1
1
  import { message, metavar, optionName, optionNames } from "./message.js";
2
+ import { createErrorWithSuggestions } from "./suggestion.js";
2
3
  import { isValueParser } from "./valueparser.js";
3
4
 
4
5
  //#region src/primitives.ts
@@ -182,10 +183,12 @@ function option(...args) {
182
183
  };
183
184
  }
184
185
  }
186
+ const invalidOption = context.buffer[0];
187
+ const baseError = message`No matched option for ${optionName(invalidOption)}.`;
185
188
  return {
186
189
  success: false,
187
190
  consumed: 0,
188
- error: message`No matched option for ${optionName(context.buffer[0])}.`
191
+ error: createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
189
192
  };
190
193
  },
191
194
  complete(state) {
@@ -386,10 +389,12 @@ function flag(...args) {
386
389
  consumed: [context.buffer[0].slice(0, 2)]
387
390
  };
388
391
  }
392
+ const invalidOption = context.buffer[0];
393
+ const baseError = message`No matched option for ${optionName(invalidOption)}.`;
389
394
  return {
390
395
  success: false,
391
396
  consumed: 0,
392
- error: message`No matched option for ${optionName(context.buffer[0])}.`
397
+ error: createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
393
398
  };
394
399
  },
395
400
  complete(state) {
@@ -565,11 +570,24 @@ function command(name, parser, options = {}) {
565
570
  if (context.state === void 0) {
566
571
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
567
572
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
568
- const errorMessage = options.errors?.notMatched ?? message`Expected command ${optionName(name)}, but got ${actual ?? "end of input"}.`;
573
+ if (options.errors?.notMatched) {
574
+ const errorMessage = options.errors.notMatched;
575
+ return {
576
+ success: false,
577
+ consumed: 0,
578
+ error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
579
+ };
580
+ }
581
+ if (actual == null) return {
582
+ success: false,
583
+ consumed: 0,
584
+ error: message`Expected command ${optionName(name)}, but got end of input.`
585
+ };
586
+ const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
569
587
  return {
570
588
  success: false,
571
589
  consumed: 0,
572
- error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
590
+ error: createErrorWithSuggestions(baseError, actual, context.usage, "command")
573
591
  };
574
592
  }
575
593
  return {
@@ -0,0 +1,175 @@
1
+ const require_message = require('./message.cjs');
2
+ const require_usage = require('./usage.cjs');
3
+
4
+ //#region src/suggestion.ts
5
+ /**
6
+ * Calculates the Levenshtein distance between two strings.
7
+ *
8
+ * The Levenshtein distance is the minimum number of single-character edits
9
+ * (insertions, deletions, or substitutions) required to transform one string
10
+ * into another.
11
+ *
12
+ * @param source The source string
13
+ * @param target The target string
14
+ * @returns The edit distance (number of insertions, deletions, substitutions)
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * levenshteinDistance("kitten", "sitting"); // returns 3
19
+ * levenshteinDistance("--verbos", "--verbose"); // returns 1
20
+ * levenshteinDistance("hello", "hello"); // returns 0
21
+ * ```
22
+ */
23
+ function levenshteinDistance(source, target) {
24
+ if (source.length === 0) return target.length;
25
+ if (target.length === 0) return source.length;
26
+ if (source.length > target.length) [source, target] = [target, source];
27
+ let previousRow = new Array(source.length + 1);
28
+ let currentRow = new Array(source.length + 1);
29
+ for (let i = 0; i <= source.length; i++) previousRow[i] = i;
30
+ for (let j = 1; j <= target.length; j++) {
31
+ currentRow[0] = j;
32
+ for (let i = 1; i <= source.length; i++) {
33
+ const cost = source[i - 1] === target[j - 1] ? 0 : 1;
34
+ currentRow[i] = Math.min(currentRow[i - 1] + 1, previousRow[i] + 1, previousRow[i - 1] + cost);
35
+ }
36
+ [previousRow, currentRow] = [currentRow, previousRow];
37
+ }
38
+ return previousRow[source.length];
39
+ }
40
+ /**
41
+ * Finds similar strings from a list of candidates.
42
+ *
43
+ * This function uses Levenshtein distance to find strings that are similar
44
+ * to the input string. Results are sorted by similarity (most similar first).
45
+ *
46
+ * @param input The input string to find matches for
47
+ * @param candidates List of candidate strings to compare against
48
+ * @param options Configuration options
49
+ * @returns Array of similar strings, sorted by similarity (most similar first)
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const candidates = ["--verbose", "--version", "--verify", "--help"];
54
+ * findSimilar("--verbos", candidates);
55
+ * // returns ["--verbose"]
56
+ *
57
+ * findSimilar("--ver", candidates, { maxDistance: 5 });
58
+ * // returns ["--verify", "--version", "--verbose"]
59
+ *
60
+ * findSimilar("--xyz", candidates);
61
+ * // returns [] (no similar matches)
62
+ * ```
63
+ */
64
+ function findSimilar(input, candidates, options = {}) {
65
+ const maxDistance = options.maxDistance ?? 3;
66
+ const maxDistanceRatio = options.maxDistanceRatio ?? .5;
67
+ const maxSuggestions = options.maxSuggestions ?? 3;
68
+ const caseSensitive = options.caseSensitive ?? false;
69
+ if (input.length === 0) return [];
70
+ const normalizedInput = caseSensitive ? input : input.toLowerCase();
71
+ const matches = [];
72
+ for (const candidate of candidates) {
73
+ const normalizedCandidate = caseSensitive ? candidate : candidate.toLowerCase();
74
+ const distance = levenshteinDistance(normalizedInput, normalizedCandidate);
75
+ if (distance === 0) return [candidate];
76
+ const distanceRatio = distance / input.length;
77
+ if (distance <= maxDistance && distanceRatio <= maxDistanceRatio) matches.push({
78
+ candidate,
79
+ distance
80
+ });
81
+ }
82
+ matches.sort((a, b) => {
83
+ if (a.distance !== b.distance) return a.distance - b.distance;
84
+ const lengthDiffA = Math.abs(a.candidate.length - input.length);
85
+ const lengthDiffB = Math.abs(b.candidate.length - input.length);
86
+ if (lengthDiffA !== lengthDiffB) return lengthDiffA - lengthDiffB;
87
+ return a.candidate.localeCompare(b.candidate);
88
+ });
89
+ return matches.slice(0, maxSuggestions).map((m) => m.candidate);
90
+ }
91
+ /**
92
+ * Creates a suggestion message for a mismatched option/command.
93
+ *
94
+ * This function formats suggestions in a user-friendly way:
95
+ * - No suggestions: returns empty message
96
+ * - One suggestion: "Did you mean `option`?"
97
+ * - Multiple suggestions: "Did you mean one of these?\n option1\n option2"
98
+ *
99
+ * @param suggestions List of similar valid options/commands
100
+ * @returns A Message array with suggestion text
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * createSuggestionMessage(["--verbose", "--version"]);
105
+ * // returns message parts for:
106
+ * // "Did you mean one of these?
107
+ * // --verbose
108
+ * // --version"
109
+ *
110
+ * createSuggestionMessage(["--verbose"]);
111
+ * // returns message parts for:
112
+ * // "Did you mean `--verbose`?"
113
+ *
114
+ * createSuggestionMessage([]);
115
+ * // returns []
116
+ * ```
117
+ */
118
+ function createSuggestionMessage(suggestions) {
119
+ if (suggestions.length === 0) return [];
120
+ if (suggestions.length === 1) return require_message.message`Did you mean ${require_message.optionName(suggestions[0])}?`;
121
+ const messageParts = [require_message.text("Did you mean one of these?")];
122
+ for (const suggestion of suggestions) {
123
+ messageParts.push(require_message.text("\n "));
124
+ messageParts.push(require_message.optionName(suggestion));
125
+ }
126
+ return messageParts;
127
+ }
128
+ /**
129
+ * Creates an error message with suggestions for similar options or commands.
130
+ *
131
+ * This is a convenience function that combines the functionality of
132
+ * `findSimilar()` and `createSuggestionMessage()` to generate user-friendly
133
+ * error messages with "Did you mean?" suggestions.
134
+ *
135
+ * @param baseError The base error message to display
136
+ * @param invalidInput The invalid option or command name that the user typed
137
+ * @param usage The usage information to extract available options/commands from
138
+ * @param type What type of names to suggest ("option", "command", or "both")
139
+ * @returns A message combining the base error with suggestions, or just the
140
+ * base error if no similar names are found
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const baseError = message`No matched option for ${optionName("--verbos")}.`;
145
+ * const error = createErrorWithSuggestions(
146
+ * baseError,
147
+ * "--verbos",
148
+ * context.usage,
149
+ * "option"
150
+ * );
151
+ * // Returns: "No matched option for `--verbos`.\nDid you mean `--verbose`?"
152
+ * ```
153
+ *
154
+ * @since 0.7.0
155
+ */
156
+ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both") {
157
+ const candidates = /* @__PURE__ */ new Set();
158
+ if (type === "option" || type === "both") for (const name of require_usage.extractOptionNames(usage)) candidates.add(name);
159
+ if (type === "command" || type === "both") for (const name of require_usage.extractCommandNames(usage)) candidates.add(name);
160
+ const suggestions = findSimilar(invalidInput, candidates, {
161
+ maxDistance: 3,
162
+ maxDistanceRatio: .5,
163
+ maxSuggestions: 3,
164
+ caseSensitive: false
165
+ });
166
+ const suggestionMsg = createSuggestionMessage(suggestions);
167
+ return suggestionMsg.length > 0 ? [
168
+ ...baseError,
169
+ require_message.text("\n\n"),
170
+ ...suggestionMsg
171
+ ] : baseError;
172
+ }
173
+
174
+ //#endregion
175
+ exports.createErrorWithSuggestions = createErrorWithSuggestions;
@@ -0,0 +1,175 @@
1
+ import { message, optionName, text } from "./message.js";
2
+ import { extractCommandNames, extractOptionNames } from "./usage.js";
3
+
4
+ //#region src/suggestion.ts
5
+ /**
6
+ * Calculates the Levenshtein distance between two strings.
7
+ *
8
+ * The Levenshtein distance is the minimum number of single-character edits
9
+ * (insertions, deletions, or substitutions) required to transform one string
10
+ * into another.
11
+ *
12
+ * @param source The source string
13
+ * @param target The target string
14
+ * @returns The edit distance (number of insertions, deletions, substitutions)
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * levenshteinDistance("kitten", "sitting"); // returns 3
19
+ * levenshteinDistance("--verbos", "--verbose"); // returns 1
20
+ * levenshteinDistance("hello", "hello"); // returns 0
21
+ * ```
22
+ */
23
+ function levenshteinDistance(source, target) {
24
+ if (source.length === 0) return target.length;
25
+ if (target.length === 0) return source.length;
26
+ if (source.length > target.length) [source, target] = [target, source];
27
+ let previousRow = new Array(source.length + 1);
28
+ let currentRow = new Array(source.length + 1);
29
+ for (let i = 0; i <= source.length; i++) previousRow[i] = i;
30
+ for (let j = 1; j <= target.length; j++) {
31
+ currentRow[0] = j;
32
+ for (let i = 1; i <= source.length; i++) {
33
+ const cost = source[i - 1] === target[j - 1] ? 0 : 1;
34
+ currentRow[i] = Math.min(currentRow[i - 1] + 1, previousRow[i] + 1, previousRow[i - 1] + cost);
35
+ }
36
+ [previousRow, currentRow] = [currentRow, previousRow];
37
+ }
38
+ return previousRow[source.length];
39
+ }
40
+ /**
41
+ * Finds similar strings from a list of candidates.
42
+ *
43
+ * This function uses Levenshtein distance to find strings that are similar
44
+ * to the input string. Results are sorted by similarity (most similar first).
45
+ *
46
+ * @param input The input string to find matches for
47
+ * @param candidates List of candidate strings to compare against
48
+ * @param options Configuration options
49
+ * @returns Array of similar strings, sorted by similarity (most similar first)
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const candidates = ["--verbose", "--version", "--verify", "--help"];
54
+ * findSimilar("--verbos", candidates);
55
+ * // returns ["--verbose"]
56
+ *
57
+ * findSimilar("--ver", candidates, { maxDistance: 5 });
58
+ * // returns ["--verify", "--version", "--verbose"]
59
+ *
60
+ * findSimilar("--xyz", candidates);
61
+ * // returns [] (no similar matches)
62
+ * ```
63
+ */
64
+ function findSimilar(input, candidates, options = {}) {
65
+ const maxDistance = options.maxDistance ?? 3;
66
+ const maxDistanceRatio = options.maxDistanceRatio ?? .5;
67
+ const maxSuggestions = options.maxSuggestions ?? 3;
68
+ const caseSensitive = options.caseSensitive ?? false;
69
+ if (input.length === 0) return [];
70
+ const normalizedInput = caseSensitive ? input : input.toLowerCase();
71
+ const matches = [];
72
+ for (const candidate of candidates) {
73
+ const normalizedCandidate = caseSensitive ? candidate : candidate.toLowerCase();
74
+ const distance = levenshteinDistance(normalizedInput, normalizedCandidate);
75
+ if (distance === 0) return [candidate];
76
+ const distanceRatio = distance / input.length;
77
+ if (distance <= maxDistance && distanceRatio <= maxDistanceRatio) matches.push({
78
+ candidate,
79
+ distance
80
+ });
81
+ }
82
+ matches.sort((a, b) => {
83
+ if (a.distance !== b.distance) return a.distance - b.distance;
84
+ const lengthDiffA = Math.abs(a.candidate.length - input.length);
85
+ const lengthDiffB = Math.abs(b.candidate.length - input.length);
86
+ if (lengthDiffA !== lengthDiffB) return lengthDiffA - lengthDiffB;
87
+ return a.candidate.localeCompare(b.candidate);
88
+ });
89
+ return matches.slice(0, maxSuggestions).map((m) => m.candidate);
90
+ }
91
+ /**
92
+ * Creates a suggestion message for a mismatched option/command.
93
+ *
94
+ * This function formats suggestions in a user-friendly way:
95
+ * - No suggestions: returns empty message
96
+ * - One suggestion: "Did you mean `option`?"
97
+ * - Multiple suggestions: "Did you mean one of these?\n option1\n option2"
98
+ *
99
+ * @param suggestions List of similar valid options/commands
100
+ * @returns A Message array with suggestion text
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * createSuggestionMessage(["--verbose", "--version"]);
105
+ * // returns message parts for:
106
+ * // "Did you mean one of these?
107
+ * // --verbose
108
+ * // --version"
109
+ *
110
+ * createSuggestionMessage(["--verbose"]);
111
+ * // returns message parts for:
112
+ * // "Did you mean `--verbose`?"
113
+ *
114
+ * createSuggestionMessage([]);
115
+ * // returns []
116
+ * ```
117
+ */
118
+ function createSuggestionMessage(suggestions) {
119
+ if (suggestions.length === 0) return [];
120
+ if (suggestions.length === 1) return message`Did you mean ${optionName(suggestions[0])}?`;
121
+ const messageParts = [text("Did you mean one of these?")];
122
+ for (const suggestion of suggestions) {
123
+ messageParts.push(text("\n "));
124
+ messageParts.push(optionName(suggestion));
125
+ }
126
+ return messageParts;
127
+ }
128
+ /**
129
+ * Creates an error message with suggestions for similar options or commands.
130
+ *
131
+ * This is a convenience function that combines the functionality of
132
+ * `findSimilar()` and `createSuggestionMessage()` to generate user-friendly
133
+ * error messages with "Did you mean?" suggestions.
134
+ *
135
+ * @param baseError The base error message to display
136
+ * @param invalidInput The invalid option or command name that the user typed
137
+ * @param usage The usage information to extract available options/commands from
138
+ * @param type What type of names to suggest ("option", "command", or "both")
139
+ * @returns A message combining the base error with suggestions, or just the
140
+ * base error if no similar names are found
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const baseError = message`No matched option for ${optionName("--verbos")}.`;
145
+ * const error = createErrorWithSuggestions(
146
+ * baseError,
147
+ * "--verbos",
148
+ * context.usage,
149
+ * "option"
150
+ * );
151
+ * // Returns: "No matched option for `--verbos`.\nDid you mean `--verbose`?"
152
+ * ```
153
+ *
154
+ * @since 0.7.0
155
+ */
156
+ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both") {
157
+ const candidates = /* @__PURE__ */ new Set();
158
+ if (type === "option" || type === "both") for (const name of extractOptionNames(usage)) candidates.add(name);
159
+ if (type === "command" || type === "both") for (const name of extractCommandNames(usage)) candidates.add(name);
160
+ const suggestions = findSimilar(invalidInput, candidates, {
161
+ maxDistance: 3,
162
+ maxDistanceRatio: .5,
163
+ maxSuggestions: 3,
164
+ caseSensitive: false
165
+ });
166
+ const suggestionMsg = createSuggestionMessage(suggestions);
167
+ return suggestionMsg.length > 0 ? [
168
+ ...baseError,
169
+ text("\n\n"),
170
+ ...suggestionMsg
171
+ ] : baseError;
172
+ }
173
+
174
+ //#endregion
175
+ export { createErrorWithSuggestions };
package/dist/usage.cjs CHANGED
@@ -23,6 +23,7 @@
23
23
  function extractOptionNames(usage) {
24
24
  const names = /* @__PURE__ */ new Set();
25
25
  function traverseUsage(terms) {
26
+ if (!terms || !Array.isArray(terms)) return;
26
27
  for (const term of terms) if (term.type === "option") for (const name of term.names) names.add(name);
27
28
  else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
28
29
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
@@ -31,6 +32,37 @@ function extractOptionNames(usage) {
31
32
  return names;
32
33
  }
33
34
  /**
35
+ * Extracts all command names from a Usage array.
36
+ *
37
+ * This function recursively traverses the usage structure and collects
38
+ * all command names, similar to {@link extractOptionNames}.
39
+ *
40
+ * @param usage The usage structure to extract command names from
41
+ * @returns A Set of all command names found in the usage structure
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const usage: Usage = [
46
+ * { type: "command", name: "build" },
47
+ * { type: "command", name: "test" },
48
+ * ];
49
+ * const names = extractCommandNames(usage);
50
+ * // names = Set(["build", "test"])
51
+ * ```
52
+ * @since 0.7.0
53
+ */
54
+ function extractCommandNames(usage) {
55
+ const names = /* @__PURE__ */ new Set();
56
+ function traverseUsage(terms) {
57
+ if (!terms || !Array.isArray(terms)) return;
58
+ for (const term of terms) if (term.type === "command") names.add(term.name);
59
+ else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
60
+ else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
61
+ }
62
+ traverseUsage(usage);
63
+ return names;
64
+ }
65
+ /**
34
66
  * Formats a usage description into a human-readable string representation
35
67
  * suitable for command-line help text.
36
68
  *
@@ -267,6 +299,7 @@ function* formatUsageTermInternal(term, options) {
267
299
  }
268
300
 
269
301
  //#endregion
302
+ exports.extractCommandNames = extractCommandNames;
270
303
  exports.extractOptionNames = extractOptionNames;
271
304
  exports.formatUsage = formatUsage;
272
305
  exports.formatUsageTerm = formatUsageTerm;
package/dist/usage.d.cts CHANGED
@@ -137,6 +137,27 @@ type Usage = readonly UsageTerm[];
137
137
  * ```
138
138
  */
139
139
  declare function extractOptionNames(usage: Usage): Set<string>;
140
+ /**
141
+ * Extracts all command names from a Usage array.
142
+ *
143
+ * This function recursively traverses the usage structure and collects
144
+ * all command names, similar to {@link extractOptionNames}.
145
+ *
146
+ * @param usage The usage structure to extract command names from
147
+ * @returns A Set of all command names found in the usage structure
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const usage: Usage = [
152
+ * { type: "command", name: "build" },
153
+ * { type: "command", name: "test" },
154
+ * ];
155
+ * const names = extractCommandNames(usage);
156
+ * // names = Set(["build", "test"])
157
+ * ```
158
+ * @since 0.7.0
159
+ */
160
+ declare function extractCommandNames(usage: Usage): Set<string>;
140
161
  /**
141
162
  * Options for formatting usage descriptions.
142
163
  */
@@ -235,4 +256,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
235
256
  */
236
257
  declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
237
258
  //#endregion
238
- export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
259
+ export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
package/dist/usage.d.ts CHANGED
@@ -137,6 +137,27 @@ type Usage = readonly UsageTerm[];
137
137
  * ```
138
138
  */
139
139
  declare function extractOptionNames(usage: Usage): Set<string>;
140
+ /**
141
+ * Extracts all command names from a Usage array.
142
+ *
143
+ * This function recursively traverses the usage structure and collects
144
+ * all command names, similar to {@link extractOptionNames}.
145
+ *
146
+ * @param usage The usage structure to extract command names from
147
+ * @returns A Set of all command names found in the usage structure
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const usage: Usage = [
152
+ * { type: "command", name: "build" },
153
+ * { type: "command", name: "test" },
154
+ * ];
155
+ * const names = extractCommandNames(usage);
156
+ * // names = Set(["build", "test"])
157
+ * ```
158
+ * @since 0.7.0
159
+ */
160
+ declare function extractCommandNames(usage: Usage): Set<string>;
140
161
  /**
141
162
  * Options for formatting usage descriptions.
142
163
  */
@@ -235,4 +256,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
235
256
  */
236
257
  declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
237
258
  //#endregion
238
- export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
259
+ export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
package/dist/usage.js CHANGED
@@ -22,6 +22,7 @@
22
22
  function extractOptionNames(usage) {
23
23
  const names = /* @__PURE__ */ new Set();
24
24
  function traverseUsage(terms) {
25
+ if (!terms || !Array.isArray(terms)) return;
25
26
  for (const term of terms) if (term.type === "option") for (const name of term.names) names.add(name);
26
27
  else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
27
28
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
@@ -30,6 +31,37 @@ function extractOptionNames(usage) {
30
31
  return names;
31
32
  }
32
33
  /**
34
+ * Extracts all command names from a Usage array.
35
+ *
36
+ * This function recursively traverses the usage structure and collects
37
+ * all command names, similar to {@link extractOptionNames}.
38
+ *
39
+ * @param usage The usage structure to extract command names from
40
+ * @returns A Set of all command names found in the usage structure
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const usage: Usage = [
45
+ * { type: "command", name: "build" },
46
+ * { type: "command", name: "test" },
47
+ * ];
48
+ * const names = extractCommandNames(usage);
49
+ * // names = Set(["build", "test"])
50
+ * ```
51
+ * @since 0.7.0
52
+ */
53
+ function extractCommandNames(usage) {
54
+ const names = /* @__PURE__ */ new Set();
55
+ function traverseUsage(terms) {
56
+ if (!terms || !Array.isArray(terms)) return;
57
+ for (const term of terms) if (term.type === "command") names.add(term.name);
58
+ else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
59
+ else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
60
+ }
61
+ traverseUsage(usage);
62
+ return names;
63
+ }
64
+ /**
33
65
  * Formats a usage description into a human-readable string representation
34
66
  * suitable for command-line help text.
35
67
  *
@@ -266,4 +298,4 @@ function* formatUsageTermInternal(term, options) {
266
298
  }
267
299
 
268
300
  //#endregion
269
- export { extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
301
+ export { extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.7.0-dev.127+2f66c91d",
3
+ "version": "0.7.0-dev.128+2a772953",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",