@optique/core 1.2.0-dev.2241 → 1.2.0-dev.2242

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.
@@ -251,6 +251,46 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
251
251
  ] : baseError;
252
252
  }
253
253
  /**
254
+ * Appends a "Did you mean …?" hint to a base error message when the input
255
+ * is close to one of the candidate values.
256
+ *
257
+ * This helper is meant for closed-set value parsers (e.g. `choice()`) that
258
+ * want to surface near-match suggestions without re-implementing distance
259
+ * logic themselves. It wraps {@link findSimilar} and
260
+ * {@link createSuggestionMessage} and returns the `base` message unchanged
261
+ * when no candidates are close enough.
262
+ *
263
+ * @param base The base error message to display.
264
+ * @param input The invalid input the user typed.
265
+ * @param candidates The list of valid candidate strings to suggest from.
266
+ * @param options Optional thresholds; defaults to
267
+ * {@link DEFAULT_FIND_SIMILAR_OPTIONS}.
268
+ * @returns `base` with a "Did you mean …?" line appended when a close
269
+ * candidate is found, or `base` unchanged when none are found.
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * const base = message`Invalid color: ${input}.`;
274
+ * appendValueHint(base, input, ["red", "green", "blue"]);
275
+ * // → "Invalid color: redd.\n\nDid you mean `red`?"
276
+ * ```
277
+ *
278
+ * @since 1.2.0
279
+ */
280
+ function appendValueHint(base, input, candidates, options) {
281
+ const suggestions = findSimilar(input, candidates, options != null ? {
282
+ ...DEFAULT_FIND_SIMILAR_OPTIONS,
283
+ ...options
284
+ } : void 0);
285
+ const suggestionMsg = createSuggestionMessage(suggestions);
286
+ return suggestionMsg.length > 0 ? [
287
+ ...base,
288
+ require_message.lineBreak(),
289
+ require_message.lineBreak(),
290
+ ...suggestionMsg
291
+ ] : base;
292
+ }
293
+ /**
254
294
  * Creates a unique key for a suggestion to enable deduplication.
255
295
  *
256
296
  * For literal suggestions, the text itself is used as the key.
@@ -313,6 +353,7 @@ function deduplicateSuggestions(suggestions) {
313
353
 
314
354
  //#endregion
315
355
  exports.DEFAULT_FIND_SIMILAR_OPTIONS = DEFAULT_FIND_SIMILAR_OPTIONS;
356
+ exports.appendValueHint = appendValueHint;
316
357
  exports.createErrorWithSuggestions = createErrorWithSuggestions;
317
358
  exports.createSuggestionMessage = createSuggestionMessage;
318
359
  exports.deduplicateSuggestions = deduplicateSuggestions;
@@ -154,6 +154,34 @@ declare function expandCommandAliasSuggestions(usage: Usage, suggestions: readon
154
154
  * @since 0.7.0
155
155
  */
156
156
  declare function createErrorWithSuggestions(baseError: Message, invalidInput: string, usage: Usage, type?: "option" | "command" | "both", customFormatter?: (suggestions: readonly string[]) => Message): Message;
157
+ /**
158
+ * Appends a "Did you mean …?" hint to a base error message when the input
159
+ * is close to one of the candidate values.
160
+ *
161
+ * This helper is meant for closed-set value parsers (e.g. `choice()`) that
162
+ * want to surface near-match suggestions without re-implementing distance
163
+ * logic themselves. It wraps {@link findSimilar} and
164
+ * {@link createSuggestionMessage} and returns the `base` message unchanged
165
+ * when no candidates are close enough.
166
+ *
167
+ * @param base The base error message to display.
168
+ * @param input The invalid input the user typed.
169
+ * @param candidates The list of valid candidate strings to suggest from.
170
+ * @param options Optional thresholds; defaults to
171
+ * {@link DEFAULT_FIND_SIMILAR_OPTIONS}.
172
+ * @returns `base` with a "Did you mean …?" line appended when a close
173
+ * candidate is found, or `base` unchanged when none are found.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const base = message`Invalid color: ${input}.`;
178
+ * appendValueHint(base, input, ["red", "green", "blue"]);
179
+ * // → "Invalid color: redd.\n\nDid you mean `red`?"
180
+ * ```
181
+ *
182
+ * @since 1.2.0
183
+ */
184
+ declare function appendValueHint(base: Message, input: string, candidates: readonly string[], options?: Readonly<Pick<FindSimilarOptions, "maxDistance" | "maxSuggestions">>): Message;
157
185
  /**
158
186
  * Removes duplicate suggestions from an array while preserving order.
159
187
  *
@@ -185,4 +213,4 @@ declare function createErrorWithSuggestions(baseError: Message, invalidInput: st
185
213
  */
