cli-kiss 0.2.3 → 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/README.md +1 -1
- package/dist/index.d.ts +643 -667
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -3
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/.vitepress/theme/style.css +4 -0
- package/docs/guide/02_commands.md +67 -31
- package/docs/guide/03_options.md +11 -10
- package/docs/guide/05_types.md +1 -1
- package/docs/guide/06_run.md +1 -1
- package/docs/index.md +8 -3
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/lib/Command.ts +36 -93
- package/src/lib/Operation.ts +12 -30
- package/src/lib/Option.ts +120 -96
- package/src/lib/Positional.ts +9 -34
- package/src/lib/Reader.ts +122 -98
- package/src/lib/Run.ts +30 -17
- package/src/lib/Type.ts +26 -22
- package/src/lib/Typo.ts +62 -83
- package/src/lib/Usage.ts +174 -78
- package/tests/unit.Reader.aliases.ts +31 -15
- package/tests/unit.Reader.commons.ts +99 -43
- package/tests/unit.Reader.shortBig.ts +75 -31
- package/tests/unit.command.execute.ts +76 -33
- package/tests/unit.command.usage.ts +35 -35
- package/tests/unit.runner.cycle.ts +13 -13
- package/tests/unit.runner.errors.ts +19 -3
package/src/lib/Reader.ts
CHANGED
|
@@ -7,44 +7,58 @@ import {
|
|
|
7
7
|
} from "./Typo";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Opaque key
|
|
11
|
-
* Returned by {@link ReaderArgs.registerOption}; passed to {@link ReaderArgs.getOptionValues}.
|
|
10
|
+
* Opaque key returned by {@link ReaderArgs.registerOption}.
|
|
12
11
|
*/
|
|
13
12
|
export type ReaderOptionKey = (string | { __brand: "ReaderOptionKey" }) & {
|
|
14
13
|
__brand: "ReaderOptionKey";
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
17
|
+
* Parsing behaviour for a registered option, passed to {@link ReaderArgs.registerOption}.
|
|
18
|
+
*/
|
|
19
|
+
export type ReaderOptionParsing = {
|
|
20
|
+
consumeShortGroup: boolean;
|
|
21
|
+
consumeNextArg: (
|
|
22
|
+
inlined: string | null,
|
|
23
|
+
separated: Array<string>,
|
|
24
|
+
next: string | undefined,
|
|
25
|
+
) => boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Result of parsing an option, including its inlined value and any following separated values.
|
|
30
|
+
*/
|
|
31
|
+
export type ReaderOptionValue = {
|
|
32
|
+
inlined: string | null;
|
|
33
|
+
separated: Array<string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Option registration/query interface. Subset of {@link ReaderArgs}.
|
|
20
38
|
*/
|
|
21
39
|
export type ReaderOptions = {
|
|
22
40
|
/**
|
|
23
|
-
* Registers an option
|
|
41
|
+
* Registers an option; all `longs` and `shorts` share the same key.
|
|
24
42
|
*
|
|
25
43
|
* @param definition.longs - Long-form names (without `--`).
|
|
26
44
|
* @param definition.shorts - Short-form names (without `-`).
|
|
27
|
-
* @param definition.
|
|
45
|
+
* @param definition.parsing - Parsing behaviour.
|
|
28
46
|
* @returns A {@link ReaderOptionKey} for later retrieval.
|
|
29
47
|
* @throws `Error` if a name is already registered or short names overlap.
|
|
30
48
|
*/
|
|
31
49
|
registerOption(definition: {
|
|
32
50
|
longs: Array<string>;
|
|
33
51
|
shorts: Array<string>;
|
|
34
|
-
|
|
52
|
+
parsing: ReaderOptionParsing;
|
|
35
53
|
}): ReaderOptionKey;
|
|
36
54
|
/**
|
|
37
|
-
* Returns all values collected for
|
|
38
|
-
*
|
|
39
|
-
* @param key - Key from {@link ReaderOptions.registerOption}.
|
|
40
|
-
* @returns Raw string values, one per occurrence; empty if never provided.
|
|
41
|
-
* @throws `Error` if `key` was not registered.
|
|
55
|
+
* Returns all values collected for `key`.
|
|
42
56
|
*/
|
|
43
|
-
getOptionValues(key: ReaderOptionKey): Array<
|
|
57
|
+
getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue>;
|
|
44
58
|
};
|
|
45
59
|
|
|
46
60
|
/**
|
|
47
|
-
* Positional
|
|
61
|
+
* Positional consumption interface. Subset of {@link ReaderArgs}.
|
|
48
62
|
*/
|
|
49
63
|
export type ReaderPositionals = {
|
|
50
64
|
/**
|
|
@@ -69,10 +83,9 @@ export class ReaderArgs {
|
|
|
69
83
|
#args: ReadonlyArray<string>;
|
|
70
84
|
#parsedIndex: number;
|
|
71
85
|
#parsedDouble: boolean;
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#resultByKey: Map<ReaderOptionKey, Array<string>>;
|
|
86
|
+
#optionContextByLong: Map<string, ReaderOptionContext>;
|
|
87
|
+
#optionContextByShort: Map<string, ReaderOptionContext>;
|
|
88
|
+
#optionContextByKey: Map<ReaderOptionKey, ReaderOptionContext>;
|
|
76
89
|
|
|
77
90
|
/**
|
|
78
91
|
* @param args - Raw CLI tokens (e.g. `process.argv.slice(2)`). Not mutated.
|
|
@@ -81,27 +94,25 @@ export class ReaderArgs {
|
|
|
81
94
|
this.#args = args;
|
|
82
95
|
this.#parsedIndex = 0;
|
|
83
96
|
this.#parsedDouble = false;
|
|
84
|
-
this.#
|
|
85
|
-
this.#
|
|
86
|
-
this.#
|
|
87
|
-
this.#resultByKey = new Map();
|
|
97
|
+
this.#optionContextByLong = new Map();
|
|
98
|
+
this.#optionContextByShort = new Map();
|
|
99
|
+
this.#optionContextByKey = new Map();
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/**
|
|
91
103
|
* Registers an option; all `longs` and `shorts` share the same key.
|
|
92
|
-
* Short names
|
|
93
|
-
* but must not be prefixes of one another.
|
|
104
|
+
* Short names must not be prefixes of one another.
|
|
94
105
|
*
|
|
95
106
|
* @param definition.longs - Long-form names (without `--`).
|
|
96
107
|
* @param definition.shorts - Short-form names (without `-`).
|
|
97
|
-
* @param definition.
|
|
108
|
+
* @param definition.parsing - Parsing behaviour.
|
|
98
109
|
* @returns A {@link ReaderOptionKey} for {@link ReaderArgs.getOptionValues}.
|
|
99
110
|
* @throws `Error` if any name is already registered or short names overlap.
|
|
100
111
|
*/
|
|
101
112
|
registerOption(definition: {
|
|
102
113
|
longs: Array<string>;
|
|
103
114
|
shorts: Array<string>;
|
|
104
|
-
|
|
115
|
+
parsing: ReaderOptionParsing;
|
|
105
116
|
}) {
|
|
106
117
|
const key = [
|
|
107
118
|
...definition.longs.map((long) => `--${long}`),
|
|
@@ -111,7 +122,7 @@ export class ReaderArgs {
|
|
|
111
122
|
if (!this.#isValidOptionName(long)) {
|
|
112
123
|
throw new Error(`Invalid option name: --${long}`);
|
|
113
124
|
}
|
|
114
|
-
if (this.#
|
|
125
|
+
if (this.#optionContextByLong.has(long)) {
|
|
115
126
|
throw new Error(`Option already registered: --${long}`);
|
|
116
127
|
}
|
|
117
128
|
}
|
|
@@ -119,18 +130,18 @@ export class ReaderArgs {
|
|
|
119
130
|
if (!this.#isValidOptionName(short)) {
|
|
120
131
|
throw new Error(`Invalid option name: -${short}`);
|
|
121
132
|
}
|
|
122
|
-
if (this.#
|
|
133
|
+
if (this.#optionContextByShort.has(short)) {
|
|
123
134
|
throw new Error(`Option already registered: -${short}`);
|
|
124
135
|
}
|
|
125
136
|
for (let i = 0; i < short.length; i++) {
|
|
126
137
|
const shortSlice = short.slice(0, i);
|
|
127
|
-
if (this.#
|
|
138
|
+
if (this.#optionContextByShort.has(shortSlice)) {
|
|
128
139
|
throw new Error(
|
|
129
140
|
`Option -${short} overlap with shorter option: -${shortSlice}`,
|
|
130
141
|
);
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
|
-
for (const shortOther of this.#
|
|
144
|
+
for (const shortOther of this.#optionContextByShort.keys()) {
|
|
134
145
|
if (shortOther.startsWith(short)) {
|
|
135
146
|
throw new Error(
|
|
136
147
|
`Option -${short} overlap with longer option: -${shortOther}`,
|
|
@@ -138,35 +149,37 @@ export class ReaderArgs {
|
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
}
|
|
152
|
+
const optionContext = {
|
|
153
|
+
parsing: definition.parsing,
|
|
154
|
+
results: new Array<ReaderOptionValue>(),
|
|
155
|
+
};
|
|
141
156
|
for (const long of definition.longs) {
|
|
142
|
-
this.#
|
|
157
|
+
this.#optionContextByLong.set(long, optionContext);
|
|
143
158
|
}
|
|
144
159
|
for (const short of definition.shorts) {
|
|
145
|
-
this.#
|
|
160
|
+
this.#optionContextByShort.set(short, optionContext);
|
|
146
161
|
}
|
|
147
|
-
this.#
|
|
148
|
-
this.#resultByKey.set(key, new Array<string>());
|
|
162
|
+
this.#optionContextByKey.set(key, optionContext);
|
|
149
163
|
return key;
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
/**
|
|
153
|
-
* Returns all values collected for
|
|
167
|
+
* Returns all values collected for `key`.
|
|
154
168
|
*
|
|
155
169
|
* @param key - Key from {@link ReaderArgs.registerOption}.
|
|
156
|
-
* @returns
|
|
170
|
+
* @returns One entry per occurrence.
|
|
157
171
|
* @throws `Error` if `key` was not registered.
|
|
158
172
|
*/
|
|
159
|
-
getOptionValues(key: ReaderOptionKey): Array<
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
173
|
+
getOptionValues(key: ReaderOptionKey): Array<ReaderOptionValue> {
|
|
174
|
+
const optionContext = this.#optionContextByKey.get(key);
|
|
175
|
+
if (optionContext === undefined) {
|
|
162
176
|
throw new Error(`Unregistered option: ${key}`);
|
|
163
177
|
}
|
|
164
|
-
return
|
|
178
|
+
return optionContext.results;
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
/**
|
|
168
|
-
* Returns the next
|
|
169
|
-
* Parse intervening options as side-effects.
|
|
182
|
+
* Returns the next positional token; parses intervening options as a side-effect.
|
|
170
183
|
* All tokens after `--` are treated as positionals.
|
|
171
184
|
*
|
|
172
185
|
* @returns The next positional, or `undefined` when exhausted.
|
|
@@ -175,19 +188,19 @@ export class ReaderArgs {
|
|
|
175
188
|
consumePositional(): string | undefined {
|
|
176
189
|
while (true) {
|
|
177
190
|
const arg = this.#consumeArg();
|
|
178
|
-
if (arg ===
|
|
191
|
+
if (arg === undefined) {
|
|
179
192
|
return undefined;
|
|
180
193
|
}
|
|
181
|
-
if (this.#
|
|
194
|
+
if (!this.#tryConsumeAsOption(arg)) {
|
|
182
195
|
return arg;
|
|
183
196
|
}
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
#consumeArg(): string |
|
|
200
|
+
#consumeArg(): string | undefined {
|
|
188
201
|
const arg = this.#args[this.#parsedIndex];
|
|
189
202
|
if (arg === undefined) {
|
|
190
|
-
return
|
|
203
|
+
return undefined;
|
|
191
204
|
}
|
|
192
205
|
this.#parsedIndex++;
|
|
193
206
|
if (!this.#parsedDouble) {
|
|
@@ -199,9 +212,9 @@ export class ReaderArgs {
|
|
|
199
212
|
return arg;
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
#
|
|
215
|
+
#tryConsumeAsOption(arg: string): boolean {
|
|
203
216
|
if (this.#parsedDouble) {
|
|
204
|
-
return
|
|
217
|
+
return false;
|
|
205
218
|
}
|
|
206
219
|
if (arg.startsWith("--")) {
|
|
207
220
|
const valueIndexStart = arg.indexOf("=");
|
|
@@ -213,80 +226,90 @@ export class ReaderArgs {
|
|
|
213
226
|
arg.slice(valueIndexStart + 1),
|
|
214
227
|
);
|
|
215
228
|
}
|
|
216
|
-
return
|
|
229
|
+
return true;
|
|
217
230
|
}
|
|
218
231
|
if (arg.startsWith("-")) {
|
|
219
232
|
let shortIndexStart = 1;
|
|
220
233
|
let shortIndexEnd = 2;
|
|
221
234
|
while (shortIndexEnd <= arg.length) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (result === false) {
|
|
235
|
+
const short = arg.slice(shortIndexStart, shortIndexEnd);
|
|
236
|
+
const optionContext = this.#optionContextByShort.get(short);
|
|
237
|
+
if (optionContext !== undefined) {
|
|
238
|
+
const rest = arg.slice(shortIndexEnd);
|
|
239
|
+
if (this.#tryConsumeOptionShort(optionContext, short, rest)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
230
242
|
shortIndexStart = shortIndexEnd;
|
|
231
243
|
}
|
|
232
244
|
shortIndexEnd++;
|
|
233
245
|
}
|
|
234
246
|
throw new TypoError(
|
|
235
247
|
new TypoText(
|
|
236
|
-
new TypoString(
|
|
237
|
-
new TypoString(
|
|
248
|
+
new TypoString(`Unexpected unknown option(s): `),
|
|
249
|
+
new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleQuote),
|
|
238
250
|
),
|
|
239
251
|
);
|
|
240
252
|
}
|
|
241
|
-
return
|
|
253
|
+
return false;
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
#consumeOptionLong(long: string,
|
|
256
|
+
#consumeOptionLong(long: string, inlined: string | null): void {
|
|
245
257
|
const constant = `--${long}`;
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
return this.#acknowledgeOption(key, direct);
|
|
250
|
-
}
|
|
251
|
-
const valued = this.#valuedByKey.get(key);
|
|
252
|
-
if (valued) {
|
|
253
|
-
return this.#acknowledgeOption(key, this.#consumeOptionValue(constant));
|
|
254
|
-
}
|
|
255
|
-
return this.#acknowledgeOption(key, "true");
|
|
258
|
+
const optionContext = this.#optionContextByLong.get(long);
|
|
259
|
+
if (optionContext !== undefined) {
|
|
260
|
+
return this.#consumeOptionValues(optionContext, constant, inlined);
|
|
256
261
|
}
|
|
257
262
|
throw new TypoError(
|
|
258
263
|
new TypoText(
|
|
259
|
-
new TypoString(
|
|
260
|
-
new TypoString(
|
|
264
|
+
new TypoString(`Unexpected unknown option: `),
|
|
265
|
+
new TypoString(constant, typoStyleQuote),
|
|
261
266
|
),
|
|
262
267
|
);
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
#tryConsumeOptionShort(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return rest === "";
|
|
270
|
+
#tryConsumeOptionShort(
|
|
271
|
+
optionContext: ReaderOptionContext,
|
|
272
|
+
short: string,
|
|
273
|
+
rest: string,
|
|
274
|
+
): boolean {
|
|
275
|
+
const constant = `-${short}`;
|
|
276
|
+
if (rest.startsWith("=")) {
|
|
277
|
+
this.#consumeOptionValues(optionContext, constant, rest.slice(1));
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
if (rest.length === 0) {
|
|
281
|
+
this.#consumeOptionValues(optionContext, constant, null);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (optionContext.parsing.consumeShortGroup) {
|
|
285
|
+
this.#consumeOptionValues(optionContext, constant, rest);
|
|
286
|
+
return true;
|
|
283
287
|
}
|
|
284
|
-
|
|
288
|
+
this.#consumeOptionValues(optionContext, constant, null);
|
|
289
|
+
return false;
|
|
285
290
|
}
|
|
286
291
|
|
|
287
|
-
#
|
|
292
|
+
#consumeOptionValues(
|
|
293
|
+
optionContext: ReaderOptionContext,
|
|
294
|
+
constant: string,
|
|
295
|
+
inlined: string | null,
|
|
296
|
+
) {
|
|
297
|
+
const separated = new Array<string>();
|
|
298
|
+
while (
|
|
299
|
+
optionContext.parsing.consumeNextArg(
|
|
300
|
+
inlined,
|
|
301
|
+
separated,
|
|
302
|
+
this.#args[this.#parsedIndex],
|
|
303
|
+
)
|
|
304
|
+
) {
|
|
305
|
+
separated.push(this.#consumeOptionValue(constant));
|
|
306
|
+
}
|
|
307
|
+
optionContext.results.push({ inlined, separated });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#consumeOptionValue(constant: string): string {
|
|
288
311
|
const arg = this.#consumeArg();
|
|
289
|
-
if (arg ===
|
|
312
|
+
if (arg === undefined) {
|
|
290
313
|
throw new TypoError(
|
|
291
314
|
new TypoText(
|
|
292
315
|
new TypoString(constant, typoStyleConstants),
|
|
@@ -316,11 +339,12 @@ export class ReaderArgs {
|
|
|
316
339
|
return arg;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
|
-
#acknowledgeOption(key: ReaderOptionKey, value: string) {
|
|
320
|
-
this.getOptionValues(key).push(value);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
342
|
#isValidOptionName(name: string): boolean {
|
|
324
343
|
return name.length > 0 && !name.includes("=");
|
|
325
344
|
}
|
|
326
345
|
}
|
|
346
|
+
|
|
347
|
+
type ReaderOptionContext = {
|
|
348
|
+
parsing: ReaderOptionParsing;
|
|
349
|
+
results: Array<ReaderOptionValue>;
|
|
350
|
+
};
|
package/src/lib/Run.ts
CHANGED
|
@@ -7,15 +7,16 @@ import { usageToStyledLines } from "./Usage";
|
|
|
7
7
|
* Main entry point: parses CLI arguments, executes the matched command, and exits.
|
|
8
8
|
* Handles `--help`, `--version`, usage-on-error, and exit codes.
|
|
9
9
|
*
|
|
10
|
-
* Exit codes:
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* - `0` on success / `--help` / `--version`
|
|
12
|
+
* - `1` on parse error or execution error.
|
|
11
13
|
*
|
|
12
|
-
* @typeParam Context -
|
|
14
|
+
* @typeParam Context - Forwarded unchanged to the handler.
|
|
13
15
|
*
|
|
14
16
|
* @param cliName - Program name used in usage and `--version` output.
|
|
15
17
|
* @param cliArgs - Raw arguments, typically `process.argv.slice(2)`.
|
|
16
|
-
* @param context - Forwarded to the
|
|
18
|
+
* @param context - Forwarded to the handler.
|
|
17
19
|
* @param command - Root {@link Command}.
|
|
18
|
-
* @param options - Optional runner configuration.
|
|
19
20
|
* @param options.useTtyColors - Color mode: `true` (always), `false` (never),
|
|
20
21
|
* `"mock"` (snapshot-friendly), `undefined` (auto-detect from env).
|
|
21
22
|
* @param options.usageOnHelp - Enables `--help` flag (default `true`).
|
|
@@ -34,7 +35,7 @@ import { usageToStyledLines } from "./Usage";
|
|
|
34
35
|
* { description: "Greet someone" },
|
|
35
36
|
* operation(
|
|
36
37
|
* { options: {}, positionals: [positionalRequired({ type: typeString, label: "NAME" })] },
|
|
37
|
-
* async (_ctx, { positionals: [name] })
|
|
38
|
+
* async function (_ctx, { positionals: [name] }) {
|
|
38
39
|
* console.log(`Hello, ${name}!`);
|
|
39
40
|
* },
|
|
40
41
|
* ),
|
|
@@ -51,7 +52,7 @@ export async function runAndExit<Context>(
|
|
|
51
52
|
context: Context,
|
|
52
53
|
command: Command<Context, void>,
|
|
53
54
|
options?: {
|
|
54
|
-
useTtyColors?: boolean | undefined | "mock";
|
|
55
|
+
useTtyColors?: boolean | undefined | "mock"; // TODO - flag setter option
|
|
55
56
|
usageOnHelp?: boolean | undefined;
|
|
56
57
|
usageOnError?: boolean | undefined;
|
|
57
58
|
buildVersion?: string | undefined;
|
|
@@ -65,7 +66,10 @@ export async function runAndExit<Context>(
|
|
|
65
66
|
readerArgs.registerOption({
|
|
66
67
|
shorts: [],
|
|
67
68
|
longs: ["help"],
|
|
68
|
-
|
|
69
|
+
parsing: {
|
|
70
|
+
consumeShortGroup: false,
|
|
71
|
+
consumeNextArg: () => false,
|
|
72
|
+
},
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
const buildVersion = options?.buildVersion;
|
|
@@ -73,7 +77,10 @@ export async function runAndExit<Context>(
|
|
|
73
77
|
readerArgs.registerOption({
|
|
74
78
|
shorts: [],
|
|
75
79
|
longs: ["version"],
|
|
76
|
-
|
|
80
|
+
parsing: {
|
|
81
|
+
consumeShortGroup: false,
|
|
82
|
+
consumeNextArg: () => false,
|
|
83
|
+
},
|
|
77
84
|
});
|
|
78
85
|
}
|
|
79
86
|
/*
|
|
@@ -84,6 +91,7 @@ export async function runAndExit<Context>(
|
|
|
84
91
|
longs: ["completion"],
|
|
85
92
|
});
|
|
86
93
|
*/
|
|
94
|
+
// TODO - handle color flag ?
|
|
87
95
|
const commandDecoder = command.consumeAndMakeDecoder(readerArgs);
|
|
88
96
|
while (true) {
|
|
89
97
|
try {
|
|
@@ -93,15 +101,8 @@ export async function runAndExit<Context>(
|
|
|
93
101
|
}
|
|
94
102
|
} catch (_) {}
|
|
95
103
|
}
|
|
104
|
+
const typoSupport = computeTypoSupport(options?.useTtyColors);
|
|
96
105
|
const onExit = options?.onExit ?? process.exit;
|
|
97
|
-
const typoSupport =
|
|
98
|
-
options?.useTtyColors === undefined
|
|
99
|
-
? TypoSupport.inferFromProcess()
|
|
100
|
-
: options.useTtyColors === "mock"
|
|
101
|
-
? TypoSupport.mock()
|
|
102
|
-
: options.useTtyColors
|
|
103
|
-
? TypoSupport.tty()
|
|
104
|
-
: TypoSupport.none();
|
|
105
106
|
if (usageOnHelp) {
|
|
106
107
|
if (readerArgs.getOptionValues("--help" as any).length > 0) {
|
|
107
108
|
console.log(computeUsageString(cliName, commandDecoder, typoSupport));
|
|
@@ -151,7 +152,19 @@ function computeUsageString<Context, Result>(
|
|
|
151
152
|
) {
|
|
152
153
|
return usageToStyledLines({
|
|
153
154
|
cliName,
|
|
154
|
-
|
|
155
|
+
usage: commandDecoder.generateUsage(),
|
|
155
156
|
typoSupport,
|
|
156
157
|
}).join("\n");
|
|
157
158
|
}
|
|
159
|
+
|
|
160
|
+
function computeTypoSupport(
|
|
161
|
+
useTtyColors: boolean | undefined | "mock",
|
|
162
|
+
): TypoSupport {
|
|
163
|
+
return useTtyColors === undefined
|
|
164
|
+
? TypoSupport.inferFromProcess()
|
|
165
|
+
: useTtyColors === "mock"
|
|
166
|
+
? TypoSupport.mock()
|
|
167
|
+
: useTtyColors
|
|
168
|
+
? TypoSupport.tty()
|
|
169
|
+
: TypoSupport.none();
|
|
170
|
+
}
|
package/src/lib/Type.ts
CHANGED
|
@@ -18,11 +18,11 @@ import {
|
|
|
18
18
|
*/
|
|
19
19
|
export type Type<Value> = {
|
|
20
20
|
/**
|
|
21
|
-
* Human-readable name shown in help and
|
|
21
|
+
* Human-readable name shown in help and errors (e.g. `"String"`, `"Number"`).
|
|
22
22
|
*/
|
|
23
23
|
content: string;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Decodes a raw CLI string into `Value`.
|
|
26
26
|
*
|
|
27
27
|
* @param input - Raw string from the command line.
|
|
28
28
|
* @returns The decoded value.
|
|
@@ -32,24 +32,27 @@ export type Type<Value> = {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Decodes
|
|
36
|
-
* Used
|
|
35
|
+
* Decodes a string to `boolean` (case-insensitive).
|
|
36
|
+
* Used by {@link optionFlag} for `--flag=<value>`.
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```ts
|
|
40
|
+
* typeBoolean.decoder("true") // → true
|
|
40
41
|
* typeBoolean.decoder("yes") // → true
|
|
42
|
+
* typeBoolean.decoder("y") // → true
|
|
41
43
|
* typeBoolean.decoder("false") // → false
|
|
42
|
-
* typeBoolean.decoder("
|
|
44
|
+
* typeBoolean.decoder("no") // → false
|
|
45
|
+
* typeBoolean.decoder("n") // → false
|
|
43
46
|
* ```
|
|
44
47
|
*/
|
|
45
48
|
export const typeBoolean: Type<boolean> = {
|
|
46
49
|
content: "Boolean",
|
|
47
50
|
decoder(input: string) {
|
|
48
51
|
const lower = input.toLowerCase();
|
|
49
|
-
if (lower
|
|
52
|
+
if (booleanValuesTrue.has(lower)) {
|
|
50
53
|
return true;
|
|
51
54
|
}
|
|
52
|
-
if (lower
|
|
55
|
+
if (booleanValuesFalse.has(lower)) {
|
|
53
56
|
return false;
|
|
54
57
|
}
|
|
55
58
|
throw new TypoError(
|
|
@@ -60,9 +63,11 @@ export const typeBoolean: Type<boolean> = {
|
|
|
60
63
|
);
|
|
61
64
|
},
|
|
62
65
|
};
|
|
66
|
+
const booleanValuesTrue = new Set(["true", "yes", "on", "1", "y", "t"]);
|
|
67
|
+
const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
|
|
63
68
|
|
|
64
69
|
/**
|
|
65
|
-
* Parses a date/time string via `Date.parse
|
|
70
|
+
* Parses a date/time string via `Date.parse`.
|
|
66
71
|
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
67
72
|
*
|
|
68
73
|
* @example
|
|
@@ -93,8 +98,7 @@ export const typeDate: Type<Date> = {
|
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
/**
|
|
96
|
-
* Parses a string
|
|
97
|
-
* Accepts integers, floats, and scientific notation; `NaN` throws a {@link TypoError}.
|
|
101
|
+
* Parses a string to `number` via `Number()`; `NaN` throws {@link TypoError}.
|
|
98
102
|
*
|
|
99
103
|
* @example
|
|
100
104
|
* ```ts
|
|
@@ -124,8 +128,8 @@ export const typeNumber: Type<number> = {
|
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
|
-
* Parses an integer string
|
|
128
|
-
* Floats and non-numeric strings throw
|
|
131
|
+
* Parses an integer string to `bigint` via `BigInt()`.
|
|
132
|
+
* Floats and non-numeric strings throw {@link TypoError}.
|
|
129
133
|
*
|
|
130
134
|
* @example
|
|
131
135
|
* ```ts
|
|
@@ -151,8 +155,8 @@ export const typeInteger: Type<bigint> = {
|
|
|
151
155
|
};
|
|
152
156
|
|
|
153
157
|
/**
|
|
154
|
-
* Parses an absolute URL string
|
|
155
|
-
* Relative or malformed URLs throw
|
|
158
|
+
* Parses an absolute URL string to a `URL` object.
|
|
159
|
+
* Relative or malformed URLs throw {@link TypoError}.
|
|
156
160
|
*
|
|
157
161
|
* @example
|
|
158
162
|
* ```ts
|
|
@@ -193,7 +197,7 @@ export const typeString: Type<string> = {
|
|
|
193
197
|
};
|
|
194
198
|
|
|
195
199
|
/**
|
|
196
|
-
*
|
|
200
|
+
* Chains `before`'s decoder with an `after` transformation.
|
|
197
201
|
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
198
202
|
*
|
|
199
203
|
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
@@ -240,8 +244,8 @@ export function typeMapped<Before, After>(
|
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/**
|
|
243
|
-
* Creates a {@link Type}`<string>`
|
|
244
|
-
* Out-of-set inputs throw
|
|
247
|
+
* Creates a {@link Type}`<string>` that only accepts a fixed set of values.
|
|
248
|
+
* Out-of-set inputs throw {@link TypoError} listing up to 5 valid options.
|
|
245
249
|
*
|
|
246
250
|
* @param content - Name shown in help and errors (e.g. `"Environment"`).
|
|
247
251
|
* @param values - Ordered list of accepted values.
|
|
@@ -291,7 +295,7 @@ export function typeOneOf<const Value extends string>(
|
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
/**
|
|
294
|
-
* Splits a delimited string into a
|
|
298
|
+
* Splits a delimited string into a typed tuple.
|
|
295
299
|
* Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
|
|
296
300
|
*
|
|
297
301
|
* @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
|
|
@@ -316,14 +320,14 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
316
320
|
content: elementTypes
|
|
317
321
|
.map((elementType) => elementType.content)
|
|
318
322
|
.join(separator),
|
|
319
|
-
decoder(
|
|
320
|
-
const splits =
|
|
323
|
+
decoder(input: string) {
|
|
324
|
+
const splits = input.split(separator, elementTypes.length);
|
|
321
325
|
if (splits.length !== elementTypes.length) {
|
|
322
326
|
throw new TypoError(
|
|
323
327
|
new TypoText(
|
|
324
328
|
new TypoString(`Found ${splits.length} splits: `),
|
|
325
329
|
new TypoString(`Expected ${elementTypes.length} splits from: `),
|
|
326
|
-
new TypoString(`"${
|
|
330
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
327
331
|
),
|
|
328
332
|
);
|
|
329
333
|
}
|
|
@@ -343,7 +347,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
/**
|
|
346
|
-
* Splits a delimited string into a
|
|
350
|
+
* Splits a delimited string into a typed array.
|
|
347
351
|
* Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
|
|
348
352
|
* Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
|
|
349
353
|
*
|