cli-kiss 0.1.8 → 0.2.0

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/src/lib/Type.ts CHANGED
@@ -6,11 +6,53 @@ import {
6
6
  TypoText,
7
7
  } from "./Typo";
8
8
 
9
+ /**
10
+ * Describes how to decode a raw CLI string token into a typed TypeScript value.
11
+ *
12
+ * A `Type` is a pair of:
13
+ * - a `content` string — a human-readable name shown in help/error messages (e.g.
14
+ * `"String"`, `"Number"`, `"Url"`).
15
+ * - a `decoder` function — converts the raw string or throws a {@link TypoError} on
16
+ * invalid input.
17
+ *
18
+ * Built-in types: {@link typeString}, {@link typeBoolean}, {@link typeNumber},
19
+ * {@link typeInteger}, {@link typeDate}, {@link typeUrl}.
20
+ *
21
+ * Composite types: {@link typeOneOf}, {@link typeConverted}, {@link typeTuple},
22
+ * {@link typeList}.
23
+ *
24
+ * @typeParam Value - The TypeScript type that the decoder produces.
25
+ */
9
26
  export type Type<Value> = {
27
+ /**
28
+ * Human-readable name for this type, used in help text and error messages.
29
+ * Examples: `"String"`, `"Number"`, `"Url"`.
30
+ */
10
31
  content: string;
32
+ /**
33
+ * Decodes a raw string token into a `Value`.
34
+ *
35
+ * @param value - The raw string from the command line.
36
+ * @returns The decoded value.
37
+ * @throws {@link TypoError} if the value cannot be decoded.
38
+ */
11
39
  decoder(value: string): Value;
12
40
  };
13
41
 
