cli-kiss 0.2.8 → 0.2.9

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
@@ -12,9 +12,20 @@ import {
12
12
  * Decodes a raw CLI string into a typed value.
13
13
  * A pair of a human-readable `content` name and a `decoder` function.
14
14
  *
15
- * Built-in: {@link type}, {@link typeBoolean}, {@link typeNumber},
16
- * {@link typeInteger}, {@link typeDatetime}, {@link typeUrl}.
17
- * Composite: {@link typeChoice}, {@link typeConverted}, {@link typeTuple}, {@link typeList}.
15
+ * Primitive Types:
16
+ * - {@link typeString}
17
+ * - {@link typeBoolean}
18
+ * - {@link typeNumber},
19
+ * - {@link typeInteger}
20
+ * - {@link typeDatetime}
21
+ * - {@link typeUrl}
22
+ * - {@link typePath}
23
+ * - {@link typeChoice}
24
+ *
25
+ * Composed Types:
26
+ * - {@link typeMapped}
27
+ * - {@link typeTuple}
28
+ * - {@link typeList}
18
29
  *
19
30
  * @typeParam Value - Type produced by the decoder.
20
31
  */
@@ -33,6 +44,22 @@ export type Type<Value> = {
33
44
  decoder(input: string): Value;
34
45
  };
35
46
 
47
+ /**
48
+ * A named type that accepts any string as input.
49
+ * @param name - Name shown in help and errors (e.g. `"my-value"`).
50
+ * @example
51
+ * ```ts
52
+ * typeString("greeting").decoder("hello") // → "hello"
53
+ * typeString("greeting").decoder("") // → ""
54
+ * ```
55
+ */
56
+ export function typeString(name?: string): Type<string> {
57
+ return {
58
+ content: name ?? "string",
59
+ decoder: (input: string) => input,
60
+ };
61
+ }
62
+
36
63
  /**
37
64
  * Decodes a string to `boolean` (case-insensitive).
38
65
  * Used by {@link optionFlag} for `--flag=<value>`.
@@ -41,10 +68,11 @@ export type Type<Value> = {
41
68
  * ```ts
42
69
  * typeBoolean("flag").decoder("true") // → true
43
70
  * typeBoolean("flag").decoder("yes") // → true
44
- * typeBoolean("flag").decoder("y") // → true
45
- * typeBoolean("flag").decoder("false") // → false
46
- * typeBoolean("flag").decoder("no") // → false
71
+ * typeBoolean("flag").decoder("Y") // → true
72
+ * typeBoolean("flag").decoder("FALSE") // → false
73
+ * typeBoolean("flag").decoder("NO") // → false
47
74
  * typeBoolean("flag").decoder("n") // → false
75
+ * typeBoolean("flag").decoder("maybe") // throws
48
76
  * ```
49
77
  */
50
78
  export function typeBoolean(name?: string): Type<boolean> {
@@ -62,36 +90,6 @@ export function typeBoolean(name?: string): Type<boolean> {
62
90
  },
63
91
  };
64
92
  }
65
- const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
66
- const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
67
-
68
- /**
69
- * Parses a date/time string via `Date.parse`.
70
- * Accepts any format supported by `Date.parse`, including ISO 8601.
71
- *
72
- * @example
73
- * ```ts
74
- * typeDatetime("my-datetime").decoder("2024-01-15") // → Date object for 2024-01-15
75
- * typeDatetime("my-datetime").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
76
- * typeDatetime("my-datetime").decoder("not a date") // throws TypoError
77
- * ```
78
- */
79
- export function typeDatetime(name?: string): Type<Date> {
80
- return {
81
- content: name ?? "datetime",
82
- decoder(input: string) {
83
- try {
84
- const timestampMs = Date.parse(input);
85
- if (isNaN(timestampMs)) {
86
- throw new Error();
87
- }
88
- return new Date(timestampMs);
89
- } catch {
90
- throwInvalidValue("a valid ISO_8601 datetime", input);
91
- }
92
- },
93
- };
94
- }
95
93
 
96
94
  /**
97
95
  * Parses a string to `number` via `Number()`; `NaN` throws.
@@ -145,118 +143,68 @@ export function typeInteger(name?: string): Type<bigint> {
145
143
  }
146
144
 
147
145
  /**
148
- * Parses an absolute URL string to a `URL` object.
149
- * Relative or malformed URLs throws.
146
+ * Parses a date/time string via `Date.parse`.
147
+ * Accepts any format supported by `Date.parse`, including ISO 8601.
150
148
  *
151
149
  * @example
152
150
  * ```ts
153
- * typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
154
- * typeUrl("my-url").decoder("not-a-url") // throws
151
+ * typeDatetime("start").decoder("2024-01-15") // → Date object for 2024-01-15
152
+ * typeDatetime("start").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
153
+ * typeDatetime("start").decoder("not a date") // throws
155
154
  * ```
156
155
  */
