cli-kiss 0.2.7 → 0.2.8

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 (39) hide show
  1. package/dist/index.d.ts +127 -137
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/docs/.vitepress/config.mts +1 -1
  5. package/docs/.vitepress/theme/Layout.vue +16 -0
  6. package/docs/.vitepress/theme/index.ts +5 -1
  7. package/docs/.vitepress/theme/style.css +5 -1
  8. package/docs/guide/02_commands.md +1 -1
  9. package/docs/guide/03_options.md +11 -11
  10. package/docs/guide/05_input_types.md +9 -10
  11. package/docs/guide/06_run_as_cli.md +1 -1
  12. package/docs/index.md +2 -2
  13. package/docs/public/favicon.ico +0 -0
  14. package/docs/public/logo.png +0 -0
  15. package/package.json +1 -1
  16. package/src/index.ts +1 -1
  17. package/src/lib/Command.ts +45 -39
  18. package/src/lib/Operation.ts +28 -20
  19. package/src/lib/Option.ts +196 -127
  20. package/src/lib/Positional.ts +44 -23
  21. package/src/lib/Reader.ts +194 -226
  22. package/src/lib/Run.ts +19 -8
  23. package/src/lib/Suggest.ts +78 -0
  24. package/src/lib/Type.ts +36 -37
  25. package/src/lib/Typo.ts +58 -55
  26. package/src/lib/Usage.ts +12 -12
  27. package/tests/unit.Reader.commons.ts +92 -116
  28. package/tests/unit.Reader.parsings.ts +14 -26
  29. package/tests/unit.Reader.shortBig.ts +81 -96
  30. package/tests/unit.command.aliases.ts +100 -0
  31. package/tests/unit.command.execute.ts +1 -1
  32. package/tests/unit.command.usage.ts +12 -6
  33. package/tests/unit.fuzzed.alternatives.ts +35 -26
  34. package/tests/unit.runner.colors.ts +8 -33
  35. package/tests/unit.runner.cycle.ts +118 -146
  36. package/tests/unit.runner.errors.ts +25 -22
  37. package/docs/public/hero.png +0 -0
  38. package/src/lib/Similarity.ts +0 -41
  39. package/tests/unit.Reader.aliases.ts +0 -62
package/src/lib/Option.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { ReaderOptionParsing, ReaderArgs as ReaderOptions } from "./Reader";
1
+ import {
2
+ ReaderOptionGetter,
3
+ ReaderOptionNextGuard,
4
+ ReaderOptionRestGuard,
5
+ ReaderOptions,
6
+ ReaderOptionValue,
7
+ } from "./Reader";
2
8
  import { Type, typeBoolean } from "./Type";