42
+ /**
43
+ * A {@link Type} that decodes `"true"` / `"yes"` to `true` and `"false"` / `"no"` to
44
+ * `false` (case-insensitive). Any other value throws a {@link TypoError}.
45
+ *
46
+ * Primarily used internally by {@link optionFlag} for the `--flag=<value>` syntax, but
47
+ * can also be used in positionals or valued options.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * typeBoolean.decoder("yes") // → true
52
+ * typeBoolean.decoder("false") // → false
53
+ * typeBoolean.decoder("1") // throws TypoError
54
+ * ```
55
+ */
14
56
  export const typeBoolean: Type<boolean> = {
15
57
  content: "Boolean",
16
58
  decoder(value: string) {
@@ -30,6 +72,21 @@ export const typeBoolean: Type<boolean> = {
30
72
  },
31
73
  };
32
74
 
75
+ /**
76
+ * A {@link Type} that parses a date/time string using `Date.parse`.
77
+ *
78
+ * Accepts any format supported by the JavaScript `Date.parse` API, including ISO 8601
79
+ * strings (e.g. `"2024-01-15"`, `"2024-01-15T10:30:00Z"`). Non-parseable values throw
80
+ * a {@link TypoError}.
81
+ *
82
+ * Produces a `Date` object. The decoded value is the result of `new Date(Date.parse(value))`.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * typeDate.decoder("2024-01-15") // → Date object for 2024-01-15
87
+ * typeDate.decoder("not a date") // throws TypoError
88
+ * ```
89
+ */
33
90
  export const typeDate: Type<Date> = {
34
91
  content: "Date",
35
92
  decoder(value: string) {
@@ -50,6 +107,20 @@ export const typeDate: Type<Date> = {
50
107
  },
51
108
  };
52
109
 
110
+ /**
111
+ * A {@link Type} that parses a string into a JavaScript `number` using the `Number()`
112
+ * constructor.
113
+ *
114
+ * Accepts integers, floating-point values, and scientific notation (e.g. `"3.14"`,
115
+ * `"-1"`, `"1e10"`). Values that produce `NaN` throw a {@link TypoError}.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * typeNumber.decoder("3.14") // → 3.14
120
+ * typeNumber.decoder("-1") // → -1
121
+ * typeNumber.decoder("hello") // throws TypoError
122
+ * ```
123
+ */
53
124
  export const typeNumber: Type<number> = {
54
125
  content: "Number",
55
126
  decoder(value: string) {
@@ -70,6 +141,20 @@ export const typeNumber: Type<number> = {
70
141
  },
71
142
  };
72
143
 
144
+ /**
145
+ * A {@link Type} that parses a string into a JavaScript `bigint` using the `BigInt()`
146
+ * constructor.
147
+ *
148
+ * Only accepts valid integer strings (e.g. `"42"`, `"-100"`, `"9007199254740993"`).
149
+ * Floating-point strings or non-numeric values throw a {@link TypoError}.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * typeInteger.decoder("42") // → 42n
154
+ * typeInteger.decoder("3.14") // throws TypoError
155
+ * typeInteger.decoder("abc") // throws TypoError
156
+ * ```
157
+ */
73
158
  export const typeInteger: Type<bigint> = {
74
159
  content: "Integer",
75
160
  decoder(value: string) {
@@ -86,6 +171,18 @@ export const typeInteger: Type<bigint> = {
86
171
  },
87
172
  };
88
173
 
174
+ /**
175
+ * A {@link Type} that parses a string into a `URL` object using the `URL` constructor.
176
+ *
177
+ * The string must be a valid absolute URL (e.g. `"https://example.com/path?q=1"`).
178
+ * Relative URLs and malformed strings throw a {@link TypoError}.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * typeUrl.decoder("https://example.com") // → URL { href: "https://example.com/", ... }
183
+ * typeUrl.decoder("not-a-url") // throws TypoError
184
+ * ```
185
+ */
89
186
  export const typeUrl: Type<URL> = {
90
187
  content: "Url",
91
188
  decoder(value: string) {
@@ -102,6 +199,17 @@ export const typeUrl: Type<URL> = {
102
199
  },
103
200
  };
104
201
 
202
+ /**
203
+ * A {@link Type} that passes the raw string through unchanged (identity decoder).
204
+ *
205
+ * This is the simplest type and accepts any string value without validation.
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * typeString.decoder("hello") // → "hello"
210
+ * typeString.decoder("") // → ""
211
+ * ```
212
+ */
105
213
  export const typeString: Type<string> = {
106
214
  content: "String",
107
215
  decoder(value: string) {
@@ -109,6 +217,40 @@ export const typeString: Type<string> = {
109
217
  },
110
218
  };
111
219
 
220
+ /**
221
+ * Creates a new {@link Type} by chaining a `before` type decoder with an `after`
222
+ * transformation.
223
+ *
224
+ * The raw string is first decoded by `before.decoder`; its result is then passed to
225
+ * `after.decoder`. Errors from `before` are wrapped with a "from: <content>" context
226
+ * prefix so that the full decoding path is visible in error messages.
227
+ *
228
+ * Use this when an existing type (e.g. {@link typeString}, {@link typeOneOf}) produces
229
+ * an intermediate value that needs a further transformation (e.g. parsing a
230
+ * string-keyed enum into a number).
231
+ *
232
+ * @typeParam Before - The intermediate type produced by `before.decoder`.
233
+ * @typeParam After - The final type produced by `after.decoder`.
234
+ *
235
+ * @param before - The base type that decodes the raw CLI string.
236
+ * @param after - The transformation applied to the `Before` value.
237
+ * @param after.content - Human-readable name for the resulting type (shown in errors).
238
+ * @param after.decoder - Function that converts a `Before` value into `After`.
239
+ * @returns A new {@link Type}`<After>` whose `content` is `after.content`.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const typePort = typeConverted(typeNumber, {
244
+ * content: "Port",
245
+ * decoder: (n) => {
246
+ * if (n < 1 || n > 65535) throw new Error("Out of range");
247
+ * return n;
248
+ * },
249
+ * });
250
+ * // "--port 8080" → 8080
251
+ * // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
252
+ * ```
253
+ */
112
254
  export function typeConverted<Before, After>(
113
255
  before: Type<Before>,
114
256
  after: { content: string; decoder: (value: Before) => After },
@@ -130,6 +272,27 @@ export function typeConverted<Before, After>(
130
272
  };
131
273
  }
132
274
 
275
+ /**
276
+ * Creates a {@link Type}`<string>` that only accepts a fixed set of string values.
277
+ *
278
+ * The decoder performs an exact (case-sensitive) lookup in `values`. If the input is
279
+ * not in the set, a {@link TypoError} is thrown listing up to 5 of the valid options.
280
+ *
281
+ * Combine with {@link typeConverted} to map the accepted strings to a richer type.
282
+ *
283
+ * @param content - Human-readable name for this type shown in help text and error
284
+ * messages (e.g. `"Environment"`, `"LogLevel"`).
285
+ * @param values - The ordered list of accepted string values. The order is preserved in
286
+ * the error message preview.
287
+ * @returns A {@link Type}`<string>` that validates membership in `values`.
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * const typeEnv = typeOneOf("Environment", ["dev", "staging", "prod"]);
292
+ * typeEnv.decoder("prod") // → "prod"
293
+ * typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
294
+ * ```
295
+ */
133
296
  export function typeOneOf(
134
297
  content: string,
135
298
  values: Array<string>,
@@ -165,6 +328,33 @@ export function typeOneOf(
165
328
  };
166
329
  }
167
330
 
331
+ /**
332
+ * Creates a {@link Type} that decodes a single delimited string into a fixed-length
333
+ * tuple of typed elements.
334
+ *
335
+ * The raw string is split on `separator` into exactly `elementTypes.length` parts.
336
+ * Each part is decoded by its corresponding element type. If the number of splits does
337
+ * not match, or if any element's decoder fails, a {@link TypoError} is thrown with the
338
+ * index and element type context.
339
+ *
340
+ * The resulting `content` is the element types' `content` values joined by `separator`
341
+ * (e.g. `"Number,String"` for a `[number, string]` tuple with `","` separator).
342
+ *
343
+ * @typeParam Elements - The tuple type of decoded element values (inferred from
344
+ * `elementTypes`).
345
+ *
346
+ * @param elementTypes - An ordered array of {@link Type}s, one per tuple element.
347
+ * @param separator - The string used to split the raw value (default: `","`).
348
+ * @returns A {@link Type}`<Elements>` tuple type.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * const typePoint = typeTuple([typeNumber, typeNumber]);
353
+ * typePoint.decoder("3.14,2.71") // → [3.14, 2.71]
354
+ * typePoint.decoder("1,2,3") // → [1, 2]
355
+ * typePoint.decoder("x,2") // throws TypoError: at 0: Number: Unable to parse: "x"
356
+ * ```
357
+ */
168
358
  export function typeTuple<const Elements extends Array<any>>(
169
359
  elementTypes: { [K in keyof Elements]: Type<Elements[K]> },
170
360
  separator: string = ",",
@@ -199,6 +389,39 @@ export function typeTuple<const Elements extends Array<any>>(
199
389
  };
200
390
  }
201
391
 
392
+ /**
393
+ * Creates a {@link Type} that decodes a single delimited string into an array of
394
+ * homogeneous typed elements.
395
+ *
396
+ * The raw string is split on `separator` and each part is decoded by `elementType`.
397
+ * If any element's decoder fails, a {@link TypoError} is thrown with the index and
398
+ * element type context.
399
+ *
400
+ * Unlike {@link typeTuple}, the number of elements is not fixed; the result array
401
+ * length equals the number of `separator`-delimited parts in the input string. To pass
402
+ * an empty array, the user must pass an empty string (`""`), which splits into one
403
+ * empty-string element — consider using {@link optionRepeatable} instead if you want a
404
+ * naturally empty default.
405
+ *
406
+ * The `content` is formatted as `"<elementContent>[<sep><elementContent>]..."` to
407
+ * signal repeatability.
408
+ *
409
+ * @typeParam Value - The TypeScript element type produced by `elementType.decoder`.
410
+ *
411
+ * @param elementType - The {@link Type} used to decode each element.
412
+ * @param separator - The string used to split the raw value (default: `","`).
413
+ * @returns A {@link Type}`<Array<Value>>`.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * const typeNumbers = typeList(typeNumber);
418
+ * typeNumbers.decoder("1,2,3") // → [1, 2, 3]
419
+ * typeNumbers.decoder("1,x,3") // throws TypoError: at 1: Number: Unable to parse: "x"
420
+ *
421
+ * const typePaths = typeList(typeString, ":");
422
+ * typePaths.decoder("/usr/bin:/usr/local/bin") // → ["/usr/bin", "/usr/local/bin"]
423
+ * ```
424
+ */
202
425
  export function typeList<Value>(
203
426
  elementType: Type<Value>,
204
427
  separator: string = ",",
package/src/lib/Typo.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Available foreground and background color names for terminal styling.
3
+ *
4
+ * Colors are divided into two groups:
5
+ * - **dark** variants correspond to standard ANSI colors (codes 30–37 / 40–47).
6
+ * - **bright** variants correspond to high-intensity ANSI colors (codes 90–97 / 100–107).
7
+ *
8
+ * Used by {@link TypoStyle}'s `fgColor` and `bgColor` fields.
9
+ */
1
10
  export type TypoColor =
2
11
  | "darkBlack"
3
12
  | "darkRed"
@@ -16,68 +25,130 @@ export type TypoColor =
16
25
  | "brightCyan"
17
26
  | "brightWhite";
18
27
 
28
+ /**
29
+ * Describes the visual styling to apply to a text segment when rendered by a
30
+ * {@link TypoSupport} instance.
31
+ *
32
+ * All fields are optional. When `TypoSupport` is in `"none"` mode, no styling is
33
+ * applied and the raw text is returned unchanged. In `"tty"` mode the corresponding
34
+ * ANSI escape codes are emitted. In `"mock"` mode a deterministic textual representation
35
+ * is produced (useful for snapshot tests).
36
+ */
19
37
  export type TypoStyle = {
38
+ /** Foreground (text) color. */
20
39
  fgColor?: TypoColor;
40
+ /** Background color. */
21
41
  bgColor?: TypoColor;
42
+ /** Render the text with reduced intensity. */
22
43
  dim?: boolean;
44
+ /** Render the text in bold. */
23
45
  bold?: boolean;
46
+ /** Render the text in italic. */
24
47
  italic?: boolean;
48
+ /** Render the text with an underline. */
25
49
  underline?: boolean;
50
+ /** Render the text with a strikethrough. */
26
51
  strikethrough?: boolean;
27
52
  };
28
53
 
54
+ /**
55
+ * Pre-defined {@link TypoStyle} for section titles in the usage output (e.g.
56
+ * `"Positionals:"`, `"Options:"`).
57
+ * Rendered in bold dark-green.
58
+ */
29
59
  export const typoStyleTitle: TypoStyle = {
30
60
  fgColor: "darkGreen",
31
61
  bold: true,
32
62
  };
63
+ /** Pre-defined {@link TypoStyle} for logic/type identifiers in error messages. Rendered in bold dark-magenta. */
33
64
  export const typoStyleLogic: TypoStyle = {
34
65
  fgColor: "darkMagenta",
35
66
  bold: true,
36
67
  };
68
+ /** Pre-defined {@link TypoStyle} for quoted user-supplied values in error messages. Rendered in bold dark-yellow. */
37
69
  export const typoStyleQuote: TypoStyle = {
38
70
  fgColor: "darkYellow",
39
71
  bold: true,
40
72
  };
41
73
 
74
+ /** Pre-defined {@link TypoStyle} for failure/error labels (e.g. `"Error:"`). Rendered in bold dark-red. */
42
75
  export const typoStyleFailure: TypoStyle = {
43
76
  fgColor: "darkRed",
44
77
  bold: true,
45
78
  };
46
79
 
80
+ /** Pre-defined {@link TypoStyle} for CLI flag/option/command constant names. Rendered in bold dark-cyan. */
47
81
  export const typoStyleConstants: TypoStyle = {
48
82
  fgColor: "darkCyan",
49
83
  bold: true,
50
84
  };
85
+ /** Pre-defined {@link TypoStyle} for positional placeholders and user-input labels. Rendered in bold dark-blue. */
51
86
  export const typoStyleUserInput: TypoStyle = {
52
87
  fgColor: "darkBlue",
53
88
  bold: true,
54
89
  };
55
90
 
91
+ /** Pre-defined {@link TypoStyle} for strong regular text (e.g. command descriptions). Rendered in bold. */
56
92
  export const typoStyleRegularStrong: TypoStyle = {
57
93
  bold: true,
58
94
  };
95
+ /** Pre-defined {@link TypoStyle} for subtle supplementary text (e.g. hints). Rendered in italic and dim. */
59
96
  export const typoStyleRegularWeaker: TypoStyle = {
60
97
  italic: true,
61
98
  dim: true,
62
99
  };
63
100
 
101
+ /**
102
+ * An immutable styled string segment consisting of a raw text value and an associated
103
+ * {@link TypoStyle}.
104
+ *
105
+ * Multiple `TypoString`s are composed into a {@link TypoText} for multi-part messages.
106
+ * Rendering is deferred until {@link TypoString.computeStyledString} is called with a
107
+ * {@link TypoSupport} instance.
108
+ */
64
109
  export class TypoString {
65
110
  #value: string;
66
111
  #typoStyle: TypoStyle;
112
+ /**
113
+ * @param value - The raw text content.
114
+ * @param typoStyle - The style to apply when rendering. Defaults to `{}` (no style).
115
+ */
67
116
  constructor(value: string, typoStyle: TypoStyle = {}) {
68
117
  this.#value = value;
69
118
  this.#typoStyle = typoStyle;
70
119
  }
120
+ /** Returns the unstyled raw text content. */
71
121
  getRawString(): string {
72
122
  return this.#value;
73
123
  }
124
+ /**
125
+ * Returns the text with ANSI escape codes (or mock markers) applied by `typoSupport`.
126
+ *
127
+ * @param typoSupport - Controls how styles are rendered (tty colors, mock, or none).
128
+ */
74
129
  computeStyledString(typoSupport: TypoSupport): string {
75
130
  return typoSupport.computeStyledString(this.#value, this.#typoStyle);
76
131
  }
77
132
  }
78
133
 
134
+ /**
135
+ * A mutable sequence of {@link TypoString} segments that together form a styled
136
+ * multi-part message.
137
+ *
138
+ * `TypoText` is used throughout the library to build error messages and usage output
139
+ * that carry styling information without being coupled to a specific output mode.
140
+ * Rendering is deferred to {@link TypoText.computeStyledString}.
141
+ */
79
142
  export class TypoText {
80
143
  #typoStrings: Array<TypoString>;
144
+ /**
145
+ * Creates a `TypoText` pre-populated with the provided parts. Each part can be a
146
+ * `TypoText` (flattened by value), a `TypoString`, or a plain `string` (wrapped in an
147
+ * unstyled `TypoString`).
148
+ *
149
+ * @param typoParts - Initial parts to append. Can be any mix of `TypoText`,
150
+ * `TypoString`, and `string`.
151
+ */
81
152
  constructor(...typoParts: Array<TypoText | TypoString | string>) {
82
153
  this.#typoStrings = [];
83
154
  for (const typoPart of typoParts) {
@@ -90,22 +161,48 @@ export class TypoText {
90
161
  }
91
162
  }
92
163
  }
164
+ /**
165
+ * Appends a single {@link TypoString} segment to the end of this text.
166
+ *
167
+ * @param typoString - The segment to append.
168
+ */
93
169
  pushString(typoString: TypoString) {
94
170
  this.#typoStrings.push(typoString);
95
171
  }
172
+ /**
173
+ * Appends all segments from another {@link TypoText} to the end of this text
174
+ * (shallow copy of segments).
175
+ *
176
+ * @param typoText - The text whose segments are appended.
177
+ */
96
178
  pushText(typoText: TypoText) {
97
179
  for (const typoString of typoText.#typoStrings) {
98
180
  this.#typoStrings.push(typoString);
99
181
  }
100
182
  }
183
+ /**
184
+ * Renders all segments into a single string, applying styles via `typoSupport`.
185
+ *
186
+ * @param typoSupport - Controls how styles are rendered.
187
+ * @returns The concatenated, optionally styled string.
188
+ */
101
189
  computeStyledString(typoSupport: TypoSupport): string {
102
190
  return this.#typoStrings
103
191
  .map((t) => t.computeStyledString(typoSupport))
104
192
  .join("");
105
193
  }
194
+ /**
195
+ * Returns the concatenation of all segments' raw (unstyled) text.
196
+ * Equivalent to calling {@link TypoText.computeStyledString} with
197
+ * {@link TypoSupport.none}.
198
+ */
106
199
  computeRawString(): string {
107
200
  return this.#typoStrings.map((t) => t.getRawString()).join("");
108
201
  }
202
+ /**
203
+ * Returns the total character length of the raw (unstyled) text.
204
+ * Used by {@link TypoGrid} to compute column widths for alignment.
205
+ */
109
206
  computeRawLength(): number {
110
207
  let length = 0;
111
208
  for (const typoString of this.#typoStrings) {
@@ -115,14 +212,38 @@ export class TypoText {
115
212
  }
116
213
  }
117
214
 
215
+ /**
216
+ * A grid of {@link TypoText} cells that renders with column-aligned padding.
217
+ *
218
+ * Each row is an array of `TypoText` cells. When {@link TypoGrid.computeStyledGrid} is
219
+ * called, each column is padded to the width of its widest cell (measured in raw
220
+ * characters). The last column in each row is **not** padded.
221
+ *
222
+ * Used internally by {@link usageToStyledLines} to render the `Positionals:`,
223
+ * `Subcommands:`, and `Options:` sections with neat alignment.
224
+ */
118
225
  export class TypoGrid {
119
226
  #typoRows: Array<Array<TypoText>>;
120
227
  constructor() {
121
228
  this.#typoRows = [];
122
229
  }
230
+ /**
231
+ * Appends a row of cells to the grid.
232
+ *
233
+ * @param cells - An ordered array of {@link TypoText} cells for this row. All rows
234
+ * should have the same number of cells for alignment to be meaningful.
235
+ */
123
236
  pushRow(cells: Array<TypoText>) {
124
237
  this.#typoRows.push(cells);
125
238
  }
239
+ /**
240
+ * Renders the grid into a 2-D array of styled strings, with space padding added
241
+ * between columns (except after the last column).
242
+ *
243
+ * @param typoSupport - Controls how styles are rendered.
244
+ * @returns A 2-D array where each inner array is the styled (and padded) cells of
245
+ * one row. Join the inner arrays with `""` to get a single line string.
246
+ */
126
247
  computeStyledGrid(typoSupport: TypoSupport): Array<Array<string>> {
127
248
  const widths = new Array<number>();
128
249
  const printableGrid = new Array<Array<string>>();
@@ -164,8 +285,26 @@ export class TypoGrid {
164
285
  }
165
286
  }
166
287
 
288
+ /**
289
+ * An `Error` subclass that carries a {@link TypoText} styled message in addition to
290
+ * the plain-text `Error.message` used by the standard JS error chain.
291
+ *
292
+ * `TypoError` is used throughout `cli-kiss` to report parsing failures (unknown option,
293
+ * type decoding error, missing required argument, etc.). Its styled representation is
294
+ * rendered by {@link TypoSupport.computeStyledErrorMessage} when outputting errors to
295
+ * the terminal.
296
+ *
297
+ * Errors can be chained: if `source` is a `TypoError`, its styled text is appended
298
+ * after `": "` to form the full message context chain.
299
+ */
167
300
  export class TypoError extends Error {
168
301
  #typoText: TypoText;
302
+ /**
303
+ * @param currentTypoText - The styled message for this error level.
304
+ * @param source - An optional cause. If it is a `TypoError`, its styled text is
305
+ * appended (chained context). If it is a plain `Error`, its `.message` is appended
306
+ * as a plain string. Any other value is stringified with `String()`.
307
+ */
169
308
  constructor(currentTypoText: TypoText, source?: unknown) {
170
309
  const typoText = new TypoText();
171
310
  typoText.pushText(currentTypoText);
@@ -180,9 +319,31 @@ export class TypoError extends Error {
180
319
  super(typoText.computeRawString());
181
320
  this.#typoText = typoText;
182
321
  }
322
+ /**
323
+ * Renders this error's styled message as a string.
324
+ *
325
+ * @param typoSupport - Controls how ANSI styles are applied.
326
+ * @returns The full styled error message (without a leading `"Error:"` prefix).
327
+ */
183
328
  computeStyledString(typoSupport: TypoSupport): string {
184
329
  return this.#typoText.computeStyledString(typoSupport);
185
330
  }
331
+ /**
332
+ * Executes `thrower` and returns its result. If `thrower` throws any error, the error
333
+ * is re-thrown as a new `TypoError` whose message is `context()` with the original
334
+ * error chained as the source.
335
+ *
336
+ * This is a convenience helper for adding contextual information to errors that arise
337
+ * deep in a call chain (e.g. "at 0: Number: Unable to parse: ...").
338
+ *
339
+ * @typeParam Value - The return type of `thrower`.
340
+ * @param thrower - A zero-argument function whose return value is passed through on
341
+ * success.
342
+ * @param context - A zero-argument factory that produces the {@link TypoText} context
343
+ * prepended to the caught error. Called only when `thrower` throws.
344
+ * @returns The value returned by `thrower`.
345
+ * @throws `TypoError` wrapping the original error with the provided context prepended.
346
+ */
186
347
  static tryWithContext<Value>(
187
348
  thrower: () => Value,
188
349
  context: () => TypoText,
@@ -195,20 +356,61 @@ export class TypoError extends Error {
195
356
  }
196
357
  }
197
358
 
359
+ /**
360
+ * Controls whether and how ANSI terminal styling is applied when rendering
361
+ * {@link TypoString}, {@link TypoText}, and error messages.
362
+ *
363
+ * Instances are created via the static factory methods:
364
+ * - {@link TypoSupport.none} — strips all styling (plain text).
365
+ * - {@link TypoSupport.tty} — applies ANSI escape codes for color terminals.
366
+ * - {@link TypoSupport.mock} — applies a deterministic textual representation useful
367
+ * for snapshot tests.
368
+ * - {@link TypoSupport.inferFromProcess} — auto-detects based on `process.stdout.isTTY`
369
+ * and the `FORCE_COLOR` / `NO_COLOR` environment variables.
370
+ *
371
+ * `TypoSupport` is consumed by {@link runAndExit} (via the `useTtyColors` option)
372
+ * and can also be used directly when building custom usage renderers with {@link usageToStyledLines}.
373
+ */
198
374
  export class TypoSupport {
199
375
  #kind: "none" | "tty" | "mock";
200
376
  private constructor(kind: "none" | "tty" | "mock") {
201
377
  this.#kind = kind;
202
378
  }
379
+ /**
380
+ * Returns a `TypoSupport` that strips all styling — every styled string is returned
381
+ * as-is (plain text, no ANSI codes).
382
+ */
203
383
  static none(): TypoSupport {
204
384
  return new TypoSupport("none");
205
385
  }
386
+ /**
387
+ * Returns a `TypoSupport` that applies ANSI escape codes.
388
+ * Use this when writing to a color-capable terminal (`stdout.isTTY === true`).
389
+ */
206
390
  static tty(): TypoSupport {
207
391
  return new TypoSupport("tty");
208
392
  }
393
+ /**
394
+ * Returns a `TypoSupport` that applies a deterministic mock styling representation.
395
+ *
396
+ * Instead of real ANSI codes, each style flag is expressed as a readable suffix:
397
+ * `{text}@color`, `{text}+` (bold), `{text}-` (dim), `{text}*` (italic),
398
+ * `{text}_` (underline), `{text}~` (strikethrough). Useful for snapshot testing.
399
+ */
209
400
  static mock(): TypoSupport {
210
401
  return new TypoSupport("mock");
211
402
  }
403
+ /**
404
+ * Selects a `TypoSupport` mode automatically based on the current process environment:
405
+ *
406
+ * 1. `FORCE_COLOR=0` or `NO_COLOR` env var set → {@link TypoSupport.none}.
407
+ * 2. `FORCE_COLOR` env var set (any truthy value) → {@link TypoSupport.tty}.
408
+ * 3. `process.stdout.isTTY === true` → {@link TypoSupport.tty}.
409
+ * 4. Otherwise → {@link TypoSupport.none}.
410
+ *
411
+ * Falls back to {@link TypoSupport.none} if `process` is not available (e.g. in a
412
+ * non-Node environment).
413
+ */
212
414
  static inferFromProcess(): TypoSupport {
213
415
  if (!process) {
214
416
  return TypoSupport.none();
@@ -229,6 +431,17 @@ export class TypoSupport {
229
431
  }
230
432
  return TypoSupport.none();
231
433
  }
434
+ /**
435
+ * Applies the given {@link TypoStyle} to `value` and returns the styled string.
436
+ *
437
+ * - In `"none"` mode: returns `value` unchanged.
438
+ * - In `"tty"` mode: wraps `value` in ANSI escape codes and appends a reset code.
439
+ * - In `"mock"` mode: wraps `value` in a deterministic textual representation.
440
+ *
441
+ * @param value - The raw text to style.
442
+ * @param typoStyle - The style to apply.
443
+ * @returns The styled string.
444
+ */
232
445
  computeStyledString(value: string, typoStyle: TypoStyle): string {
233
446
  if (this.#kind === "none") {
234
447
  return value;
@@ -269,6 +482,18 @@ export class TypoSupport {
269
482
  }
270
483
  throw new Error(`Unknown TypoSupport kind: ${this.#kind}`);
271
484
  }
485
+ /**
486
+ * Formats an error value as a styled `"Error: <message>"` string.
487
+ *
488
+ * - If `error` is a {@link TypoError}, its styled text is used for the message part.
489
+ * - If `error` is a plain `Error`, its `.message` property is used.
490
+ * - Otherwise `String(error)` is used.
491
+ *
492
+ * The `"Error:"` prefix is always styled with {@link typoStyleFailure}.
493
+ *
494
+ * @param error - The error to format (any value thrown by a handler).
495
+ * @returns A styled error string ready to print to stderr.
496
+ */
272
497
  computeStyledErrorMessage(error: unknown): string {
273
498
  return [
274
499
  this.computeStyledString("Error:", typoStyleFailure),