186
214
  declare function deduplicateSuggestions(suggestions: readonly Suggestion[]): Suggestion[];
187
215
  //#endregion
188
- export { DEFAULT_FIND_SIMILAR_OPTIONS, FindSimilarOptions, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
216
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, FindSimilarOptions, appendValueHint, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
@@ -154,6 +154,34 @@ declare function expandCommandAliasSuggestions(usage: Usage, suggestions: readon
154
154
  * @since 0.7.0
155
155
  */
156
156
  declare function createErrorWithSuggestions(baseError: Message, invalidInput: string, usage: Usage, type?: "option" | "command" | "both", customFormatter?: (suggestions: readonly string[]) => Message): Message;
157
+ /**
158
+ * Appends a "Did you mean …?" hint to a base error message when the input
159
+ * is close to one of the candidate values.
160
+ *
161
+ * This helper is meant for closed-set value parsers (e.g. `choice()`) that
162
+ * want to surface near-match suggestions without re-implementing distance
163
+ * logic themselves. It wraps {@link findSimilar} and
164
+ * {@link createSuggestionMessage} and returns the `base` message unchanged
165
+ * when no candidates are close enough.
166
+ *
167
+ * @param base The base error message to display.
168
+ * @param input The invalid input the user typed.
169
+ * @param candidates The list of valid candidate strings to suggest from.
170
+ * @param options Optional thresholds; defaults to
171
+ * {@link DEFAULT_FIND_SIMILAR_OPTIONS}.
172
+ * @returns `base` with a "Did you mean …?" line appended when a close
173
+ * candidate is found, or `base` unchanged when none are found.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const base = message`Invalid color: ${input}.`;
178
+ * appendValueHint(base, input, ["red", "green", "blue"]);
179
+ * // → "Invalid color: redd.\n\nDid you mean `red`?"
180
+ * ```
181
+ *
182
+ * @since 1.2.0
183
+ */
184
+ declare function appendValueHint(base: Message, input: string, candidates: readonly string[], options?: Readonly<Pick<FindSimilarOptions, "maxDistance" | "maxSuggestions">>): Message;
157
185
  /**
158
186
  * Removes duplicate suggestions from an array while preserving order.
159
187
  *
@@ -185,4 +213,4 @@ declare function createErrorWithSuggestions(baseError: Message, invalidInput: st
185
213
  */
186
214
  declare function deduplicateSuggestions(suggestions: readonly Suggestion[]): Suggestion[];
187
215
  //#endregion
188
- export { DEFAULT_FIND_SIMILAR_OPTIONS, FindSimilarOptions, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
216
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, FindSimilarOptions, appendValueHint, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
@@ -1,4 +1,4 @@
1
- import { message, optionName, text } from "./message.js";
1
+ import { lineBreak, message, optionName, text } from "./message.js";
2
2
  import { extractCommandNames, extractOptionNames, isSuggestionHidden } from "./usage.js";
3
3
 
4
4
  //#region src/suggestion.ts
@@ -251,6 +251,46 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
251
251
  ] : baseError;
252
252
  }
253
253
  /**
254
+ * Appends a "Did you mean …?" hint to a base error message when the input
255
+ * is close to one of the candidate values.
256
+ *
257
+ * This helper is meant for closed-set value parsers (e.g. `choice()`) that
258
+ * want to surface near-match suggestions without re-implementing distance
259
+ * logic themselves. It wraps {@link findSimilar} and
260
+ * {@link createSuggestionMessage} and returns the `base` message unchanged
261
+ * when no candidates are close enough.
262
+ *
263
+ * @param base The base error message to display.
264
+ * @param input The invalid input the user typed.
265
+ * @param candidates The list of valid candidate strings to suggest from.
266
+ * @param options Optional thresholds; defaults to
267
+ * {@link DEFAULT_FIND_SIMILAR_OPTIONS}.
268
+ * @returns `base` with a "Did you mean …?" line appended when a close
269
+ * candidate is found, or `base` unchanged when none are found.
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * const base = message`Invalid color: ${input}.`;
274
+ * appendValueHint(base, input, ["red", "green", "blue"]);
275
+ * // → "Invalid color: redd.\n\nDid you mean `red`?"
276
+ * ```
277
+ *
278
+ * @since 1.2.0
279
+ */
280
+ function appendValueHint(base, input, candidates, options) {
281
+ const suggestions = findSimilar(input, candidates, options != null ? {
282
+ ...DEFAULT_FIND_SIMILAR_OPTIONS,
283
+ ...options
284
+ } : void 0);
285
+ const suggestionMsg = createSuggestionMessage(suggestions);
286
+ return suggestionMsg.length > 0 ? [
287
+ ...base,
288
+ lineBreak(),
289
+ lineBreak(),
290
+ ...suggestionMsg
291
+ ] : base;
292
+ }
293
+ /**
254
294
  * Creates a unique key for a suggestion to enable deduplication.
255
295
  *
256
296
  * For literal suggestions, the text itself is used as the key.
@@ -312,4 +352,4 @@ function deduplicateSuggestions(suggestions) {
312
352
  }
313
353
 
314
354
  //#endregion
315
- export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
355
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, appendValueHint, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, expandCommandAliasSuggestions, findSimilar, levenshteinDistance };
@@ -139,6 +139,13 @@ function choice(choices, options = {}) {
139
139
  }
140
140
  }
141
141
  const stringInvalidChoice = stringOptions.errors?.invalidChoice;
142
+ const stringSuggest = stringOptions.suggest;
143
+ if (stringSuggest !== void 0) {
144
+ const isValidString = stringSuggest === "nearest" || stringSuggest === "never";
145
+ const isValidObject = typeof stringSuggest === "object" && stringSuggest !== null;
146
+ const isValidFunction = typeof stringSuggest === "function";
147
+ if (!isValidString && !isValidObject && !isValidFunction) throw new TypeError(`Expected suggest to be "nearest", "never", an object, or a function, but got ${typeof stringSuggest}: ${String(stringSuggest)}.`);
148
+ }
142
149
  return {
143
150
  mode: "sync",
144
151
  metavar: metavar$1,
@@ -149,7 +156,7 @@ function choice(choices, options = {}) {
149
156
  const index = normalizedValues.indexOf(normalizedInput);
150
157
  if (index < 0) return {
151
158
  success: false,
152
- error: formatStringChoiceError(input, stringChoices, stringInvalidChoice)
159
+ error: formatStringChoiceError(input, stringChoices, stringInvalidChoice, stringSuggest)
153
160
  };
154
161
  return {
155
162
  success: true,
@@ -256,9 +263,22 @@ function normalizeDecimal(s) {
256
263
  /**
257
264
  * Formats error message for string choice parser.
258
265
  */
259
- function formatStringChoiceError(input, choices, invalidChoice) {
260
- if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice;
261
- return formatDefaultChoiceError(input, choices);
266
+ function formatStringChoiceError(input, choices, invalidChoice, suggest) {
267
+ const base = invalidChoice ? typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice : formatDefaultChoiceError(input, choices);
268
+ if (suggest == null || suggest === "never") return base;
269
+ if (suggest === "nearest") return require_suggestion.appendValueHint(base, input, choices);
270
+ if (typeof suggest === "function") {
271
+ const hints = suggest(input, choices);
272
+ if (!hints || hints.length === 0) return base;
273
+ const suggestionMsg = require_suggestion.createSuggestionMessage(hints);
274
+ return suggestionMsg.length > 0 ? [
275
+ ...base,
276
+ require_message.lineBreak(),
277
+ require_message.lineBreak(),
278
+ ...suggestionMsg
279
+ ] : base;
280
+ }
281
+ return require_suggestion.appendValueHint(base, input, choices, suggest);
262
282
  }
263
283
  /**
264
284
  * Formats error message for number choice parser.
@@ -1,5 +1,6 @@
1
1
  import { NonEmptyString, ensureNonEmptyString, isNonEmptyString } from "./nonempty.cjs";
2
2
  import { Message } from "./message.cjs";
3
+ import { FindSimilarOptions } from "./suggestion.cjs";
3
4
  import { Mode, ModeIterable, ModeValue, Suggestion } from "./internal/parser.cjs";
4
5
 
5
6
  //#region src/valueparser.d.ts
@@ -269,6 +270,25 @@ interface ChoiceOptionsString extends ChoiceOptionsBase {
269
270
  * @default `false`
270
271
  */
271
272
  readonly caseInsensitive?: boolean;
273
+ /**
274
+ * Controls whether the error message for an invalid value includes a
275
+ * "Did you mean …?" hint based on Levenshtein distance.
276
+ *
277
+ * - `"nearest"`: append a hint with the closest valid choice(s) using
278
+ * default distance thresholds. Recommended for small-to-medium
279
+ * enumeration sets.
280
+ * - `{ maxDistance?, maxSuggestions? }`: same as `"nearest"` but with
281
+ * custom thresholds.
282
+ * - `"never"`: disable hints; emit the plain "not one of" error message
283
+ * unchanged.
284
+ * - A function `(input, choices) => readonly string[] | undefined`:
285
+ * custom suggestion logic. Return `undefined` to suppress the hint,
286
+ * or a non-empty array of strings to display as suggestions.
287
+ *
288
+ * @default `"never"` (backward-compatible: no hint is appended)
289
+ * @since 1.2.0
290
+ */
291
+ readonly suggest?: "nearest" | "never" | Readonly<Pick<FindSimilarOptions, "maxDistance" | "maxSuggestions">> | ((input: string, choices: readonly string[]) => readonly string[] | undefined);
272
292
  /**
273
293
  * Custom error messages for choice parsing failures.
274
294
  * @since 0.5.0
@@ -1,5 +1,6 @@
1
1
  import { NonEmptyString, ensureNonEmptyString, isNonEmptyString } from "./nonempty.js";
2
2
  import { Message } from "./message.js";
3
+ import { FindSimilarOptions } from "./suggestion.js";
3
4
  import { Mode, ModeIterable, ModeValue, Suggestion } from "./internal/parser.js";
4
5
 
5
6
  //#region src/valueparser.d.ts
@@ -269,6 +270,25 @@ interface ChoiceOptionsString extends ChoiceOptionsBase {
269
270
  * @default `false`
270
271
  */
271
272
  readonly caseInsensitive?: boolean;
273
+ /**
274
+ * Controls whether the error message for an invalid value includes a
275
+ * "Did you mean …?" hint based on Levenshtein distance.
276
+ *
277
+ * - `"nearest"`: append a hint with the closest valid choice(s) using
278
+ * default distance thresholds. Recommended for small-to-medium
279
+ * enumeration sets.
280
+ * - `{ maxDistance?, maxSuggestions? }`: same as `"nearest"` but with
281
+ * custom thresholds.
282
+ * - `"never"`: disable hints; emit the plain "not one of" error message
283
+ * unchanged.
284
+ * - A function `(input, choices) => readonly string[] | undefined`:
285
+ * custom suggestion logic. Return `undefined` to suppress the hint,
286
+ * or a non-empty array of strings to display as suggestions.
287
+ *
288
+ * @default `"never"` (backward-compatible: no hint is appended)
289
+ * @since 1.2.0
290
+ */
291
+ readonly suggest?: "nearest" | "never" | Readonly<Pick<FindSimilarOptions, "maxDistance" | "maxSuggestions">> | ((input: string, choices: readonly string[]) => readonly string[] | undefined);
272
292
  /**
273
293
  * Custom error messages for choice parsing failures.
274
294
  * @since 0.5.0
@@ -1,6 +1,6 @@
1
1
  import { cloneMessage, lineBreak, message, metavar, text, valueSet } from "./message.js";
2
2
  import { isDerivedValueParser } from "./internal/dependency.js";
3
- import { deduplicateSuggestions } from "./suggestion.js";
3
+ import { appendValueHint, createSuggestionMessage, deduplicateSuggestions } from "./suggestion.js";
4
4
  import { ensureNonEmptyString, isNonEmptyString } from "./nonempty.js";
5
5
 
6
6
  //#region src/valueparser.ts
@@ -139,6 +139,13 @@ function choice(choices, options = {}) {
139
139
  }
140
140
  }
141
141
  const stringInvalidChoice = stringOptions.errors?.invalidChoice;
142
+ const stringSuggest = stringOptions.suggest;
143
+ if (stringSuggest !== void 0) {
144
+ const isValidString = stringSuggest === "nearest" || stringSuggest === "never";
145
+ const isValidObject = typeof stringSuggest === "object" && stringSuggest !== null;
146
+ const isValidFunction = typeof stringSuggest === "function";
147
+ if (!isValidString && !isValidObject && !isValidFunction) throw new TypeError(`Expected suggest to be "nearest", "never", an object, or a function, but got ${typeof stringSuggest}: ${String(stringSuggest)}.`);
148
+ }
142
149
  return {
143
150
  mode: "sync",
144
151
  metavar: metavar$1,
@@ -149,7 +156,7 @@ function choice(choices, options = {}) {
149
156
  const index = normalizedValues.indexOf(normalizedInput);
150
157
  if (index < 0) return {
151
158
  success: false,
152
- error: formatStringChoiceError(input, stringChoices, stringInvalidChoice)
159
+ error: formatStringChoiceError(input, stringChoices, stringInvalidChoice, stringSuggest)
153
160
  };
154
161
  return {
155
162
  success: true,
@@ -256,9 +263,22 @@ function normalizeDecimal(s) {
256
263
  /**
257
264
  * Formats error message for string choice parser.
258
265
  */
259
- function formatStringChoiceError(input, choices, invalidChoice) {
260
- if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice;
261
- return formatDefaultChoiceError(input, choices);
266
+ function formatStringChoiceError(input, choices, invalidChoice, suggest) {
267
+ const base = invalidChoice ? typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice : formatDefaultChoiceError(input, choices);
268
+ if (suggest == null || suggest === "never") return base;
269
+ if (suggest === "nearest") return appendValueHint(base, input, choices);
270
+ if (typeof suggest === "function") {
271
+ const hints = suggest(input, choices);
272
+ if (!hints || hints.length === 0) return base;
273
+ const suggestionMsg = createSuggestionMessage(hints);
274
+ return suggestionMsg.length > 0 ? [
275
+ ...base,
276
+ lineBreak(),
277
+ lineBreak(),
278
+ ...suggestionMsg
279
+ ] : base;
280
+ }
281
+ return appendValueHint(base, input, choices, suggest);
262
282
  }
263
283
  /**
264
284
  * Formats error message for number choice parser.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.2.0-dev.2241",
3
+ "version": "1.2.0-dev.2242",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -179,6 +179,14 @@
179
179
  "import": "./dist/program.js",
180
180
  "require": "./dist/program.cjs"
181
181
  },
182
+ "./suggestion": {
183
+ "types": {
184
+ "import": "./dist/suggestion.d.ts",
185
+ "require": "./dist/suggestion.d.cts"
186
+ },
187
+ "import": "./dist/suggestion.js",
188
+ "require": "./dist/suggestion.cjs"
189
+ },
182
190
  "./usage": {
183
191
  "types": {
184
192
  "import": "./dist/usage.d.ts",
@@ -208,7 +216,7 @@
208
216
  "fast-check": "^4.7.0",
209
217
  "tsdown": "^0.13.0",
210
218
  "typescript": "^5.8.3",
211
- "@optique/env": "1.2.0-dev.2241+d223bc07"
219
+ "@optique/env": "1.2.0-dev.2242+4f7a5e89"
212
220
  },
213
221
  "scripts": {
214
222
  "build": "tsdown",