cli-kiss 0.2.2 → 0.2.4

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/Typo.ts CHANGED
@@ -1,11 +1,6 @@
1
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.
2
+ * Color names for terminal styling, used by {@link TypoStyle}.
3
+ * `dark*` = standard ANSI (30–37); `bright*` = high-intensity (90–97).
9
4
  */
10
5
  export type TypoColor =
11
6
  | "darkBlack"
@@ -26,165 +21,168 @@ export type TypoColor =
26
21
  | "brightWhite";
27
22
 
28
23
  /**
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).
24
+ * Visual styling applied by a {@link TypoSupport} instance.
25
+ * All fields are optional; ignored entirely in `"none"` mode.
36
26
  */
37
27
  export type TypoStyle = {
38
- /** Foreground (text) color. */
28
+ /**
29
+ * Foreground (text) color.
30
+ */
39
31
  fgColor?: TypoColor;
40
- /** Background color. */
32
+ /**
33
+ * Background color.
34
+ */
41
35
  bgColor?: TypoColor;
42
- /** Render the text with reduced intensity. */
36
+ /**
37
+ * Reduced intensity.
38
+ */
43
39
  dim?: boolean;
44
- /** Render the text in bold. */
40
+ /**
41
+ * Bold.
42
+ */
45
43
  bold?: boolean;
46
- /** Render the text in italic. */
44
+ /**
45
+ * Italic.
46
+ */
47
47
  italic?: boolean;
48
- /** Render the text with an underline. */
48
+ /**
49
+ * Underline.
50
+ */
49
51
  underline?: boolean;
50
- /** Render the text with a strikethrough. */
52
+ /**
53
+ * Strikethrough.
54
+ */
51
55
  strikethrough?: boolean;
52
56
  };
53
57
 
54
58
  /**
55
- * Pre-defined {@link TypoStyle} for section titles in the usage output (e.g.
56
- * `"Positionals:"`, `"Options:"`).
57
- * Rendered in bold dark-green.
59
+ * Section title style. Bold dark-green.
58
60
  */
59
61
  export const typoStyleTitle: TypoStyle = {
60
62
  fgColor: "darkGreen",
61
63
  bold: true,
62
64
  };
63
- /** Pre-defined {@link TypoStyle} for logic/type identifiers in error messages. Rendered in bold dark-magenta. */
65
+ /**
66
+ * Logic/type identifier style. Bold dark-magenta.
67
+ */
64
68
  export const typoStyleLogic: TypoStyle = {
65
69
  fgColor: "darkMagenta",
66
70
  bold: true,
67
71
  };
68
- /** Pre-defined {@link TypoStyle} for quoted user-supplied values in error messages. Rendered in bold dark-yellow. */
72
+ /**
73
+ * Quoted user-input style. Bold dark-yellow.
74
+ */
69
75
  export const typoStyleQuote: TypoStyle = {
70
76
  fgColor: "darkYellow",
71
77
  bold: true,
72
78
  };
73
79
 
74
- /** Pre-defined {@link TypoStyle} for failure/error labels (e.g. `"Error:"`). Rendered in bold dark-red. */
80
+ /**
81
+ * Error label style. Bold dark-red.
82
+ */
75
83
  export const typoStyleFailure: TypoStyle = {
76
84
  fgColor: "darkRed",
77
85
  bold: true,
78
86
  };
79
87
 
80
- /** Pre-defined {@link TypoStyle} for CLI flag/option/command constant names. Rendered in bold dark-cyan. */
88
+ /**
89
+ * Option/command name style. Bold dark-cyan.
90
+ */
81
91
  export const typoStyleConstants: TypoStyle = {
82
92
  fgColor: "darkCyan",
83
93
  bold: true,
84
94
  };
85
- /** Pre-defined {@link TypoStyle} for positional placeholders and user-input labels. Rendered in bold dark-blue. */
95
+ /**
96
+ * Positional/user-input label style. Bold dark-blue.
97
+ */
86
98
  export const typoStyleUserInput: TypoStyle = {
87
99
  fgColor: "darkBlue",
88
100
  bold: true,
89
101
  };
90
102
 
91
- /** Pre-defined {@link TypoStyle} for strong regular text (e.g. command descriptions). Rendered in bold. */
103
+ /**
104
+ * Strong text style. Bold.
105
+ */
92
106
  export const typoStyleRegularStrong: TypoStyle = {
93
107
  bold: true,
94
108
  };
