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/dist/index.d.ts +1308 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/Command.ts +226 -0
- package/src/lib/Operation.ts +108 -0
- package/src/lib/Option.ts +164 -0
- package/src/lib/Positional.ts +139 -0
- package/src/lib/Reader.ts +110 -0
- package/src/lib/Run.ts +86 -22
- package/src/lib/Type.ts +223 -0
- package/src/lib/Typo.ts +225 -0
- package/src/lib/Usage.ts +42 -0
- package/tests/unit.runner.cycle.ts +6 -6
- package/tests/unit.runner.errors.ts +33 -33
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),
|