cli-kiss 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +8 -3
  2. package/dist/index.d.ts +200 -190
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/docs/.vitepress/config.mts +1 -1
  6. package/docs/.vitepress/theme/Layout.vue +16 -0
  7. package/docs/.vitepress/theme/index.ts +5 -1
  8. package/docs/.vitepress/theme/style.css +5 -1
  9. package/docs/guide/01_getting_started.md +2 -2
  10. package/docs/guide/02_commands.md +3 -3
  11. package/docs/guide/03_options.md +11 -11
  12. package/docs/guide/04_positionals.md +9 -9
  13. package/docs/guide/05_input_types.md +17 -16
  14. package/docs/guide/06_run_as_cli.md +1 -1
  15. package/docs/index.md +2 -2
  16. package/docs/public/favicon.ico +0 -0
  17. package/docs/public/logo.png +0 -0
  18. package/package.json +1 -1
  19. package/src/index.ts +1 -1
  20. package/src/lib/Command.ts +51 -40
  21. package/src/lib/Operation.ts +41 -25
  22. package/src/lib/Option.ts +198 -127
  23. package/src/lib/Positional.ts +51 -25
  24. package/src/lib/Reader.ts +188 -226
  25. package/src/lib/Run.ts +20 -9
  26. package/src/lib/Suggest.ts +78 -0
  27. package/src/lib/Type.ts +178 -154
  28. package/src/lib/Typo.ts +58 -55
  29. package/src/lib/Usage.ts +12 -12
  30. package/tests/unit.Reader.commons.ts +86 -123
  31. package/tests/unit.Reader.parsings.ts +14 -26
  32. package/tests/unit.Reader.shortBig.ts +75 -101
  33. package/tests/unit.command.aliases.ts +88 -0
  34. package/tests/unit.command.execute.ts +6 -6
  35. package/tests/unit.command.usage.ts +19 -13
  36. package/tests/unit.fuzzed.alternatives.ts +35 -26
  37. package/tests/unit.runner.colors.ts +8 -33
  38. package/tests/unit.runner.cycle.ts +141 -156
  39. package/tests/unit.runner.errors.ts +25 -22
  40. package/docs/public/hero.png +0 -0
  41. package/src/lib/Similarity.ts +0 -41
  42. package/tests/unit.Reader.aliases.ts +0 -62