3
9
  import {
4
10
  TypoError,
@@ -36,7 +42,7 @@ export type OptionDecoder<Value> = {
36
42
  /**
37
43
  * Returns the decoded option value.
38
44
  *
39
- * @throws {@link TypoError} if decoding failed.
45
+ * @throws if decoding failed.
40
46
  */
41
47
  getAndDecodeValue(): Value;
42
48
  };
@@ -44,8 +50,12 @@ export type OptionDecoder<Value> = {
44
50
  /**
45
51
  * Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
46
52
  *
47
- * Parsing: absent → default value; `--flag` / `--flag=yes` `true`; `--flag=no` → `false`;
48
- * specified more than once → throws {@link TypoError}.
53
+ * Syntax: `--long`, `--long=no`, `-s`, `-s=no`.
54
+ * Parsing logic:
55
+ * - absent → default value
56
+ * - `--flag` / `--flag=yes` → `true`
57
+ * - `--flag=no` → `false`
58
+ * - specified more than once → throws.
49
59
  *
50
60
  * @param definition.long - Long-form name (without `--`).
51
61
  * @param definition.short - Short-form name (without `-`).
@@ -62,11 +72,6 @@ export type OptionDecoder<Value> = {
62
72
  * short: "v",
63
73
  * description: "Enable verbose output",
64
74
  * });
65
- * // Usage:
66
- * // my-cli → false
67
- * // my-cli --verbose → true
68
- * // my-cli --verbose=yes → true
69
- * // my-cli -v=no → false
70
75
  * ```
71
76
  */
72
77
  export function optionFlag(definition: {
@@ -77,35 +82,37 @@ export function optionFlag(definition: {
77
82
  aliases?: { longs?: Array<string>; shorts?: Array<string> };
78
83
  default?: boolean;
79
84
  }): Option<boolean> {
80
- const typeBool = typeBoolean("value");
85
+ const type = typeBoolean("value");
81
86
  const { long, short, description, hint, aliases } = definition;
82
87
  return {
83
88
  generateUsage() {
84
89
  return { short, long, annotation: "[=no]", description, hint };
85
90
  },
86
91
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
87
- const key = registerOption(readerOptions, {
88
- long,
89
- short,
90
- aliasesLongs: aliases?.longs,
91
- aliasesShorts: aliases?.shorts,
92
- parsing: { consumeShortGroup: false, consumeNextArg: () => false },
92
+ const resultsGetter = setupOptionAliased(readerOptions, {
93
+ longKey: long,
94
+ shortKey: short,
95
+ aliasLongKeys: aliases?.longs,
96
+ aliasShortKeys: aliases?.shorts,
97
+ restGuard: () => false,
98
+ nextGuard: () => false,
93
99
  });
94
100
  return {
95
101
  getAndDecodeValue() {
96
- const optionResults = readerOptions.getOptionValues(key);
97
- if (optionResults.length > 1) {
98
- throwSetMultipleTimesError(long);
102
+ const results = resultsGetter();
103
+ if (results.length > 1) {
104
+ throwSetMultipleTimesError(results.map((r) => r.identifier));
99
105
  }
100
- if (optionResults.length === 0) {
106
+ if (results.length === 0) {
101
107
  return definition.default === undefined
102
108
  ? false
103
109
  : definition.default;
104
110
  }
105
- const positiveResult = optionResults[0]!;
106
- const value =
107
- positiveResult.inlined === null ? "true" : positiveResult.inlined;
108
- return decodeValue({ long, type: typeBool, input: value });
111
+ const input = results[0]!.value.inlined;
112
+ if (input === null) {
113
+ return true;
114
+ }
115
+ return decodeValue({ long, label: undefined, type, input });
109
116
  },
110
117
  };
111
118
  },
@@ -115,8 +122,13 @@ export function optionFlag(definition: {
115
122
  /**
116
123
  * Creates an option that accepts exactly one value (e.g. `--output dist/`).
117
124
  *
118
- * Parsing: absent `defaultValue()`; once decoded with `type`; more than once → {@link TypoError}.
119
- * Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
125
+ * Syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
126
+ * Parsing logic:
127
+ * - absent → `fallbackValueIfAbsent()`
128
+ * - `--long value` → decoded with `type.decoder("value")`
129
+ * - more than once → throws
130
+ * - if `impliedValueIfNotInlined` is not provided, then: `--long` / `-s` without a value → throws
131
+ * - if `impliedValueIfNotInlined` is provided, then: `--long` / `-s` without an inline value → `impliedValueIfNotInlined()`
120
132
  *
121
133
  * @typeParam Value - Type produced by the decoder.
122
134
  *
@@ -126,8 +138,8 @@ export function optionFlag(definition: {
126
138
  * @param definition.hint - Short note shown in parentheses.
127
139
  * @param definition.aliases - Additional names.
128
140
  * @param definition.type - Decoder for the raw string value.
129
- * @param definition.defaultIfNotSpecified - Default value when the option is not specified at all.
130
- * @param definition.valueIfNothingInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
141
+ * @param definition.fallbackValueIfAbsent - Default value when the option is not specified at all.
142
+ * @param definition.impliedValueIfNotInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
131
143
  * @returns An {@link Option}`<Value>`.
132
144
  *
133
145
  * @example
@@ -137,12 +149,8 @@ export function optionFlag(definition: {
137
149
  * short: "o",
138
150
  * type: typePath(),
139
151
  * description: "Output directory",
140
- * defaultIfNotSpecified: () => "dist",
152
+ * fallbackValueIfAbsent: () => "dist",
141
153
  * });
142
- * // Usage:
143
- * // my-cli → "dist"
144
- * // my-cli --output folder → "folder"
145
- * // my-cli -o folder → "folder"
146
154
  * ```
147
155
  */
148
156
  export function optionSingleValue<Value>(definition: {
@@ -152,59 +160,82 @@ export function optionSingleValue<Value>(definition: {
152
160
  hint?: string;
153
161
  aliases?: { longs?: Array<string>; shorts?: Array<string> };
154
162
  type: Type<Value>;
155
- defaultIfNotSpecified: () => Value;
156
- valueIfNothingInlined?: () => Value;
163
+ fallbackValueIfAbsent?: () => Value;
164
+ impliedValueIfNotInlined?: () => Value;
157
165
  }): Option<Value> {
158
166
  const { long, short, description, hint, aliases, type } = definition;
159
- const label = `<${type.content}>`;
167
+ const label = definition.impliedValueIfNotInlined
168
+ ? undefined
169
+ : `<${type.content}>`;
170
+ const annotation = definition.impliedValueIfNotInlined
171
+ ? `[=${type.content}]`
172
+ : undefined; // TODO - handle implied value and default better in usage and errors
160
173
  return {
161
174
  generateUsage() {
162
- return { short, long, label, description, hint };
175
+ return { short, long, label, annotation, description, hint };
163
176
  },
164
177
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
165
- const key = registerOption(readerOptions, {
166
- long,
167
- short,
168
- aliasesLongs: aliases?.longs,
169
- aliasesShorts: aliases?.shorts,
170
- parsing: {
171
- consumeShortGroup: true,
172
- consumeNextArg(inlined, separated) {
173
- if (definition.valueIfNothingInlined !== undefined) {
174
- return false;
175
- }
176
- return inlined === null && separated.length === 0;
177
- },
178
+ const resultsGetter = setupOptionAliased(readerOptions, {
179
+ longKey: long,
180
+ shortKey: short,
181
+ aliasLongKeys: aliases?.longs,
182
+ aliasShortKeys: aliases?.shorts,
183
+ restGuard: () => {
184
+ if (definition.impliedValueIfNotInlined !== undefined) {
185
+ return false;
186
+ }
187
+ return true;
188
+ },
189
+ nextGuard: (value) => {
190
+ if (definition.impliedValueIfNotInlined !== undefined) {
191
+ return false;
192
+ }
193
+ if (value.inlined !== null) {
194
+ return false;
195
+ }
196
+ if (value.separated.length !== 0) {
197
+ return false;
198
+ }
199
+ return true;
178
200
  },
179
201
  });
180
202
  return {
181
203
  getAndDecodeValue() {
182
- const optionResults = readerOptions.getOptionValues(key);
183
- if (optionResults.length > 1) {
184
- throwSetMultipleTimesError(long);
204
+ const results = resultsGetter();
205
+ if (results.length > 1) {
206
+ throwSetMultipleTimesError(
207
+ results.map((result) => result.identifier),
208
+ );
185
209
  }
186
- const optionResult = optionResults[0];
187
- if (optionResult === undefined) {
210
+ const result = results[0];
211
+ if (result === undefined) {
212
+ if (definition.fallbackValueIfAbsent === undefined) {
213
+ const errorText = makeErrorText({ long, label, type });
214
+ errorText.push(new TypoString(`: Is required, but was not set.`));
215
+ throw new TypoError(errorText);
216
+ }
188
217
  try {
189
- return definition.defaultIfNotSpecified();
218
+ return definition.fallbackValueIfAbsent();
190
219
  } catch (error) {
191
- const context = "Not specified";
192
- throwFailedToGetDefaultValueError({ long, error, context });
220
+ const errorText = makeErrorText({ long, label, type });
221
+ errorText.push(new TypoString(`: Failed to get fallback value.`));
222
+ throw new TypoError(errorText, error);
193
223
  }
194
224
  }
195
- if (optionResult.inlined) {
196
- const inlined = optionResult.inlined;
225
+ const inlined = result.value.inlined;
226
+ if (inlined !== null) {
197
227
  return decodeValue({ long, label, type, input: inlined });
198
228
  }
199
- if (definition.valueIfNothingInlined !== undefined) {
229
+ if (definition.impliedValueIfNotInlined !== undefined) {
200
230
  try {
201
- return definition.valueIfNothingInlined();
231
+ return definition.impliedValueIfNotInlined();
202
232
  } catch (error) {
203
- const context = "Nothing inlined";
204
- throwFailedToGetDefaultValueError({ long, error, context });
233
+ const errorText = makeErrorText({ long, label, type });
234
+ errorText.push(new TypoString(`: Failed to get implied value.`));
235
+ throw new TypoError(errorText, error);
205
236
  }
206
237
  }
207
- const separated = optionResult.separated[0]!;
238
+ const separated = result.value.separated[0]!;
208
239
  return decodeValue({ long, label, type, input: separated });
209
240
  },
210
241
  };
@@ -215,8 +246,10 @@ export function optionSingleValue<Value>(definition: {
215
246
  /**
216
247
  * Creates an option that collects every occurrence into an array (e.g. `--file a.ts --file b.ts`).
217
248
  *
218
- * Parsing: absent `[]`; N occurrences array of N decoded values in order.
219
- * Value syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
249
+ * Syntax: `--long value`, `--long=value`, `-s value`, `-s=value`, `-svalue`.
250
+ * Parsing logic:
251
+ * - absent → `[]`
252
+ * - N occurrences → array of N decoded values in order.
220
253
  *
221
254
  * @typeParam Value - Type produced by the decoder for each occurrence.
222
255
  *
@@ -237,7 +270,6 @@ export function optionSingleValue<Value>(definition: {
237
270
  * label: "PATH",
238
271
  * description: "Input file (may be repeated)",
239
272
  * });
240
- * // Usage: my-cli --file a.ts --file b.ts → ["a.ts", "b.ts"]
241
273
  * ```
242
274
  */
243
275
  export function optionRepeatable<Value>(definition: {
@@ -255,22 +287,27 @@ export function optionRepeatable<Value>(definition: {
255
287
  return { short, long, label, annotation: " [*]", description, hint };
256
288
  },
257
289
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
258
- const key = registerOption(readerOptions, {
259
- long,
260
- short,
261
- aliasesLongs: aliases?.longs,
262
- aliasesShorts: aliases?.shorts,
263
- parsing: {
264
- consumeShortGroup: true,
265
- consumeNextArg: (inlined, separated) =>
266
- inlined === null && separated.length === 0,
290
+ const resultsGetter = setupOptionAliased(readerOptions, {
291
+ longKey: long,
292
+ shortKey: short,
293
+ aliasLongKeys: aliases?.longs,
294
+ aliasShortKeys: aliases?.shorts,
295
+ restGuard: () => true,
296
+ nextGuard: (value) => {
297
+ if (value.inlined !== null) {
298
+ return false;
299
+ }
300
+ if (value.separated.length !== 0) {
301
+ return false;
302
+ }
303
+ return true;
267
304
  },
268
305
  });
269
306
  return {
270
307
  getAndDecodeValue() {
271
- const optionResults = readerOptions.getOptionValues(key);
272
- return optionResults.map((optionResult) => {
273
- const input = optionResult.inlined ?? optionResult.separated[0]!;
308
+ return resultsGetter().map((result) => {
309
+ const value = result.value;
310
+ const input = value.inlined ?? value.separated[0]!;
274
311
  return decodeValue({ long, label, type, input });
275
312
  });
276
313
  },
@@ -281,67 +318,99 @@ export function optionRepeatable<Value>(definition: {
281
318
 
282
319
  function decodeValue<Value>(params: {
283
320
  long: string;
284
- label?: string | undefined;
321
+ label: string | undefined;
285
322
  type: Type<Value>;
286
323
  input: string;
287
324
  }): Value {
325
+ const { long, label, type, input } = params;
288
326
  return TypoError.tryWithContext(
289
- () => params.type.decoder(params.input),
290
- () => {
291
- const text = new TypoText();
292
- text.push(new TypoString(`--${params.long}`, typoStyleConstants));
293
- if (params.label) {
294
- text.push(new TypoString(`: `));
295
- text.push(new TypoString(params.label, typoStyleUserInput));
296
- } else {
297
- text.push(new TypoString(`: `));
298
- text.push(new TypoString(params.type.content, typoStyleLogic));
299
- }
300
- return text;
301
- },
327
+ () => type.decoder(input),
328
+ () => makeErrorText({ long, label, type }),
302
329
  );
303
330
  }
304
331
 
305
- function registerOption(
332
+ function makeErrorText(params: {
333
+ long: string;
334
+ label: string | undefined;
335
+ type: Type<any>;
336
+ }): TypoText {
337
+ const errorText = new TypoText();
338
+ errorText.push(new TypoString(`--${params.long}`, typoStyleConstants));
339
+ if (params.label !== undefined) {
340
+ errorText.push(new TypoString(`: `));
341
+ errorText.push(new TypoString(params.label, typoStyleUserInput));
342
+ } else {
343
+ errorText.push(new TypoString(`: `));
344
+ errorText.push(new TypoString(params.type.content, typoStyleLogic));
345
+ }
346
+ return errorText;
347
+ }
348
+
349
+ function setupOptionAliased(
306
350
  readerOptions: ReaderOptions,
307
- definition: {
308
- long: string;
309
- short: undefined | string;
310
- aliasesLongs: undefined | Array<string>;
311
- aliasesShorts: undefined | Array<string>;
312
- parsing: ReaderOptionParsing;
351
+ params: {
352
+ longKey: string;
353
+ shortKey: string | undefined;
354
+ aliasLongKeys: Array<string> | undefined;
355
+ aliasShortKeys: Array<string> | undefined;
356
+ restGuard: ReaderOptionRestGuard;
357
+ nextGuard: ReaderOptionNextGuard;
313
358
  },
314
- ) {
315
- const { long, short, aliasesLongs, aliasesShorts, parsing } = definition;
316
- const longs = long ? [long] : [];
317
- if (aliasesLongs) {
318
- longs.push(...aliasesLongs);
359
+ ): () => Array<{ identifier: string; value: ReaderOptionValue }> {
360
+ const { longKey, shortKey, aliasLongKeys, aliasShortKeys } = params;
361
+ const longKeys = [longKey];
362
+ if (aliasLongKeys !== undefined) {
363
+ longKeys.push(...aliasLongKeys);
319
364
  }
320
- const shorts = short ? [short] : [];
321
- if (aliasesShorts) {
322
- shorts.push(...aliasesShorts);
365
+ const shortKeys = shortKey ? [shortKey] : [];
366
+ if (aliasShortKeys !== undefined) {
367
+ shortKeys.push(...aliasShortKeys);
323
368
  }
324
- return readerOptions.registerOption({ longs, shorts, parsing });
369
+ return setupOptionMany(readerOptions, {
370
+ longKeys,
371
+ shortKeys,
372
+ restGuard: params.restGuard,
373
+ nextGuard: params.nextGuard,
374
+ });
325
375
  }
326
376
 
327
- function throwSetMultipleTimesError(long: string): never {
328
- throw new TypoError(
329
- new TypoText(
330
- new TypoString(`--${long}`, typoStyleConstants),
331
- new TypoString(`: Must not be set multiple times`),
332
- ),
333
- );
377
+ function setupOptionMany(
378
+ readerOptions: ReaderOptions,
379
+ params: {
380
+ longKeys: Array<string>;
381
+ shortKeys: Array<string>;
382
+ restGuard: ReaderOptionRestGuard;
383
+ nextGuard: ReaderOptionNextGuard;
384
+ },
385
+ ): () => Array<{ identifier: string; value: ReaderOptionValue }> {
386
+ const { longKeys, shortKeys, restGuard, nextGuard } = params;
387
+ const getters = new Array<ReaderOptionGetter>();
388
+ for (const key of longKeys) {
389
+ getters.push(readerOptions.registerOptionLong({ key, nextGuard }));
390
+ }
391
+ for (const key of shortKeys) {
392
+ getters.push(
393
+ readerOptions.registerOptionShort({ key, restGuard, nextGuard }),
394
+ );
395
+ }
396
+ return () => {
397
+ const results = new Array();
398
+ for (const getter of getters) {
399
+ const { identifier, values } = getter();
400
+ for (const value of values) {
401
+ results.push({ identifier, value });
402
+ }
403
+ }
404
+ return results;
405
+ };
334
406
  }
335
407
 
336
- function throwFailedToGetDefaultValueError(params: {
337
- long: string;
338
- error: unknown;
339
- context: string;
340
- }): never {
341
- const text = new TypoText();
342
- text.push(new TypoString(`--${params.long}`, typoStyleConstants));
343
- text.push(
344
- new TypoString(`: ${params.context}: Failed to generate default value`),
408
+ function throwSetMultipleTimesError(identifiers: Array<string>): never {
409
+ const identifiersTexts = Array.from(new Set(identifiers)).map(
410
+ (identifier) => new TypoString(identifier, typoStyleConstants),
345
411
  );
346
- throw new TypoError(text, params.error);
412
+ const errorText = new TypoText();
413
+ errorText.pushJoined(identifiersTexts, new TypoString(", "), 3);
414
+ errorText.push(new TypoString(`: Must not be set multiple times.`));
415
+ throw new TypoError(errorText);
347
416
  }
@@ -1,6 +1,12 @@
1
1
  import { ReaderPositionals } from "./Reader";
2
2
  import { Type } from "./Type";
3
- import { TypoError, TypoString, typoStyleUserInput, TypoText } from "./Typo";
3
+ import {
4
+ TypoError,
5
+ TypoString,
6
+ typoStyleRegularWeaker,
7
+ typoStyleUserInput,
8
+ TypoText,
9
+ } from "./Typo";
4
10
  import { UsagePositional } from "./Usage";
5
11
 
6
12
  /**
@@ -32,19 +38,25 @@ export type PositionalDecoder<Value> = {
32
38
  /**
33
39
  * Returns the decoded positional value.
34
40
  *
35
- * @throws {@link TypoError} if decoding failed.
41
+ * @throws if decoding failed.
36
42
  */
37
43
  decodeValue(): Value;
38
44
  };
39
45
 
40
46
  /**
41
- * Creates a required positional — missing token throws {@link TypoError}.
47
+ * Creates a required positional — missing token throws.
48
+ *
49
+ * Syntax: `<type>`, e.g. `<NAME>`.
50
+ * Parsing logic:
51
+ * - "token" → decoded with `type.decoder("token")`
52
+ * - token missing → throws
42
53
  *
43
54
  * @typeParam Value - Type produced by the decoder.
44
55
  *
45
56
  * @param definition.description - Help text.
46
57
  * @param definition.hint - Short note shown in parentheses.
47
58
  * @param definition.type - Decoder for the raw token.
59
+ * @param definition.missing - Message shown when the token is missing.
48
60
  * @returns A {@link Positional}`<Value>`.
49
61
  *
50
62
  * @example
@@ -53,8 +65,6 @@ export type PositionalDecoder<Value> = {
53
65
  * type: type("name"),
54
66
  * description: "The name to greet",
55
67
  * });
56
- * // Usage:
57
- * // my-cli Alice → "Alice"
58
68
  * ```
59
69
  */
60
70
  export function positionalRequired<Value>(definition: {
@@ -71,12 +81,15 @@ export function positionalRequired<Value>(definition: {
71
81
  consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
72
82
  const positional = readerPositionals.consumePositional();
73
83
  if (positional === undefined) {
74
- throw new TypoError(
75
- new TypoText(
76
- new TypoString(label, typoStyleUserInput),
77
- new TypoString(`: Is required, but was not provided`),
78
- ),
79
- );
84
+ const errorText = makeErrorText(label);
85
+ errorText.push(new TypoString(`: Is required, but was not provided.`));
86
+ if (description !== undefined) {
87
+ // TODO - should there be a dedicated hint here ?
88
+ errorText.push(
89
+ new TypoString(` (${description})`, typoStyleRegularWeaker),
90
+ );
91
+ }
92
+ throw new TypoError(errorText);
80
93
  }
81
94
  return {
82
95
  decodeValue() {
@@ -90,6 +103,11 @@ export function positionalRequired<Value>(definition: {
90
103
  /**
91
104
  * Creates an optional positional — absent token falls back to `default()`.
92
105
  *
106
+ * Syntax: `[type]`, e.g. `[NAME]`.
107
+ * Parsing logic:
108
+ * - "token" → decoded with `type.decoder("token")`
109
+ * - token missing → `default()`
110
+ *
93
111
  * @typeParam Value - Type produced by the decoder (or the default).
94
112
  *
95
113
  * @param definition.description - Help text.
@@ -106,9 +124,6 @@ export function positionalRequired<Value>(definition: {
106
124
  * hint: "Defaults to \"world\"",
107
125
  * default: () => "world",
108
126
  * });
109
- * // Usage:
110
- * // my-cli → "world"
111
- * // my-cli Alice → "Alice"
112
127
  * ```
113
128
  */
114
129
  export function positionalOptional<Value>(definition: {
@@ -145,6 +160,12 @@ export function positionalOptional<Value>(definition: {
145
160
  * Creates a variadic positional that collects zero or more remaining tokens into an array.
146
161
  * Optionally stops at `endDelimiter` (consumed, not included).
147
162
  *
163
+ * Syntax: `[type]...`, e.g. `[NAME]...`.
164
+ * Parsing logic:
165
+ * - "a b ..." → decoded with `[type.decoder("a")`, `type.decoder("b"), ...]``
166
+ * - token missing → stops collection
167
+ * - endDelimiter encountered → stops collection
168
+ *
148
169
  * @typeParam Value - Type produced by the decoder for each token.
149
170
  *
150
171
  * @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
@@ -159,9 +180,6 @@ export function positionalOptional<Value>(definition: {
159
180
  * type: typePath(),
160
181
  * description: "Files to process",
161
182
  * });
162
- * // Usage:
163
- * // my-cli → []
164
- * // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
165
183
  * ```
166
184
  */
167
185
  export function positionalVariadics<Value>(definition: {
@@ -213,11 +231,14 @@ function decodeValue<Value>(
213
231
  );
214
232
  }
215
233
 
234
+ function makeErrorText(label: string): TypoText {
235
+ const errorText = new TypoText();
236
+ errorText.push(new TypoString(label, typoStyleUserInput));
237
+ return errorText;
238
+ }
239
+
216
240
  function throwsWhenFailedToGetDefault(label: string): never {
217
- throw new TypoError(
218
- new TypoText(
219
- new TypoString(label, typoStyleUserInput),
220
- new TypoString(`: Failed to get default value`),
221
- ),
222
- );
241
+ const errorText = makeErrorText(label);
242
+ errorText.push(new TypoString(`: Failed to get default value.`));
243
+ throw new TypoError(errorText);
223
244
  }