157
- export function typeUrl(name?: string): Type<URL> {
156
+ export function typeDatetime(name?: string): Type<Date> {
158
157
  return {
159
- content: name ?? "url",
158
+ content: name ?? "datetime",
160
159
  decoder(input: string) {
161
160
  try {
162
- return new URL(input);
161
+ const timestampMs = Date.parse(input);
162
+ if (isNaN(timestampMs)) {
163
+ throw new Error();
164
+ }
165
+ return new Date(timestampMs);
163
166
  } catch {
164
- throwInvalidValue("an URL", input);
167
+ throwInvalidValue("a valid ISO_8601 datetime", input);
165
168
  }
166
169
  },
167
170
  };
168
171
  }
169
172
 
170
173
  /**
171
- * A named type that accepts any string as input.
172
- * @param name - Name shown in help and errors (e.g. `"my-value"`).
173
- * @example
174
- * ```ts
175
- * type("greeting").decoder("hello") // → "hello"
176
- * type("greeting").decoder("") // → ""
177
- * ```
178
- */
179
- export function type(name?: string): Type<string> {
180
- return {
181
- content: name ?? "string",
182
- decoder: (input: string) => input,
183
- };
184
- }
185
-
186
- /**
187
- * Chains `before`'s decoder with an `after` transformation.
188
- * `before` errors are prefixed with `"from: <content>"` for traceability.
189
- *
190
- * @typeParam Before - Intermediate type from `before.decoder`.
191
- * @typeParam After - Final type from `after.decoder`.
192
- *
193
- * @param name - Name shown in help and errors (e.g. `"my-value"`).
194
- * @param before - Base type to decode the raw string.
195
- * @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
196
- * @returns A {@link Type}`<After>`.
174
+ * Parses an absolute URL string to a `URL` object.
175
+ * Relative or malformed URLs throws.
197
176
  *
198
177
  * @example
199
178
  * ```ts
200
- * const typePort = typeConverted("port", typeNumber(), (n) => {
201
- * if (n < 1 || n > 65535) throw new Error("Out of range");
202
- * return n;
203
- * });
204
- * // "--port 8080" → 8080
205
- * // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
179
+ * typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
180
+ * typeUrl("my-url").decoder("not-a-url") // throws
206
181
  * ```
207
182
  */
208
- export function typeConverted<Before, After>(
209
- name: string,
210
- before: Type<Before>,
211
- mapper: (value: Before) => After,
212
- ): Type<After> {
213
- return {
214
- content: name,
215
- decoder: (input: string) => {
216
- return mapper(
217
- TypoError.tryWithContext(
218
- () => before.decoder(input),
219
- () =>
220
- new TypoText(
221
- new TypoString("from: "),
222
- new TypoString(before.content, typoStyleLogic),
223
- ),
224
- ),
225
- );
226
- },
227
- };
228
- }
229
-
230
- /**
231
- * Adds a name to a {@link Type} for clearer error messages and help text.
232
- *
233
- * @param name - Name to use for the type.
234
- * @param type - Base type to name.
235
- * @returns A {@link Type} with the given name.
236
- */
237
- export function typeRenamed<Value>(
238
- type: Type<Value>,
239
- name: string,
240
- ): Type<Value> {
183
+ export function typeUrl(name?: string): Type<URL> {
241
184
  return {
242
- content: name,
243
- decoder: (input: string) => {
244
- return TypoError.tryWithContext(
245
- () => type.decoder(input),
246
- () =>
247
- new TypoText(
248
- new TypoString("from: "),
249
- new TypoString(type.content, typoStyleLogic),
250
- ),
251
- );
185
+ content: name ?? "url",
186
+ decoder(input: string) {
187
+ try {
188
+ return new URL(input);
189
+ } catch {
190
+ throwInvalidValue("an URL", input);
191
+ }
252
192
  },
253
193
  };
254
194
  }
255
195
 
256
196
  /**
257
197
  * Creates a {@link Type} for filesystem paths with optional existence checks.
198
+ *
258
199
  * @param checks - Optional checks for path existence and type (file/directory).
259
200
  * @returns A {@link Type}`<string>` representing the path.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const typeInputFile = typePath("input-file", { checkSyncExistAs: "file" });
205
+ * typeInputFile.decoder("data.txt"); // → "data.txt" if it exists and is a file, otherwise throws
206
+ * typeInputFile.decoder("somedir"); // throws if "somedir" exists and is a directory
207
+ * ```
260
208
  */
261
209
  export function typePath(
262
210
  name?: string,
@@ -365,6 +313,78 @@ export function typeChoice<const Value extends string>(
365
313
  };
366
314
  }
367
315
 
316
+ /**
317
+ * Chains `before`'s decoder with an `after` transformation.
318
+ * `before` errors are prefixed with `"from: <content>"` for traceability.
319
+ *
320
+ * @typeParam Before - Intermediate type from `before.decoder`.
321
+ * @typeParam After - Final type from `after.decoder`.
322
+ *
323
+ * @param name - Name shown in help and errors (e.g. `"my-value"`).
324
+ * @param before - Base type to decode the raw string.
325
+ * @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
326
+ * @returns A {@link Type}`<After>`.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * const typePort = typeMapped("port", typeNumber(), (n) => {
331
+ * if (n < 1 || n > 65535) {
332
+ * throw new Error("Out of range");
333
+ * }
334
+ * return n;
335
+ * });
336
+ * typePort.decoder("8080"); // → 8080
337
+ * typePort.decoder("70000"); // throws
338
+ * ```
339
+ */
340
+ export function typeMapped<Before, After>(
341
+ name: string,
342
+ before: Type<Before>,
343
+ mapper: (value: Before) => After,
344
+ ): Type<After> {
345
+ return {
346
+ content: name,
347
+ decoder: (input: string) => {
348
+ return mapper(
349
+ TypoError.tryWithContext(
350
+ () => before.decoder(input),
351
+ () =>
352
+ new TypoText(
353
+ new TypoString("from: "),
354
+ new TypoString(before.content, typoStyleLogic),
355
+ ),
356
+ ),
357
+ );
358
+ },
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Adds a name to a {@link Type} for clearer error messages and help text.
364
+ *
365
+ * @param name - Name to use for the type.
366
+ * @param type - Base type to name.
367
+ * @returns A {@link Type} with the given name.
368
+ */
369
+ export function typeRenamed<Value>(
370
+ type: Type<Value>,
371
+ name: string,
372
+ ): Type<Value> {
373
+ return {
374
+ content: name,
375
+ decoder: (input: string) => {
376
+ return TypoError.tryWithContext(
377
+ () => type.decoder(input),
378
+ () =>
379
+ new TypoText(
380
+ new TypoString("from: "),
381
+ new TypoString(type.content, typoStyleLogic),
382
+ ),
383
+ );
384
+ },
385
+ };
386
+ }
387
+
368
388
  /**
369
389
  * Splits a delimited string into a typed tuple.
370
390
  * Each part is decoded by the corresponding element type; wrong count or decode failure throws.
@@ -436,7 +456,9 @@ export function typeTuple<const Elements extends Array<any>>(
436
456
  * typeNumbers.decoder("1,2,3") // → [1, 2, 3]
437
457
  * typeNumbers.decoder("1,x,3") // throws
438
458
  * const typePaths = typeList(typePath(), ":");
439
- * typePaths.decoder("/usr/bin:/usr/local/bin") // → ["/usr/bin", "/usr/local/bin"]
459
+ * typePaths.decoder("/bin:/usr") // → ["/bin", "/usr"]
460
+ * typePaths.decoder("/usr/bin") // → ["/usr/bin"]
461
+ * typePaths.decoder("") // → throws
440
462
  * ```
441
463
  */
442
464
  export function typeList<Value>(
@@ -470,3 +492,6 @@ function throwInvalidValue(kind: string, input: string): never {
470
492
  ),
471
493
  );
472
494
  }
495
+
496
+ const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
497
+ const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
package/src/lib/Typo.ts CHANGED
@@ -402,7 +402,7 @@ export class TypoSupport {
402
402
  return TypoSupport.tty();
403
403
  }
404
404
  if (envForceColor === "3") {
405
- return TypoSupport.tty(); // TODO - should there be fancy colors possible?
405
+ return TypoSupport.tty();
406
406
  }
407
407
  if (envForceColor) {
408
408
  return TypoSupport.tty();
@@ -1,9 +1,5 @@
1
1
  import { expect, it } from "@jest/globals";
2
- import {
3
- ReaderArgs,
4
- ReaderOptionNextGuard,
5
- ReaderOptionRestGuard,
6
- } from "../src";
2
+ import { ReaderArgs, ReaderOptionNextGuard } from "../src";
7
3
 
8
4
  it("run", async function () {
9
5
  const stream = new ReaderArgs([
@@ -78,59 +74,59 @@ it("run", async function () {
78
74
 
79
75
  const kA = stream.registerOptionShort({
80
76
  key: "a",
81
- restGuard: optionFlagRestGuard,
82
77
  nextGuard: optionFlagNextGuard,
78
+ consumeGroupRestAsValue: false,
83
79
  });
84
80
  const kB = stream.registerOptionShort({
85
81
  key: "b",
86
- restGuard: optionValuedRestGuard,
87
82
  nextGuard: optionValuedNextGuard,
83
+ consumeGroupRestAsValue: true,
88
84
  });
89
85
 
90
86
  const kC = stream.registerOptionShort({
91
87
  key: "c",
92
- restGuard: optionFlagRestGuard,
93
88
  nextGuard: optionFlagNextGuard,
89
+ consumeGroupRestAsValue: false,
94
90
  });
95
91
  const kD = stream.registerOptionShort({
96
92
  key: "d",
97
- restGuard: optionValuedRestGuard,
98
93
  nextGuard: optionValuedNextGuard,
94
+ consumeGroupRestAsValue: true,
99
95
  });
100
96
 
101
97
  const kE = stream.registerOptionShort({
102
98
  key: "e",
103
- restGuard: optionFlagRestGuard,
104
99
  nextGuard: optionFlagNextGuard,
100
+ consumeGroupRestAsValue: false,
105
101
  });
106
102
  const kF = stream.registerOptionShort({
107
103
  key: "f",
108
- restGuard: optionValuedRestGuard,
109
104
  nextGuard: optionValuedNextGuard,
105
+ consumeGroupRestAsValue: true,
110
106
  });
111
107
 
112
108
  expect(stream.consumePositional()).toStrictEqual("positional-2");
113
109
 
114
110
  const kG = stream.registerOptionShort({
115
111
  key: "g",
116
- restGuard: optionFlagRestGuard,
117
112
  nextGuard: optionFlagNextGuard,
113
+ consumeGroupRestAsValue: false,
118
114
  });
119
115
  const kH = stream.registerOptionShort({
120
116
  key: "h",
121
- restGuard: optionValuedRestGuard,
122
117
  nextGuard: optionValuedNextGuard,
118
+ consumeGroupRestAsValue: true,
123
119
  });
124
120
 
125
121
  const kI = stream.registerOptionShort({
126
122
  key: "i",
127
- restGuard: optionFlagRestGuard,
128
123
  nextGuard: optionFlagNextGuard,
124
+ consumeGroupRestAsValue: false,
129
125
  });
130
126
  const kJ = stream.registerOptionShort({
131
127
  key: "j",
132
- restGuard: optionValuedRestGuard,
133
128
  nextGuard: optionValuedNextGuard,
129
+ consumeGroupRestAsValue: true,
134
130
  });
135
131
 
136
132
  expect(stream.consumePositional()).toStrictEqual("positional-3");
@@ -141,56 +137,47 @@ it("run", async function () {
141
137
  expect(stream.consumePositional()).toStrictEqual("positional-4");
142
138
  expect(stream.consumePositional()).toStrictEqual(undefined);
143
139
 
144
- expect(kFlagUnset().values).toStrictEqual([]);
145
- expect(kFlagNo().values).toStrictEqual([{ inlined: null, separated: [] }]);
146
- expect(kFlagNormal().values).toStrictEqual([
147
- { inlined: null, separated: [] },
148
- ]);
149
- expect(kFlagPositive().values).toStrictEqual([
150
- { inlined: "true", separated: [] },
151
- ]);
152
- expect(kFlagNegative().values).toStrictEqual([
153
- { inlined: "false", separated: [] },
154
- ]);
140
+ expect(kFlagUnset()).toStrictEqual([]);
141
+ expect(kFlagNo()).toStrictEqual([{ inlined: null, separated: [] }]);
142
+ expect(kFlagNormal()).toStrictEqual([{ inlined: null, separated: [] }]);
143
+ expect(kFlagPositive()).toStrictEqual([{ inlined: "true", separated: [] }]);
144
+ expect(kFlagNegative()).toStrictEqual([{ inlined: "false", separated: [] }]);
155
145
 
156
- expect(kOptionUnset().values).toStrictEqual([]);
157
- expect(kOptionSplit().values).toStrictEqual([
146
+ expect(kOptionUnset()).toStrictEqual([]);
147
+ expect(kOptionSplit()).toStrictEqual([
158
148
  { inlined: null, separated: ["1.1"] },
159
149
  { inlined: null, separated: ["1.2"] },
160
150
  ]);
161
- expect(kOptionJoin().values).toStrictEqual([
151
+ expect(kOptionJoin()).toStrictEqual([
162
152
  { inlined: "2.1", separated: [] },
163
153
  { inlined: "2.2", separated: [] },
164
154
  ]);
165
155
 
166
- expect(kA().values).toStrictEqual([{ inlined: null, separated: [] }]);
167
- expect(kB().values).toStrictEqual([
156
+ expect(kA()).toStrictEqual([{ inlined: null, separated: [] }]);
157
+ expect(kB()).toStrictEqual([
168
158
  { inlined: null, separated: ["3.1"] },
169
159
  { inlined: null, separated: ["3.2"] },
170
160
  ]);
171
161
 
172
- expect(kC().values).toStrictEqual([{ inlined: null, separated: [] }]);
173
- expect(kD().values).toStrictEqual([
162
+ expect(kC()).toStrictEqual([{ inlined: null, separated: [] }]);
163
+ expect(kD()).toStrictEqual([
174
164
  { inlined: "4.1", separated: [] },
175
165
  { inlined: "4.2", separated: [] },
176
166
  ]);
177
167
 
178
- expect(kE().values).toStrictEqual([{ inlined: null, separated: [] }]);
179
- expect(kF().values).toStrictEqual([
168
+ expect(kE()).toStrictEqual([{ inlined: null, separated: [] }]);
169
+ expect(kF()).toStrictEqual([
180
170
  { inlined: "5.1", separated: [] },
181
171
  { inlined: "5.2", separated: [] },
182
172
  ]);
183
173
 
184
- expect(kG().values).toStrictEqual([{ inlined: null, separated: [] }]);
185
- expect(kH().values).toStrictEqual([{ inlined: "FALSE", separated: [] }]);
174
+ expect(kG()).toStrictEqual([{ inlined: null, separated: [] }]);
175
+ expect(kH()).toStrictEqual([{ inlined: "FALSE", separated: [] }]);
186
176
 
187
- expect(kI().values).toStrictEqual([{ inlined: null, separated: [] }]);
188
- expect(kJ().values).toStrictEqual([{ inlined: "TRUE", separated: [] }]);
177
+ expect(kI()).toStrictEqual([{ inlined: null, separated: [] }]);
178
+ expect(kJ()).toStrictEqual([{ inlined: "TRUE", separated: [] }]);
189
179
  });
190
180
 
191
- const optionFlagRestGuard: ReaderOptionRestGuard = () => false;
192
181
  const optionFlagNextGuard: ReaderOptionNextGuard = () => false;
193
-
194
- const optionValuedRestGuard: ReaderOptionRestGuard = () => true;
195
182
  const optionValuedNextGuard: ReaderOptionNextGuard = (value) =>
196
183
  value.inlined === null && value.separated.length === 0;
@@ -11,7 +11,7 @@ it("run", async function () {
11
11
  });
12
12
  expect(readerArgs1.consumePositional()).toStrictEqual("C");
13
13
  expect(readerArgs1.consumePositional()).toStrictEqual(undefined);
14
- expect(kOptionVariadicStop().values).toStrictEqual([
14
+ expect(kOptionVariadicStop()).toStrictEqual([
15
15
  { inlined: "1", separated: ["A", "B", "STOP"] },
16
16
  ]);
17
17
 
@@ -21,7 +21,7 @@ it("run", async function () {
21
21
  nextGuard: (_value, nextArg) => nextArg !== undefined,
22
22
  });
23
23
  expect(readerArgs2.consumePositional()).toStrictEqual(undefined);
24
- expect(kOptionVariadicFull().values).toStrictEqual([
24
+ expect(kOptionVariadicFull()).toStrictEqual([
25
25
  { inlined: "1", separated: ["A", "B", "C"] },
26
26
  ]);
27
27
 
@@ -31,7 +31,7 @@ it("run", async function () {
31
31
  nextGuard: (value) => value.separated.length < Number(value.inlined ?? "0"),
32
32
  });
33
33
  expect(readerArgs3.consumePositional()).toStrictEqual(undefined);
34
- expect(kOptionVariadicKeyed().values).toStrictEqual([
34
+ expect(kOptionVariadicKeyed()).toStrictEqual([
35
35
  { inlined: "2", separated: ["A", "B"] },
36
36
  { inlined: "1", separated: ["C"] },
37
37
  ]);