@@ -0,0 +1,78 @@
1
+ import { TypoSegment, TypoString, TypoText } from "./Typo";
2
+
3
+ export function suggestTextPushMessage(
4
+ text: TypoText,
5
+ query: string,
6
+ candidates: Array<{ reference: string; hint: TypoSegment }>,
7
+ ) {
8
+ const reasonableHints = suggestReasonablePayloads(
9
+ query,
10
+ candidates.map(({ reference, hint }) => ({ reference, payload: hint })),
11
+ );
12
+ if (reasonableHints.length === 0) {
13
+ return;
14
+ }
15
+ text.push(new TypoString(" Did you mean: "));
16
+ text.pushJoined(reasonableHints, new TypoString(", "), 3);
17
+ text.push(new TypoString(` ?`));
18
+ }
19
+
20
+ function suggestReasonablePayloads<Payload>(
21
+ query: string,
22
+ candidates: Array<{ reference: string; payload: Payload }>,
23
+ ): Array<Payload> {
24
+ if (candidates.length === 0) {
25
+ return [];
26
+ }
27
+ const sortedAlternatives = computeAndSortByDivergences(query, candidates);
28
+ const divergenceThreshold = sortedAlternatives[0]!.divergence + 0.25;
29
+ const acceptablePayloads = new Array<Payload>();
30
+ for (const { divergence, payload } of sortedAlternatives) {
31
+ if (divergence > divergenceThreshold) {
32
+ break;
33
+ }
34
+ acceptablePayloads.push(payload);
35
+ }
36
+ return acceptablePayloads;
37
+ }
38
+
39
+ function computeAndSortByDivergences<Payload>(
40
+ query: string,
41
+ candidates: Array<{ reference: string; payload: Payload }>,
42
+ ): Array<{ divergence: number; payload: Payload }> {
43
+ const queryNormalized = query.toLowerCase().slice(0, 100);
44
+ const scored = candidates.map(({ reference, payload }) => {
45
+ const referenceNormalized = reference.toLowerCase().slice(0, 100);
46
+ const divergence =
47
+ distanceDamerauLevenshtein(queryNormalized, referenceNormalized) /
48
+ Math.max(queryNormalized.length, referenceNormalized.length);
49
+ return { divergence, reference, payload };
50
+ });
51
+ return scored.sort((a, b) => a.divergence - b.divergence);
52
+ }
53
+
54
+ function distanceDamerauLevenshtein(a: string, b: string): number {
55
+ const m = a.length;
56
+ const n = b.length;
57
+ const dp = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0));
58
+ for (let i = 0; i <= m; i++) {
59
+ dp[i]![0] = i;
60
+ }
61
+ for (let j = 0; j <= n; j++) {
62
+ dp[0]![j] = j;
63
+ }
64
+ for (let i = 1; i <= m; i++) {
65
+ for (let j = 1; j <= n; j++) {
66
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
67
+ dp[i]![j] = Math.min(
68
+ dp[i - 1]![j]! + 1,
69
+ dp[i]![j - 1]! + 1,
70
+ dp[i - 1]![j - 1]! + cost,
71
+ );
72
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
73
+ dp[i]![j] = Math.min(dp[i]![j]!, dp[i - 2]![j - 2]! + cost);
74
+ }
75
+ }
76
+ }
77
+ return dp[m]![n]!;
78
+ }
package/src/lib/Type.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { statSync } from "fs";
2
- import { similaritySort } from "./Similarity";
2
+ import { suggestTextPushMessage } from "./Suggest";
3
3
  import {
4
4
  TypoError,
5
5
  TypoString,
@@ -12,27 +12,54 @@ import {
12
12
  * Decodes a raw CLI string into a typed value.
13
13
  * A pair of a human-readable `content` name and a `decoder` function.
14
14
  *
15
- * Built-in: {@link type}, {@link typeBoolean}, {@link typeNumber},
16
- * {@link typeInteger}, {@link typeDatetime}, {@link typeUrl}.
17
- * Composite: {@link typeChoice}, {@link typeConverted}, {@link typeTuple}, {@link typeList}.
15
+ * Primitive Types:
16
+ * - {@link typeString}
17
+ * - {@link typeBoolean}
18
+ * - {@link typeNumber},
19
+ * - {@link typeInteger}
20
+ * - {@link typeDatetime}
21
+ * - {@link typeUrl}
22
+ * - {@link typePath}
23
+ * - {@link typeChoice}
24
+ *
25
+ * Composed Types:
26
+ * - {@link typeMapped}
27
+ * - {@link typeTuple}
28
+ * - {@link typeList}
18
29
  *
19
30
  * @typeParam Value - Type produced by the decoder.
20
31
  */
21
32
  export type Type<Value> = {
22
33
  /**
23
- * Human-readable name shown in help and errors (e.g. `"name"`, `"number"`).
34
+ * Human-readable name shown in help and errors (e.g. `"name"`, `"context"`).
24
35
  */
25
- content: string;
36
+ content: string; // TODO - add an enforcement mechanism for casing ?
26
37
  /**
27
38
  * Decodes a raw CLI string into `Value`.
28
39
  *
29
40
  * @param input - Raw string from the command line.
30
41
  * @returns The decoded value.
31
- * @throws {@link TypoError} on invalid input.
42
+ * @throws on invalid input.
32
43
  */
33
44
  decoder(input: string): Value;
34
45
  };
35
46
 
47
+ /**
48
+ * A named type that accepts any string as input.
49
+ * @param name - Name shown in help and errors (e.g. `"my-value"`).
50
+ * @example
51
+ * ```ts
52
+ * typeString("greeting").decoder("hello") // → "hello"
53
+ * typeString("greeting").decoder("") // → ""
54
+ * ```
55
+ */
56
+ export function typeString(name?: string): Type<string> {
57
+ return {
58
+ content: name ?? "string",
59
+ decoder: (input: string) => input,
60
+ };
61
+ }
62
+
36
63
  /**
37
64
  * Decodes a string to `boolean` (case-insensitive).
38
65
  * Used by {@link optionFlag} for `--flag=<value>`.
@@ -41,10 +68,11 @@ export type Type<Value> = {
41
68
  * ```ts
42
69
  * typeBoolean("flag").decoder("true") // → true
43
70
  * typeBoolean("flag").decoder("yes") // → true
44
- * typeBoolean("flag").decoder("y") // → true
45
- * typeBoolean("flag").decoder("false") // → false
46
- * typeBoolean("flag").decoder("no") // → false
71
+ * typeBoolean("flag").decoder("Y") // → true
72
+ * typeBoolean("flag").decoder("FALSE") // → false
73
+ * typeBoolean("flag").decoder("NO") // → false
47
74
  * typeBoolean("flag").decoder("n") // → false
75
+ * typeBoolean("flag").decoder("maybe") // throws
48
76
  * ```
49
77
  */
50
78
  export function typeBoolean(name?: string): Type<boolean> {
@@ -62,39 +90,9 @@ export function typeBoolean(name?: string): Type<boolean> {
62
90
  },
63
91
  };
64
92
  }
65
- export const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
66
- export const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
67
-
68
- /**
69
- * Parses a date/time string via `Date.parse`.
70
- * Accepts any format supported by `Date.parse`, including ISO 8601.
71
- *
72
- * @example
73
- * ```ts
74
- * typeDatetime("my-datetime").decoder("2024-01-15") // → Date object for 2024-01-15
75
- * typeDatetime("my-datetime").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
76
- * typeDatetime("my-datetime").decoder("not a date") // throws TypoError
77
- * ```
78
- */
79
- export function typeDatetime(name?: string): Type<Date> {
80
- return {
81
- content: name ?? "datetime",
82
- decoder(input: string) {
83
- try {
84
- const timestampMs = Date.parse(input);
85
- if (isNaN(timestampMs)) {
86
- throw new Error();
87
- }
88
- return new Date(timestampMs);
89
- } catch {
90
- throwInvalidValue("a valid ISO_8601 datetime", input);
91
- }
92
- },
93
- };
94
- }
95
93
 
96
94
  /**
97
- * Parses a string to `number` via `Number()`; `NaN` throws {@link TypoError}.
95
+ * Parses a string to `number` via `Number()`; `NaN` throws.
98
96
  *
99
97
  * @example
100
98
  * ```ts
@@ -122,7 +120,7 @@ export function typeNumber(name?: string): Type<number> {
122
120
 
123
121
  /**
124
122
  * Parses an integer string to `bigint` via `BigInt()`.
125
- * Floats and non-numeric strings throw {@link TypoError}.
123
+ * Floats and non-numeric strings throws.
126
124
  *
127
125
  * @example
128
126
  * ```ts
@@ -145,118 +143,68 @@ export function typeInteger(name?: string): Type<bigint> {
145
143
  }
146
144
 
147
145
  /**
148
- * Parses an absolute URL string to a `URL` object.
149
- * Relative or malformed URLs throw {@link TypoError}.
146
+ * Parses a date/time string via `Date.parse`.
147
+ * Accepts any format supported by `Date.parse`, including ISO 8601.
150
148
  *
151
149
  * @example
152
150
  * ```ts
153
- * typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
154
- * typeUrl("my-url").decoder("not-a-url") // throws
151
+ * typeDatetime("start").decoder("2024-01-15") // → Date object for 2024-01-15
152
+ * typeDatetime("start").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
153
+ * typeDatetime("start").decoder("not a date") // throws
155
154
  * ```
156
155
  */
157
- export function typeUrl(name?: string): Type<URL> {
156
+ export function typeDatetime(name?: string): Type<Date> {
158
157
  return {
159
- content: name ?? "url",
158
+ content: name ?? "datetime",
160
159
  decoder(input: string) {
161
160
  try {
162
- return new URL(input);
161
+ const timestampMs = Date.parse(input);
162
+ if (isNaN(timestampMs)) {
163
+ throw new Error();
164
+ }
165
+ return new Date(timestampMs);
163
166
  } catch {
164
- throwInvalidValue("an URL", input);
167
+ throwInvalidValue("a valid ISO_8601 datetime", input);
165
168
  }
166
169
  },
167
170
  };
168
171
  }
169
172
 
170
173
  /**
171
- * A named type that accepts any string as input.
172
- * @param name - Name shown in help and errors (e.g. `"my-value"`).
173
- * @example
174
- * ```ts
175
- * type("greeting").decoder("hello") // → "hello"
176
- * type("greeting").decoder("") // → ""
177
- * ```
178
- */
179
- export function type(name?: string): Type<string> {
180
- return {
181
- content: name ?? "string",
182
- decoder: (input: string) => input,
183
- };
184
- }
185
-
186
- /**
187
- * Chains `before`'s decoder with an `after` transformation.
188
- * `before` errors are prefixed with `"from: <content>"` for traceability.
189
- *
190
- * @typeParam Before - Intermediate type from `before.decoder`.
191
- * @typeParam After - Final type from `after.decoder`.
192
- *
193
- * @param name - Name shown in help and errors (e.g. `"my-value"`).
194
- * @param before - Base type to decode the raw string.
195
- * @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
196
- * @returns A {@link Type}`<After>`.
174
+ * Parses an absolute URL string to a `URL` object.
175
+ * Relative or malformed URLs throws.
197
176
  *
198
177
  * @example
199
178
  * ```ts
200
- * const typePort = typeConverted("port", typeNumber(), (n) => {
201
- * if (n < 1 || n > 65535) throw new Error("Out of range");
202
- * return n;
203
- * });
204
- * // "--port 8080" → 8080
205
- * // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
179
+ * typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
180
+ * typeUrl("my-url").decoder("not-a-url") // throws
206
181
  * ```
207
182
  */
208
- export function typeConverted<Before, After>(
209
- name: string,
210
- before: Type<Before>,
211
- mapper: (value: Before) => After,
212
- ): Type<After> {
213
- return {
214
- content: name,
215
- decoder: (input: string) => {
216
- return mapper(
217
- TypoError.tryWithContext(
218
- () => before.decoder(input),
219
- () =>
220
- new TypoText(
221
- new TypoString("from: "),
222
- new TypoString(before.content, typoStyleLogic),
223
- ),
224
- ),
225
- );
226
- },
227
- };
228
- }
229
-
230
- /**
231
- * Adds a name to a {@link Type} for clearer error messages and help text.
232
- *
233
- * @param name - Name to use for the type.
234
- * @param type - Base type to name.
235
- * @returns A {@link Type} with the given name.
236
- */
237
- export function typeRenamed<Value>(
238
- type: Type<Value>,
239
- name: string,
240
- ): Type<Value> {
183
+ export function typeUrl(name?: string): Type<URL> {
241
184
  return {
242
- content: name,
243
- decoder: (input: string) => {
244
- return TypoError.tryWithContext(
245
- () => type.decoder(input),
246
- () =>
247
- new TypoText(
248
- new TypoString("from: "),
249
- new TypoString(type.content, typoStyleLogic),
250
- ),
251
- );
185
+ content: name ?? "url",
186
+ decoder(input: string) {
187
+ try {
188
+ return new URL(input);
189
+ } catch {
190
+ throwInvalidValue("an URL", input);
191
+ }
252
192
  },
253
193
  };
254
194
  }
255
195
 
256
196
  /**
257
197
  * Creates a {@link Type} for filesystem paths with optional existence checks.
198
+ *
258
199
  * @param checks - Optional checks for path existence and type (file/directory).
259
200
  * @returns A {@link Type}`<string>` representing the path.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const typeInputFile = typePath("input-file", { checkSyncExistAs: "file" });
205
+ * typeInputFile.decoder("data.txt"); // → "data.txt" if it exists and is a file, otherwise throws
206
+ * typeInputFile.decoder("somedir"); // throws if "somedir" exists and is a directory
207
+ * ```
260
208
  */
261
209
  export function typePath(
262
210
  name?: string,
@@ -315,7 +263,6 @@ export function typePath(
315
263
 
316
264
  /**
317
265
  * Creates a {@link Type}`<string>` that only accepts a fixed set of values.
318
- * Out-of-set inputs throw {@link TypoError} listing up to 5 valid options.
319
266
  *
320
267
  * @param name - Name shown in help and errors.
321
268
  * @param values - Ordered list of accepted values.
@@ -325,52 +272,122 @@ export function typePath(
325
272
  * ```ts
326
273
  * const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
327
274
  * typeEnv.decoder("prod") // → "prod"
328
- * typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
275
+ * typeEnv.decoder("unknown") // throws
329
276
  * ```
330
277
  */
331
278
  export function typeChoice<const Value extends string>(
332
279
  name: string,
333
280
  values: Array<Value>,
334
- caseSensitive: boolean = false,
281
+ caseSensitive: boolean = true,
335
282
  ): Type<Value> {
336
283
  if (values.length === 0) {
337
284
  throw new Error("At least one value is required");
338
285
  }
339
286
  const normalize = caseSensitive
340
- ? (s: string) => s
341
- : (s: string) => s.toLowerCase();
342
- const valueByNormalizedKey = new Map(
287
+ ? (input: string) => input
288
+ : (input: string) => input.toLowerCase();
289
+ const valueByNormalized = new Map(
343
290
  values.map((value) => [normalize(value), value]),
344
291
  );
345
292
  return {
346
293
  content: name,
347
294
  decoder(input: string) {
348
- const normalizedKey = normalize(input);
349
- const value = valueByNormalizedKey.get(normalizedKey);
295
+ const value = valueByNormalized.get(normalize(input));
350
296
  if (value !== undefined) {
351
297
  return value;
352
298
  }
353
- const text = new TypoText();
354
- text.push(new TypoString(`Unknown value: `));
355
- text.push(new TypoString(`"${input}"`, typoStyleQuote));
356
- const suggestions = similaritySort(
357
- normalizedKey,
358
- [...valueByNormalizedKey.entries()].map(([normalizedKey, value]) => ({
359
- key: normalizedKey,
360
- value: new TypoString(`"${value}"`, typoStyleQuote),
299
+ const errorText = new TypoText();
300
+ errorText.push(new TypoString(`Unknown value: `));
301
+ errorText.push(new TypoString(`"${input}"`, typoStyleQuote));
302
+ errorText.push(new TypoString(`.`));
303
+ suggestTextPushMessage(
304
+ errorText,
305
+ input,
306
+ values.map((value) => ({
307
+ reference: value,
308
+ hint: new TypoString(`"${value}"`, typoStyleQuote),
361
309
  })),
362
- ).slice(0, 3);
363
- text.push(new TypoString(`: did you mean: `));
364
- text.push(TypoText.join(suggestions, new TypoString(`, `)));
365
- text.push(new TypoString(` ?`));
366
- throw new TypoError(text);
310
+ );
311
+ throw new TypoError(errorText);
312
+ },
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Chains `before`'s decoder with an `after` transformation.
318
+ * `before` errors are prefixed with `"from: <content>"` for traceability.
319
+ *
320
+ * @typeParam Before - Intermediate type from `before.decoder`.
321
+ * @typeParam After - Final type from `after.decoder`.
322
+ *
323
+ * @param name - Name shown in help and errors (e.g. `"my-value"`).
324
+ * @param before - Base type to decode the raw string.
325
+ * @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
326
+ * @returns A {@link Type}`<After>`.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * const typePort = typeMapped("port", typeNumber(), (n) => {
331
+ * if (n < 1 || n > 65535) {
332
+ * throw new Error("Out of range");
333
+ * }
334
+ * return n;
335
+ * });
336
+ * typePort.decoder("8080"); // → 8080
337
+ * typePort.decoder("70000"); // throws
338
+ * ```
339
+ */
340
+ export function typeMapped<Before, After>(
341
+ name: string,
342
+ before: Type<Before>,
343
+ mapper: (value: Before) => After,
344
+ ): Type<After> {
345
+ return {
346
+ content: name,
347
+ decoder: (input: string) => {
348
+ return mapper(
349
+ TypoError.tryWithContext(
350
+ () => before.decoder(input),
351
+ () =>
352
+ new TypoText(
353
+ new TypoString("from: "),
354
+ new TypoString(before.content, typoStyleLogic),
355
+ ),
356
+ ),
357
+ );
358
+ },
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Adds a name to a {@link Type} for clearer error messages and help text.
364
+ *
365
+ * @param name - Name to use for the type.
366
+ * @param type - Base type to name.
367
+ * @returns A {@link Type} with the given name.
368
+ */
369
+ export function typeRenamed<Value>(
370
+ type: Type<Value>,
371
+ name: string,
372
+ ): Type<Value> {
373
+ return {
374
+ content: name,
375
+ decoder: (input: string) => {
376
+ return TypoError.tryWithContext(
377
+ () => type.decoder(input),
378
+ () =>
379
+ new TypoText(
380
+ new TypoString("from: "),
381
+ new TypoString(type.content, typoStyleLogic),
382
+ ),
383
+ );
367
384
  },
368
385
  };
369
386
  }
370
387
 
371
388
  /**
372
389
  * Splits a delimited string into a typed tuple.
373
- * Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
390
+ * Each part is decoded by the corresponding element type; wrong count or decode failure throws.
374
391
  *
375
392
  * @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
376
393
  *
@@ -382,8 +399,9 @@ export function typeChoice<const Value extends string>(
382
399
  * ```ts
383
400
  * const typePoint = typeTuple([typeNumber("x"), typeNumber("y")]);
384
401
  * typePoint.decoder("3.14,2.71") // → [3.14, 2.71]
385
- * typePoint.decoder("1,2,3") // → [1, 2]
386
- * typePoint.decoder("x,2") // throws TypoError: at 0: Number: Unable to parse: "x"
402
+ * typePoint.decoder("1") // throws
403
+ * typePoint.decoder("1,2,3") // throws
404
+ * typePoint.decoder("x,2") // throws
387
405
  * ```
388
406
  */
389
407
  export function typeTuple<const Elements extends Array<any>>(
@@ -402,6 +420,7 @@ export function typeTuple<const Elements extends Array<any>>(
402
420
  new TypoString(`Found ${splits.length} splits: `),
403
421
  new TypoString(`Expected ${elementTypes.length} splits from: `),
404
422
  new TypoString(`"${input}"`, typoStyleQuote),
423
+ new TypoString(`.`),
405
424
  ),
406
425
  );
407
426
  }
@@ -422,7 +441,7 @@ export function typeTuple<const Elements extends Array<any>>(
422
441
 
423
442
  /**
424
443
  * Splits a delimited string into a typed array.
425
- * Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
444
+ * Each part is decoded by `elementType`; failed decodes throws.
426
445
  * Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
427
446
  *
428
447
  * @typeParam Value - Element type produced by `elementType.decoder`.
@@ -433,12 +452,13 @@ export function typeTuple<const Elements extends Array<any>>(
433
452
  *
434
453
  * @example
435
454
  * ```ts
436
- * const typeNumbers = typeList(typeNumber);
455
+ * const typeNumbers = typeList(typeNumber("v"));
437
456
  * typeNumbers.decoder("1,2,3") // → [1, 2, 3]
438
- * typeNumbers.decoder("1,x,3") // throws TypoError: at 1: Number: Unable to parse: "x"
439
- *
457
+ * typeNumbers.decoder("1,x,3") // throws
440
458
  * const typePaths = typeList(typePath(), ":");
441
- * typePaths.decoder("/usr/bin:/usr/local/bin") // → ["/usr/bin", "/usr/local/bin"]
459
+ * typePaths.decoder("/bin:/usr") // → ["/bin", "/usr"]
460
+ * typePaths.decoder("/usr/bin") // → ["/usr/bin"]
461
+ * typePaths.decoder("") // → throws
442
462
  * ```
443
463
  */
444
464
  export function typeList<Value>(
@@ -468,6 +488,10 @@ function throwInvalidValue(kind: string, input: string): never {
468
488
  new TypoText(
469
489
  new TypoString(`Not ${kind}: `),
470
490
  new TypoString(`"${input}"`, typoStyleQuote),
491
+ new TypoString(`.`),
471
492
  ),
472
493
  );
473
494
  }
495
+
496
+ const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
497
+ const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);