cli-kiss 0.2.2 → 0.2.3
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 +509 -820
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +3 -0
- package/docs/guide/01_getting_started.md +4 -4
- package/docs/guide/02_commands.md +5 -10
- package/docs/guide/03_options.md +4 -6
- package/docs/guide/04_positionals.md +6 -8
- package/docs/guide/05_types.md +8 -10
- package/docs/guide/06_run.md +3 -4
- package/docs/index.md +3 -3
- package/package.json +1 -1
- package/src/lib/Command.ts +178 -245
- package/src/lib/Operation.ts +60 -80
- package/src/lib/Option.ts +112 -123
- package/src/lib/Positional.ts +59 -92
- package/src/lib/Reader.ts +53 -78
- package/src/lib/Run.ts +39 -57
- package/src/lib/Type.ts +81 -141
- package/src/lib/Typo.ts +121 -164
- package/src/lib/Usage.ts +56 -18
- package/tests/unit.command.execute.ts +81 -69
- package/tests/unit.command.usage.ts +228 -107
- package/tests/unit.runner.cycle.ts +37 -7
package/src/lib/Type.ts
CHANGED
|
@@ -7,44 +7,33 @@ import {
|
|
|
7
7
|
} from "./Typo";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Decodes a raw CLI string into a typed value.
|
|
11
|
+
* A pair of a human-readable `content` name (e.g. `"Number"`) and a `decoder` function.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
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},
|
|
13
|
+
* Built-in: {@link typeString}, {@link typeBoolean}, {@link typeNumber},
|
|
19
14
|
* {@link typeInteger}, {@link typeDate}, {@link typeUrl}.
|
|
15
|
+
* Composite: {@link typeOneOf}, {@link typeMapped}, {@link typeTuple}, {@link typeList}.
|
|
20
16
|
*
|
|
21
|
-
*
|
|
22
|
-
* {@link typeList}.
|
|
23
|
-
*
|
|
24
|
-
* @typeParam Value - The TypeScript type that the decoder produces.
|
|
17
|
+
* @typeParam Value - Type produced by the decoder.
|
|
25
18
|
*/
|
|
26
19
|
export type Type<Value> = {
|
|
27
20
|
/**
|
|
28
|
-
* Human-readable name
|
|
29
|
-
* Examples: `"String"`, `"Number"`, `"Url"`.
|
|
21
|
+
* Human-readable name shown in help and error messages (e.g. `"String"`, `"Number"`).
|
|
30
22
|
*/
|
|
31
23
|
content: string;
|
|
32
24
|
/**
|
|
33
|
-
*
|
|
25
|
+
* Converts a raw CLI string into `Value`.
|
|
34
26
|
*
|
|
35
|
-
* @param
|
|
27
|
+
* @param input - Raw string from the command line.
|
|
36
28
|
* @returns The decoded value.
|
|
37
|
-
* @throws {@link TypoError}
|
|
29
|
+
* @throws {@link TypoError} on invalid input.
|
|
38
30
|
*/
|
|
39
|
-
decoder(
|
|
31
|
+
decoder(input: string): Value;
|
|
40
32
|
};
|
|
41
33
|
|
|
42
34
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* Primarily used internally by {@link optionFlag} for the `--flag=<value>` syntax, but
|
|
47
|
-
* can also be used in positionals or valued options.
|
|
35
|
+
* Decodes `"true"` / `"yes"` → `true` and `"false"` / `"no"` → `false` (case-insensitive).
|
|
36
|
+
* Used internally by {@link optionFlag} for the `--flag=<value>` syntax.
|
|
48
37
|
*
|
|
49
38
|
* @example
|
|
50
39
|
* ```ts
|
|
@@ -55,31 +44,26 @@ export type Type<Value> = {
|
|
|
55
44
|
*/
|
|
56
45
|
export const typeBoolean: Type<boolean> = {
|
|
57
46
|
content: "Boolean",
|
|
58
|
-
decoder(
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
47
|
+
decoder(input: string) {
|
|
48
|
+
const lower = input.toLowerCase();
|
|
49
|
+
if (lower === "true" || lower === "yes") {
|
|
61
50
|
return true;
|
|
62
51
|
}
|
|
63
|
-
if (
|
|
52
|
+
if (lower === "false" || lower === "no") {
|
|
64
53
|
return false;
|
|
65
54
|
}
|
|
66
55
|
throw new TypoError(
|
|
67
56
|
new TypoText(
|
|
68
57
|
new TypoString(`Invalid value: `),
|
|
69
|
-
new TypoString(`"${
|
|
58
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
70
59
|
),
|
|
71
60
|
);
|
|
72
61
|
},
|
|
73
62
|
};
|
|
74
63
|
|
|
75
64
|
/**
|
|
76
|
-
*
|
|
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))`.
|
|
65
|
+
* Parses a date/time string via `Date.parse` into a `Date` object.
|
|
66
|
+
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
83
67
|
*
|
|
84
68
|
* @example
|
|
85
69
|
* ```ts
|
|
@@ -90,9 +74,9 @@ export const typeBoolean: Type<boolean> = {
|
|
|
90
74
|
*/
|
|
91
75
|
export const typeDate: Type<Date> = {
|
|
92
76
|
content: "Date",
|
|
93
|
-
decoder(
|
|
77
|
+
decoder(input: string) {
|
|
94
78
|
try {
|
|
95
|
-
const timestampMs = Date.parse(
|
|
79
|
+
const timestampMs = Date.parse(input);
|
|
96
80
|
if (isNaN(timestampMs)) {
|
|
97
81
|
throw new Error();
|
|
98
82
|
}
|
|
@@ -101,7 +85,7 @@ export const typeDate: Type<Date> = {
|
|
|
101
85
|
throw new TypoError(
|
|
102
86
|
new TypoText(
|
|
103
87
|
new TypoString(`Not a valid ISO_8601: `),
|
|
104
|
-
new TypoString(`"${
|
|
88
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
105
89
|
),
|
|
106
90
|
);
|
|
107
91
|
}
|
|
@@ -109,11 +93,8 @@ export const typeDate: Type<Date> = {
|
|
|
109
93
|
};
|
|
110
94
|
|
|
111
95
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* Accepts integers, floating-point values, and scientific notation (e.g. `"3.14"`,
|
|
116
|
-
* `"-1"`, `"1e10"`). Values that produce `NaN` throw a {@link TypoError}.
|
|
96
|
+
* Parses a string into a `number` via `Number()`.
|
|
97
|
+
* Accepts integers, floats, and scientific notation; `NaN` throws a {@link TypoError}.
|
|
117
98
|
*
|
|
118
99
|
* @example
|
|
119
100
|
* ```ts
|
|
@@ -124,9 +105,9 @@ export const typeDate: Type<Date> = {
|
|
|
124
105
|
*/
|
|
125
106
|
export const typeNumber: Type<number> = {
|
|
126
107
|
content: "Number",
|
|
127
|
-
decoder(
|
|
108
|
+
decoder(input: string) {
|
|
128
109
|
try {
|
|
129
|
-
const parsed = Number(
|
|
110
|
+
const parsed = Number(input);
|
|
130
111
|
if (isNaN(parsed)) {
|
|
131
112
|
throw new Error();
|
|
132
113
|
}
|
|
@@ -135,7 +116,7 @@ export const typeNumber: Type<number> = {
|
|
|
135
116
|
throw new TypoError(
|
|
136
117
|
new TypoText(
|
|
137
118
|
new TypoString(`Unable to parse: `),
|
|
138
|
-
new TypoString(`"${
|
|
119
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
139
120
|
),
|
|
140
121
|
);
|
|
141
122
|
}
|
|
@@ -143,11 +124,8 @@ export const typeNumber: Type<number> = {
|
|
|
143
124
|
};
|
|
144
125
|
|
|
145
126
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* Only accepts valid integer strings (e.g. `"42"`, `"-100"`, `"9007199254740993"`).
|
|
150
|
-
* Floating-point strings or non-numeric values throw a {@link TypoError}.
|
|
127
|
+
* Parses an integer string into a `bigint` via `BigInt()`.
|
|
128
|
+
* Floats and non-numeric strings throw a {@link TypoError}.
|
|
151
129
|
*
|
|
152
130
|
* @example
|
|
153
131
|
* ```ts
|
|
@@ -158,14 +136,14 @@ export const typeNumber: Type<number> = {
|
|
|
158
136
|
*/
|
|
159
137
|
export const typeInteger: Type<bigint> = {
|
|
160
138
|
content: "Integer",
|
|
161
|
-
decoder(
|
|
139
|
+
decoder(input: string) {
|
|
162
140
|
try {
|
|
163
|
-
return BigInt(
|
|
141
|
+
return BigInt(input);
|
|
164
142
|
} catch {
|
|
165
143
|
throw new TypoError(
|
|
166
144
|
new TypoText(
|
|
167
145
|
new TypoString(`Unable to parse: `),
|
|
168
|
-
new TypoString(`"${
|
|
146
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
169
147
|
),
|
|
170
148
|
);
|
|
171
149
|
}
|
|
@@ -173,10 +151,8 @@ export const typeInteger: Type<bigint> = {
|
|
|
173
151
|
};
|
|
174
152
|
|
|
175
153
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* The string must be a valid absolute URL (e.g. `"https://example.com/path?q=1"`).
|
|
179
|
-
* Relative URLs and malformed strings throw a {@link TypoError}.
|
|
154
|
+
* Parses an absolute URL string into a `URL` object.
|
|
155
|
+
* Relative or malformed URLs throw a {@link TypoError}.
|
|
180
156
|
*
|
|
181
157
|
* @example
|
|
182
158
|
* ```ts
|
|
@@ -186,14 +162,14 @@ export const typeInteger: Type<bigint> = {
|
|
|
186
162
|
*/
|
|
187
163
|
export const typeUrl: Type<URL> = {
|
|
188
164
|
content: "Url",
|
|
189
|
-
decoder(
|
|
165
|
+
decoder(input: string) {
|
|
190
166
|
try {
|
|
191
|
-
return new URL(
|
|
167
|
+
return new URL(input);
|
|
192
168
|
} catch {
|
|
193
169
|
throw new TypoError(
|
|
194
170
|
new TypoText(
|
|
195
171
|
new TypoString(`Unable to parse: `),
|
|
196
|
-
new TypoString(`"${
|
|
172
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
197
173
|
),
|
|
198
174
|
);
|
|
199
175
|
}
|
|
@@ -201,9 +177,7 @@ export const typeUrl: Type<URL> = {
|
|
|
201
177
|
};
|
|
202
178
|
|
|
203
179
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* This is the simplest type and accepts any string value without validation.
|
|
180
|
+
* Identity decoder — passes the raw string through unchanged.
|
|
207
181
|
*
|
|
208
182
|
* @example
|
|
209
183
|
* ```ts
|
|
@@ -213,35 +187,27 @@ export const typeUrl: Type<URL> = {
|
|
|
213
187
|
*/
|
|
214
188
|
export const typeString: Type<string> = {
|
|
215
189
|
content: "String",
|
|
216
|
-
decoder(
|
|
217
|
-
return
|
|
190
|
+
decoder(input: string) {
|
|
191
|
+
return input;
|
|
218
192
|
},
|
|
219
193
|
};
|
|
220
194
|
|
|
221
195
|
/**
|
|
222
|
-
* Creates a
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* The raw string is first decoded by `before.decoder`; its result is then passed to
|
|
226
|
-
* `after.decoder`. Errors from `before` are wrapped with a "from: <content>" context
|
|
227
|
-
* prefix so that the full decoding path is visible in error messages.
|
|
228
|
-
*
|
|
229
|
-
* Use this when an existing type (e.g. {@link typeString}, {@link typeOneOf}) produces
|
|
230
|
-
* an intermediate value that needs a further transformation (e.g. parsing a
|
|
231
|
-
* string-keyed enum into a number).
|
|
196
|
+
* Creates a {@link Type} by chaining `before`'s decoder with an `after` transformation.
|
|
197
|
+
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
232
198
|
*
|
|
233
|
-
* @typeParam Before -
|
|
234
|
-
* @typeParam After -
|
|
199
|
+
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
200
|
+
* @typeParam After - Final type from `after.decoder`.
|
|
235
201
|
*
|
|
236
|
-
* @param before -
|
|
237
|
-
* @param after -
|
|
238
|
-
* @param after.content -
|
|
239
|
-
* @param after.decoder -
|
|
240
|
-
* @returns A
|
|
202
|
+
* @param before - Base decoder for the raw string.
|
|
203
|
+
* @param after - Transformation applied to the decoded value.
|
|
204
|
+
* @param after.content - Name for the resulting type (shown in errors).
|
|
205
|
+
* @param after.decoder - Converts a `Before` value to `After`.
|
|
206
|
+
* @returns A {@link Type}`<After>`.
|
|
241
207
|
*
|
|
242
208
|
* @example
|
|
243
209
|
* ```ts
|
|
244
|
-
* const typePort =
|
|
210
|
+
* const typePort = typeMapped(typeNumber, {
|
|
245
211
|
* content: "Port",
|
|
246
212
|
* decoder: (n) => {
|
|
247
213
|
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
@@ -252,16 +218,16 @@ export const typeString: Type<string> = {
|
|
|
252
218
|
* // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
|
|
253
219
|
* ```
|
|
254
220
|
*/
|
|
255
|
-
export function
|
|
221
|
+
export function typeMapped<Before, After>(
|
|
256
222
|
before: Type<Before>,
|
|
257
223
|
after: { content: string; decoder: (value: Before) => After },
|
|
258
224
|
): Type<After> {
|
|
259
225
|
return {
|
|
260
226
|
content: after.content,
|
|
261
|
-
decoder: (
|
|
227
|
+
decoder: (input: string) => {
|
|
262
228
|
return after.decoder(
|
|
263
229
|
TypoError.tryWithContext(
|
|
264
|
-
() => before.decoder(
|
|
230
|
+
() => before.decoder(input),
|
|
265
231
|
() =>
|
|
266
232
|
new TypoText(
|
|
267
233
|
new TypoString("from: "),
|
|
@@ -274,18 +240,12 @@ export function typeConverted<Before, After>(
|
|
|
274
240
|
}
|
|
275
241
|
|
|
276
242
|
/**
|
|
277
|
-
* Creates a {@link Type}`<string>`
|
|
243
|
+
* Creates a {@link Type}`<string>` accepting only a fixed set of string values.
|
|
244
|
+
* Out-of-set inputs throw a {@link TypoError} listing up to 5 valid options.
|
|
278
245
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* Combine with {@link typeConverted} to map the accepted strings to a richer type.
|
|
283
|
-
*
|
|
284
|
-
* @param content - Human-readable name for this type shown in help text and error
|
|
285
|
-
* messages (e.g. `"Environment"`, `"LogLevel"`).
|
|
286
|
-
* @param values - The ordered list of accepted string values. The order is preserved in
|
|
287
|
-
* the error message preview.
|
|
288
|
-
* @returns A {@link Type}`<string>` that validates membership in `values`.
|
|
246
|
+
* @param content - Name shown in help and errors (e.g. `"Environment"`).
|
|
247
|
+
* @param values - Ordered list of accepted values.
|
|
248
|
+
* @returns A {@link Type}`<string>`.
|
|
289
249
|
*
|
|
290
250
|
* @example
|
|
291
251
|
* ```ts
|
|
@@ -294,16 +254,17 @@ export function typeConverted<Before, After>(
|
|
|
294
254
|
* typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
|
|
295
255
|
* ```
|
|
296
256
|
*/
|
|
297
|
-
export function typeOneOf(
|
|
257
|
+
export function typeOneOf<const Value extends string>(
|
|
298
258
|
content: string,
|
|
299
|
-
values: Array<
|
|
300
|
-
): Type<
|
|
301
|
-
const valuesSet = new Set(values);
|
|
259
|
+
values: Array<Value>,
|
|
260
|
+
): Type<Value> {
|
|
302
261
|
return {
|
|
303
262
|
content: content,
|
|
304
|
-
decoder(
|
|
305
|
-
|
|
306
|
-
|
|
263
|
+
decoder(input: string) {
|
|
264
|
+
for (const value of values) {
|
|
265
|
+
if (input === value) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
307
268
|
}
|
|
308
269
|
const valuesPreview = [];
|
|
309
270
|
for (const value of values) {
|
|
@@ -319,7 +280,7 @@ export function typeOneOf(
|
|
|
319
280
|
throw new TypoError(
|
|
320
281
|
new TypoText(
|
|
321
282
|
new TypoString(`Invalid value: `),
|
|
322
|
-
new TypoString(`"${
|
|
283
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
323
284
|
new TypoString(` (expected one of: `),
|
|
324
285
|
...valuesPreview,
|
|
325
286
|
new TypoString(`)`),
|
|
@@ -330,23 +291,14 @@ export function typeOneOf(
|
|
|
330
291
|
}
|
|
331
292
|
|
|
332
293
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* The raw string is split on `separator` into exactly `elementTypes.length` parts.
|
|
337
|
-
* Each part is decoded by its corresponding element type. If the number of splits does
|
|
338
|
-
* not match, or if any element's decoder fails, a {@link TypoError} is thrown with the
|
|
339
|
-
* index and element type context.
|
|
294
|
+
* Splits a delimited string into a fixed-length typed tuple.
|
|
295
|
+
* Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
|
|
340
296
|
*
|
|
341
|
-
*
|
|
342
|
-
* (e.g. `"Number,String"` for a `[number, string]` tuple with `","` separator).
|
|
297
|
+
* @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
|
|
343
298
|
*
|
|
344
|
-
* @
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* @param elementTypes - An ordered array of {@link Type}s, one per tuple element.
|
|
348
|
-
* @param separator - The string used to split the raw value (default: `","`).
|
|
349
|
-
* @returns A {@link Type}`<Elements>` tuple type.
|
|
299
|
+
* @param elementTypes - One {@link Type} per tuple element, in order.
|
|
300
|
+
* @param separator - Delimiter (default `","`).
|
|
301
|
+
* @returns A {@link Type}`<Elements>`.
|
|
350
302
|
*
|
|
351
303
|
* @example
|
|
352
304
|
* ```ts
|
|
@@ -391,26 +343,14 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
391
343
|
}
|
|
392
344
|
|
|
393
345
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* The raw string is split on `separator` and each part is decoded by `elementType`.
|
|
398
|
-
* If any element's decoder fails, a {@link TypoError} is thrown with the index and
|
|
399
|
-
* element type context.
|
|
400
|
-
*
|
|
401
|
-
* Unlike {@link typeTuple}, the number of elements is not fixed; the result array
|
|
402
|
-
* length equals the number of `separator`-delimited parts in the input string. To pass
|
|
403
|
-
* an empty array, the user must pass an empty string (`""`), which splits into one
|
|
404
|
-
* empty-string element — consider using {@link optionRepeatable} instead if you want a
|
|
405
|
-
* naturally empty default.
|
|
346
|
+
* Splits a delimited string into a variable-length typed array.
|
|
347
|
+
* Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
|
|
348
|
+
* Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
|
|
406
349
|
*
|
|
407
|
-
*
|
|
408
|
-
* signal repeatability.
|
|
350
|
+
* @typeParam Value - Element type produced by `elementType.decoder`.
|
|
409
351
|
*
|
|
410
|
-
* @
|
|
411
|
-
*
|
|
412
|
-
* @param elementType - The {@link Type} used to decode each element.
|
|
413
|
-
* @param separator - The string used to split the raw value (default: `","`).
|
|
352
|
+
* @param elementType - Decoder applied to each element.
|
|
353
|
+
* @param separator - Delimiter (default `","`).
|
|
414
354
|
* @returns A {@link Type}`<Array<Value>>`.
|
|
415
355
|
*
|
|
416
356
|
* @example
|
|
@@ -429,8 +369,8 @@ export function typeList<Value>(
|
|
|
429
369
|
): Type<Array<Value>> {
|
|
430
370
|
return {
|
|
431
371
|
content: `${elementType.content}[${separator}${elementType.content}]...`,
|
|
432
|
-
decoder(
|
|
433
|
-
const splits =
|
|
372
|
+
decoder(input: string) {
|
|
373
|
+
const splits = input.split(separator);
|
|
434
374
|
return splits.map((split, index) =>
|
|
435
375
|
TypoError.tryWithContext(
|
|
436
376
|
() => elementType.decoder(split),
|