95
- /** Pre-defined {@link TypoStyle} for subtle supplementary text (e.g. hints). Rendered in italic and dim. */
109
+ /**
110
+ * Subtle text style. Italic and dim.
111
+ */
96
112
  export const typoStyleRegularWeaker: TypoStyle = {
97
113
  italic: true,
98
114
  dim: true,
99
115
  };
100
116
 
101
117
  /**
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.
118
+ * Immutable styled string segment. Compose into a {@link TypoText}.
108
119
  */
109
120
  export class TypoString {
110
121
  #value: string;
111
122
  #typoStyle: TypoStyle;
112
123
  /**
113
- * @param value - The raw text content.
114
- * @param typoStyle - The style to apply when rendering. Defaults to `{}` (no style).
124
+ * @param value - Raw text content.
125
+ * @param typoStyle - Style to apply when rendering. Defaults to `{}` (no style).
115
126
  */
116
127
  constructor(value: string, typoStyle: TypoStyle = {}) {
117
128
  this.#value = value;
118
129
  this.#typoStyle = typoStyle;
119
130
  }
120
- /** Returns the unstyled raw text content. */
121
- getRawString(): string {
122
- return this.#value;
123
- }
124
131
  /**
125
- * Returns the text with ANSI escape codes (or mock markers) applied by `typoSupport`.
132
+ * Returns the text styled by `typoSupport`.
126
133
  *
127
- * @param typoSupport - Controls how styles are rendered (tty colors, mock, or none).
134
+ * @param typoSupport - Rendering mode.
128
135
  */
129
136
  computeStyledString(typoSupport: TypoSupport): string {
130
137
  return typoSupport.computeStyledString(this.#value, this.#typoStyle);
131
138
  }
139
+ /**
140
+ * Returns the raw text.
141
+ */
142
+ getRawString(): string {
143
+ return this.#value;
144
+ }
132
145
  }
133
146
 
134
147
  /**
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}.
148
+ * Mutable sequence of {@link TypoString} segments.
141
149
  */
142
150
  export class TypoText {
143
151
  #typoStrings: Array<TypoString>;
144
152
  /**
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`.
153
+ * @param segments - Initial text segments
151
154
  */
152
- constructor(...typoParts: Array<TypoText | TypoString | string>) {
155
+ constructor(
156
+ ...segments: Array<TypoText | Array<TypoString> | TypoString | string>
157
+ ) {
153
158
  this.#typoStrings = [];
154
- for (const typoPart of typoParts) {
155
- if (typoPart instanceof TypoText) {
156
- this.pushText(typoPart);
157
- } else if (typoPart instanceof TypoString) {
158
- this.pushString(typoPart);
159
- } else if (typeof typoPart === "string") {
160
- this.pushString(new TypoString(typoPart));
161
- }
159
+ for (const typoPart of segments) {
160
+ this.push(typoPart);
162
161
  }
163
162
  }
164
163
  /**
165
- * Appends a single {@link TypoString} segment to the end of this text.
164
+ * Appends new text segment(s).
166
165
  *
167
- * @param typoString - The segment to append.
166
+ * @param segment - Text segment(s) to append.
168
167
  */
169
- pushString(typoString: TypoString) {
170
- this.#typoStrings.push(typoString);
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
- */
178
- pushText(typoText: TypoText) {
179
- for (const typoString of typoText.#typoStrings) {
180
- this.#typoStrings.push(typoString);
168
+ push(segment: TypoText | Array<TypoString> | TypoString | string) {
169
+ if (typeof segment === "string") {
170
+ this.#typoStrings.push(new TypoString(segment));
171
+ } else if (segment instanceof TypoText) {
172
+ this.#typoStrings.push(...segment.#typoStrings);
173
+ } else if (Array.isArray(segment)) {
174
+ for (const typoString of segment) {
175
+ this.#typoStrings.push(typoString);
176
+ }
177
+ } else {
178
+ this.#typoStrings.push(segment);
181
179
  }
182
180
  }
183
181
  /**
184
- * Renders all segments into a single string, applying styles via `typoSupport`.
182
+ * Renders all segments into a single styled string.
185
183
  *
186
- * @param typoSupport - Controls how styles are rendered.
187
- * @returns The concatenated, optionally styled string.
184
+ * @param typoSupport - Rendering mode.
185
+ * @returns Concatenated styled string.
188
186
  */
189
187
  computeStyledString(typoSupport: TypoSupport): string {
190
188
  return this.#typoStrings
@@ -192,16 +190,13 @@ export class TypoText {
192
190
  .join("");
193
191
  }
194
192
  /**
195
- * Returns the concatenation of all segments' raw (unstyled) text.
196
- * Equivalent to calling {@link TypoText.computeStyledString} with
197
- * {@link TypoSupport.none}.
193
+ * Returns the concatenated raw text.
198
194
  */
199
195
  computeRawString(): string {
200
196
  return this.#typoStrings.map((t) => t.getRawString()).join("");
201
197
  }
202
198
  /**
203
- * Returns the total character length of the raw (unstyled) text.
204
- * Used by {@link TypoGrid} to compute column widths for alignment.
199
+ * Returns the total raw character count.
205
200
  */
206
201
  computeRawLength(): number {
207
202
  let length = 0;
@@ -213,14 +208,8 @@ export class TypoText {
213
208
  }
214
209
 
215
210
  /**
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.
211
+ * Column-aligned grid of {@link TypoText} cells.
212
+ * Each column is padded to the widest cell (raw chars); the last column is not padded.
224
213
  */
225
214
  export class TypoGrid {
226
215
  #typoRows: Array<Array<TypoText>>;
@@ -228,25 +217,22 @@ export class TypoGrid {
228
217
  this.#typoRows = [];
229
218
  }
230
219
  /**
231
- * Appends a row of cells to the grid.
220
+ * Appends a row. All rows should have the same cell count.
232
221
  *
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.
222
+ * @param cells - Ordered {@link TypoText} cells.
235
223
  */
236
224
  pushRow(cells: Array<TypoText>) {
237
225
  this.#typoRows.push(cells);
238
226
  }
239
227
  /**
240
- * Renders the grid into a 2-D array of styled strings, with space padding added
241
- * between columns (except after the last column).
228
+ * Renders as an array of styled, column-padded strings.
242
229
  *
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.
230
+ * @param typoSupport - Rendering mode.
231
+ * @returns 2-D array of styled strings.
246
232
  */
247
- computeStyledGrid(typoSupport: TypoSupport): Array<Array<string>> {
233
+ computeStyledLines(typoSupport: TypoSupport): Array<string> {
248
234
  const widths = new Array<number>();
249
- const printableGrid = new Array<Array<string>>();
235
+ const styledLines = new Array<string>();
250
236
  for (const typoGridRow of this.#typoRows) {
251
237
  for (
252
238
  let typoGridColumnIndex = 0;
@@ -264,85 +250,67 @@ export class TypoGrid {
264
250
  }
265
251
  }
266
252
  for (const typoGridRow of this.#typoRows) {
267
- const printableGridRow = new Array<string>();
253
+ const styledGridRow = new Array<string>();
268
254
  for (
269
255
  let typoGridColumnIndex = 0;
270
256
  typoGridColumnIndex < typoGridRow.length;
271
257
  typoGridColumnIndex++
272
258
  ) {
273
259
  const typoGridCell = typoGridRow[typoGridColumnIndex]!;
274
- const printableGridCell = typoGridCell.computeStyledString(typoSupport);
275
- printableGridRow.push(printableGridCell);
260
+ styledGridRow.push(typoGridCell.computeStyledString(typoSupport));
276
261
  if (typoGridColumnIndex < typoGridRow.length - 1) {
277
262
  const width = typoGridCell.computeRawLength();
278
263
  const padding = " ".repeat(widths[typoGridColumnIndex]! - width);
279
- printableGridRow.push(padding);
264
+ styledGridRow.push(padding);
280
265
  }
281
266
  }
282
- printableGrid.push(printableGridRow);
267
+ styledLines.push(styledGridRow.join(""));
283
268
  }
284
- return printableGrid;
269
+ return styledLines;
285
270
  }
286
271
  }
287
272
 
288
273
  /**
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.
274
+ * `Error` subclass with a {@link TypoText} styled message for rich terminal output.
275
+ * Chains `TypoError` sources after `": "`.
299
276
  */
300
277
  export class TypoError extends Error {
301
278
  #typoText: TypoText;
302
279
  /**
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()`.
280
+ * @param currentTypoText - Styled message for this error.
281
+ * @param source - Optional cause; `TypoError` chains styled text, `Error` appends `.message`, else `String()`.
307
282
  */
308
283
  constructor(currentTypoText: TypoText, source?: unknown) {
309
284
  const typoText = new TypoText();
310
- typoText.pushText(currentTypoText);
285
+ typoText.push(currentTypoText);
311
286
  if (source instanceof TypoError) {
312
- typoText.pushString(new TypoString(": "));
313
- typoText.pushText(source.#typoText);
287
+ typoText.push(new TypoString(": "));
288
+ typoText.push(source.#typoText);
314
289
  } else if (source instanceof Error) {
315
- typoText.pushString(new TypoString(`: ${source.message}`));
290
+ typoText.push(new TypoString(`: ${source.message}`));
316
291
  } else if (source !== undefined) {
317
- typoText.pushString(new TypoString(`: ${String(source)}`));
292
+ typoText.push(new TypoString(`: ${String(source)}`));
318
293
  }
319
294
  super(typoText.computeRawString());
320
295
  this.#typoText = typoText;
321
296
  }
322
297
  /**
323
- * Renders this error's styled message as a string.
298
+ * Renders the styled message (without `"Error:"` prefix).
324
299
  *
325
- * @param typoSupport - Controls how ANSI styles are applied.
326
- * @returns The full styled error message (without a leading `"Error:"` prefix).
300
+ * @param typoSupport - Rendering mode.
301
+ * @returns Styled error string.
327
302
  */
328
303
  computeStyledString(typoSupport: TypoSupport): string {
329
304
  return this.#typoText.computeStyledString(typoSupport);
330
305
  }
331
306
  /**
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: ...").
307
+ * Runs `thrower`; wraps any thrown error as a `TypoError` with `context()` prepended.
338
308
  *
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.
309
+ * @typeParam Value - Return type of `thrower`.
310
+ * @param thrower - Function to execute; result passed through on success.
311
+ * @param context - Produces the {@link TypoText} prepended to the caught error.
312
+ * @returns Value from `thrower`.
313
+ * @throws `TypoError` wrapping the original error with context prepended.
346
314
  */
347
315
  static tryWithContext<Value>(
348
316
  thrower: () => Value,
@@ -357,19 +325,7 @@ export class TypoError extends Error {
357
325
  }
358
326
 
359
327
  /**
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}.
328
+ * Controls ANSI terminal styling. Create via the static factory methods.
373
329
  */
374
330
  export class TypoSupport {
375
331
  #kind: "none" | "tty" | "mock";
@@ -377,39 +333,29 @@ export class TypoSupport {
377
333
  this.#kind = kind;
378
334
  }
379
335
  /**
380
- * Returns a `TypoSupport` that strips all styling every styled string is returned
381
- * as-is (plain text, no ANSI codes).
336
+ * Plain textno ANSI codes.
382
337
  */
383
338
  static none(): TypoSupport {
384
339
  return new TypoSupport("none");
385
340
  }
386
341
  /**
387
- * Returns a `TypoSupport` that applies ANSI escape codes.
388
- * Use this when writing to a color-capable terminal (`stdout.isTTY === true`).
342
+ * ANSI escape codes for color terminals.
389
343
  */
390
344
  static tty(): TypoSupport {
391
345
  return new TypoSupport("tty");
392
346
  }
393
347
  /**
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.
348
+ * Deterministic textual styling for snapshot tests.
349
+ * Style flags appear as suffixes: `{text}@color`, `{text}+` (bold), `{text}-` (dim),
350
+ * `{text}*` (italic), `{text}_` (underline), `{text}~` (strikethrough).
399
351
  */
400
352
  static mock(): TypoSupport {
401
353
  return new TypoSupport("mock");
402
354
  }
403
355
  /**
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).
356
+ * Auto-detects styling mode from the process environment.
357
+ * `FORCE_COLOR=0` / `NO_COLOR` → none; `FORCE_COLOR` (truthy) / `isTTY` → tty; else → none.
358
+ * Falls back to none if `process` is unavailable.
413
359
  */
414
360
  static inferFromProcess(): TypoSupport {
415
361
  if (!process) {
@@ -432,15 +378,11 @@ export class TypoSupport {
432
378
  return TypoSupport.none();
433
379
  }
434
380
  /**
435
- * Applies the given {@link TypoStyle} to `value` and returns the styled string.
381
+ * Applies `typoStyle` to `value` according to the current mode.
436
382
  *
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.
383
+ * @param value - Raw text.
384
+ * @param typoStyle - Style to apply.
385
+ * @returns Styled string.
444
386
  */
445
387
  computeStyledString(value: string, typoStyle: TypoStyle): string {
446
388
  if (this.#kind === "none") {
@@ -483,16 +425,10 @@ export class TypoSupport {
483
425
  throw new Error(`Unknown TypoSupport kind: ${this.#kind}`);
484
426
  }
485
427
  /**
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}.
428
+ * Formats any thrown value as `"Error: <message>"` with {@link typoStyleFailure} on the prefix.
493
429
  *
494
- * @param error - The error to format (any value thrown by a handler).
495
- * @returns A styled error string ready to print to stderr.
430
+ * @param error - Any thrown value.
431
+ * @returns Styled error string.
496
432
  */
497
433
  computeStyledErrorMessage(error: unknown): string {
498
434
  return [