cli-kiss 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +711 -1046
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/.vitepress/theme/style.css +4 -0
- package/docs/guide/01_getting_started.md +4 -4
- package/docs/guide/02_commands.md +72 -41
- package/docs/guide/03_options.md +13 -14
- package/docs/guide/04_positionals.md +6 -8
- package/docs/guide/05_types.md +9 -11
- package/docs/guide/06_run.md +4 -5
- package/docs/index.md +8 -3
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/lib/Command.ts +151 -275
- package/src/lib/Operation.ts +57 -95
- package/src/lib/Option.ts +194 -181
- package/src/lib/Positional.ts +54 -112
- package/src/lib/Reader.ts +155 -156
- package/src/lib/Run.ts +64 -69
- package/src/lib/Type.ts +89 -145
- package/src/lib/Typo.ts +131 -195
- package/src/lib/Usage.ts +203 -69
- 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 +146 -91
- package/tests/unit.command.usage.ts +235 -114
- package/tests/unit.runner.cycle.ts +50 -20
- package/tests/unit.runner.errors.ts +19 -3
package/src/lib/Run.ts
CHANGED
|
@@ -1,56 +1,31 @@
|
|
|
1
|
-
import { Command,
|
|
1
|
+
import { Command, CommandDecoder } from "./Command";
|
|
2
2
|
import { ReaderArgs } from "./Reader";
|
|
3
3
|
import { TypoSupport } from "./Typo";
|
|
4
4
|
import { usageToStyledLines } from "./Usage";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Main entry point: parses CLI arguments, executes the matched command, and exits.
|
|
8
|
+
* Handles `--help`, `--version`, usage-on-error, and exit codes.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* - `0` on success / `--help` / `--version`
|
|
12
|
+
* - `1` on parse error or execution error.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* - `0` — Command executed successfully, or `--help` / `--version` was handled.
|
|
16
|
-
* - `1` — Argument parsing failed (a usage summary is also printed to stderr), or the
|
|
17
|
-
* command threw an unhandled execution error.
|
|
14
|
+
* @typeParam Context - Forwarded unchanged to the handler.
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* @param cliName - Program name used in usage and `--version` output.
|
|
17
|
+
* @param cliArgs - Raw arguments, typically `process.argv.slice(2)`.
|
|
18
|
+
* @param context - Forwarded to the handler.
|
|
19
|
+
* @param command - Root {@link Command}.
|
|
20
|
+
* @param options.useTtyColors - Color mode: `true` (always), `false` (never),
|
|
21
|
+
* `"mock"` (snapshot-friendly), `undefined` (auto-detect from env).
|
|
22
|
+
* @param options.usageOnHelp - Enables `--help` flag (default `true`).
|
|
23
|
+
* @param options.usageOnError - Prints usage to stderr on parse error (default `true`).
|
|
24
|
+
* @param options.buildVersion - Enables `--version`; prints `<cliName> <buildVersion>`.
|
|
25
|
+
* @param options.onError - Custom handler for errors.
|
|
26
|
+
* @param options.onExit - Overrides `process.exit`; useful for testing.
|
|
24
27
|
*
|
|
25
|
-
* @
|
|
26
|
-
* Use this to inject dependencies (e.g. a database connection, a logger) into your commands.
|
|
27
|
-
*
|
|
28
|
-
* @param cliName - The name of the CLI program (e.g. `"my-cli"`). Used in the usage
|
|
29
|
-
* summary header and in the `--version` output.
|
|
30
|
-
* @param cliArgs - The raw command-line arguments to parse, typically `process.argv.slice(2)`.
|
|
31
|
-
* @param context - The context value forwarded to the command's execution handler.
|
|
32
|
-
* @param command - The root {@link Command} that describes how to parse and execute
|
|
33
|
-
* the CLI.
|
|
34
|
-
* @param options - Optional configuration for the runner.
|
|
35
|
-
* @param options.useTtyColors - Controls terminal color output in styled messages.
|
|
36
|
-
* - `true` — Always apply ANSI color codes.
|
|
37
|
-
* - `false` — Never apply color codes (plain text).
|
|
38
|
-
* - `"mock"` — Use a deterministic mock style useful for snapshot testing.
|
|
39
|
-
* - `undefined` (default) — Auto-detect based on `process.stdout.isTTY` and the
|
|
40
|
-
* `FORCE_COLOR` / `NO_COLOR` environment variables.
|
|
41
|
-
* @param options.usageOnHelp - When `true` (default), registers a `--help` flag that
|
|
42
|
-
* prints the usage summary and exits with code `0`.
|
|
43
|
-
* @param options.usageOnError - When `true` (default), prints the usage summary to
|
|
44
|
-
* stderr before the error message whenever argument parsing fails.
|
|
45
|
-
* @param options.buildVersion - When provided, registers a `--version` flag that prints
|
|
46
|
-
* `<cliName> <buildVersion>` to stdout and exits with code `0`.
|
|
47
|
-
* @param options.onError - Custom handler for errors thrown during command execution.
|
|
48
|
-
* If omitted, the error is printed to stderr via {@link TypoSupport}.
|
|
49
|
-
* @param options.onExit - Overrides the process exit function (default: `process.exit`).
|
|
50
|
-
* Useful for testing — supply a function that throws or captures the exit code instead
|
|
51
|
-
* of actually terminating the process.
|
|
52
|
-
*
|
|
53
|
-
* @returns A `Promise<never>` because the function always terminates by calling `onExit`.
|
|
28
|
+
* @returns `Promise<never>` — always calls `onExit`.
|
|
54
29
|
*
|
|
55
30
|
* @example
|
|
56
31
|
* ```ts
|
|
@@ -60,7 +35,7 @@ import { usageToStyledLines } from "./Usage";
|
|
|
60
35
|
* { description: "Greet someone" },
|
|
61
36
|
* operation(
|
|
62
37
|
* { options: {}, positionals: [positionalRequired({ type: typeString, label: "NAME" })] },
|
|
63
|
-
* async (_ctx, { positionals: [name] })
|
|
38
|
+
* async function (_ctx, { positionals: [name] }) {
|
|
64
39
|
* console.log(`Hello, ${name}!`);
|
|
65
40
|
* },
|
|
66
41
|
* ),
|
|
@@ -77,7 +52,7 @@ export async function runAndExit<Context>(
|
|
|
77
52
|
context: Context,
|
|
78
53
|
command: Command<Context, void>,
|
|
79
54
|
options?: {
|
|
80
|
-
useTtyColors?: boolean | undefined | "mock";
|
|
55
|
+
useTtyColors?: boolean | undefined | "mock"; // TODO - flag setter option
|
|
81
56
|
usageOnHelp?: boolean | undefined;
|
|
82
57
|
usageOnError?: boolean | undefined;
|
|
83
58
|
buildVersion?: string | undefined;
|
|
@@ -91,7 +66,10 @@ export async function runAndExit<Context>(
|
|
|
91
66
|
readerArgs.registerOption({
|
|
92
67
|
shorts: [],
|
|
93
68
|
longs: ["help"],
|
|
94
|
-
|
|
69
|
+
parsing: {
|
|
70
|
+
consumeShortGroup: false,
|
|
71
|
+
consumeNextArg: () => false,
|
|
72
|
+
},
|
|
95
73
|
});
|
|
96
74
|
}
|
|
97
75
|
const buildVersion = options?.buildVersion;
|
|
@@ -99,7 +77,10 @@ export async function runAndExit<Context>(
|
|
|
99
77
|
readerArgs.registerOption({
|
|
100
78
|
shorts: [],
|
|
101
79
|
longs: ["version"],
|
|
102
|
-
|
|
80
|
+
parsing: {
|
|
81
|
+
consumeShortGroup: false,
|
|
82
|
+
consumeNextArg: () => false,
|
|
83
|
+
},
|
|
103
84
|
});
|
|
104
85
|
}
|
|
105
86
|
/*
|
|
@@ -110,7 +91,8 @@ export async function runAndExit<Context>(
|
|
|
110
91
|
longs: ["completion"],
|
|
111
92
|
});
|
|
112
93
|
*/
|
|
113
|
-
|
|
94
|
+
// TODO - handle color flag ?
|
|
95
|
+
const commandDecoder = command.consumeAndMakeDecoder(readerArgs);
|
|
114
96
|
while (true) {
|
|
115
97
|
try {
|
|
116
98
|
const positional = readerArgs.consumePositional();
|
|
@@ -119,18 +101,11 @@ export async function runAndExit<Context>(
|
|
|
119
101
|
}
|
|
120
102
|
} catch (_) {}
|
|
121
103
|
}
|
|
104
|
+
const typoSupport = computeTypoSupport(options?.useTtyColors);
|
|
122
105
|
const onExit = options?.onExit ?? process.exit;
|
|
123
|
-
const typoSupport =
|
|
124
|
-
options?.useTtyColors === undefined
|
|
125
|
-
? TypoSupport.inferFromProcess()
|
|
126
|
-
: options.useTtyColors === "mock"
|
|
127
|
-
? TypoSupport.mock()
|
|
128
|
-
: options.useTtyColors
|
|
129
|
-
? TypoSupport.tty()
|
|
130
|
-
: TypoSupport.none();
|
|
131
106
|
if (usageOnHelp) {
|
|
132
107
|
if (readerArgs.getOptionValues("--help" as any).length > 0) {
|
|
133
|
-
console.log(computeUsageString(cliName,
|
|
108
|
+
console.log(computeUsageString(cliName, commandDecoder, typoSupport));
|
|
134
109
|
return onExit(0);
|
|
135
110
|
}
|
|
136
111
|
}
|
|
@@ -141,35 +116,55 @@ export async function runAndExit<Context>(
|
|
|
141
116
|
}
|
|
142
117
|
}
|
|
143
118
|
try {
|
|
144
|
-
const
|
|
119
|
+
const commandInterpreter = commandDecoder.decodeAndMakeInterpreter();
|
|
145
120
|
try {
|
|
146
|
-
await
|
|
121
|
+
await commandInterpreter.executeWithContext(context);
|
|
147
122
|
return onExit(0);
|
|
148
123
|
} catch (executionError) {
|
|
149
|
-
|
|
150
|
-
options.onError(executionError);
|
|
151
|
-
} else {
|
|
152
|
-
console.error(typoSupport.computeStyledErrorMessage(executionError));
|
|
153
|
-
}
|
|
124
|
+
handleError(options?.onError, executionError, typoSupport);
|
|
154
125
|
return onExit(1);
|
|
155
126
|
}
|
|
156
127
|
} catch (parsingError) {
|
|
157
128
|
if (options?.usageOnError ?? true) {
|
|
158
|
-
console.error(computeUsageString(cliName,
|
|
129
|
+
console.error(computeUsageString(cliName, commandDecoder, typoSupport));
|
|
159
130
|
}
|
|
160
|
-
|
|
131
|
+
handleError(options?.onError, parsingError, typoSupport);
|
|
161
132
|
return onExit(1);
|
|
162
133
|
}
|
|
163
134
|
}
|
|
164
135
|
|
|
136
|
+
function handleError(
|
|
137
|
+
onError: ((error: unknown) => void) | undefined,
|
|
138
|
+
error: unknown,
|
|
139
|
+
typoSupport: TypoSupport,
|
|
140
|
+
) {
|
|
141
|
+
if (onError !== undefined) {
|
|
142
|
+
onError(error);
|
|
143
|
+
} else {
|
|
144
|
+
console.error(typoSupport.computeStyledErrorMessage(error));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
165
148
|
function computeUsageString<Context, Result>(
|
|
166
149
|
cliName: Lowercase<string>,
|
|
167
|
-
|
|
150
|
+
commandDecoder: CommandDecoder<Context, Result>,
|
|
168
151
|
typoSupport: TypoSupport,
|
|
169
152
|
) {
|
|
170
153
|
return usageToStyledLines({
|
|
171
154
|
cliName,
|
|
172
|
-
|
|
155
|
+
usage: commandDecoder.generateUsage(),
|
|
173
156
|
typoSupport,
|
|
174
157
|
}).join("\n");
|
|
175
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
|
@@ -7,79 +7,68 @@ 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 errors (e.g. `"String"`, `"Number"`).
|
|
30
22
|
*/
|
|
31
23
|
content: string;
|
|
32
24
|
/**
|
|
33
|
-
* Decodes a raw string
|
|
25
|
+
* Decodes 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 a string to `boolean` (case-insensitive).
|
|
36
|
+
* Used by {@link optionFlag} for `--flag=<value>`.
|
|
48
37
|
*
|
|
49
38
|
* @example
|
|
50
39
|
* ```ts
|
|
40
|
+
* typeBoolean.decoder("true") // → true
|
|
51
41
|
* typeBoolean.decoder("yes") // → true
|
|
42
|
+
* typeBoolean.decoder("y") // → true
|
|
52
43
|
* typeBoolean.decoder("false") // → false
|
|
53
|
-
* typeBoolean.decoder("
|
|
44
|
+
* typeBoolean.decoder("no") // → false
|
|
45
|
+
* typeBoolean.decoder("n") // → false
|
|
54
46
|
* ```
|
|
55
47
|
*/
|
|
56
48
|
export const typeBoolean: Type<boolean> = {
|
|
57
49
|
content: "Boolean",
|
|
58
|
-
decoder(
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
50
|
+
decoder(input: string) {
|
|
51
|
+
const lower = input.toLowerCase();
|
|
52
|
+
if (booleanValuesTrue.has(lower)) {
|
|
61
53
|
return true;
|
|
62
54
|
}
|
|
63
|
-
if (
|
|
55
|
+
if (booleanValuesFalse.has(lower)) {
|
|
64
56
|
return false;
|
|
65
57
|
}
|
|
66
58
|
throw new TypoError(
|
|
67
59
|
new TypoText(
|
|
68
60
|
new TypoString(`Invalid value: `),
|
|
69
|
-
new TypoString(`"${
|
|
61
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
70
62
|
),
|
|
71
63
|
);
|
|
72
64
|
},
|
|
73
65
|
};
|
|
66
|
+
const booleanValuesTrue = new Set(["true", "yes", "on", "1", "y", "t"]);
|
|
67
|
+
const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
|
|
74
68
|
|
|
75
69
|
/**
|
|
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))`.
|
|
70
|
+
* Parses a date/time string via `Date.parse`.
|
|
71
|
+
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
83
72
|
*
|
|
84
73
|
* @example
|
|
85
74
|
* ```ts
|
|
@@ -90,9 +79,9 @@ export const typeBoolean: Type<boolean> = {
|
|
|
90
79
|
*/
|
|
91
80
|
export const typeDate: Type<Date> = {
|
|
92
81
|
content: "Date",
|
|
93
|
-
decoder(
|
|
82
|
+
decoder(input: string) {
|
|
94
83
|
try {
|
|
95
|
-
const timestampMs = Date.parse(
|
|
84
|
+
const timestampMs = Date.parse(input);
|
|
96
85
|
if (isNaN(timestampMs)) {
|
|
97
86
|
throw new Error();
|
|
98
87
|
}
|
|
@@ -101,7 +90,7 @@ export const typeDate: Type<Date> = {
|
|
|
101
90
|
throw new TypoError(
|
|
102
91
|
new TypoText(
|
|
103
92
|
new TypoString(`Not a valid ISO_8601: `),
|
|
104
|
-
new TypoString(`"${
|
|
93
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
105
94
|
),
|
|
106
95
|
);
|
|
107
96
|
}
|
|
@@ -109,11 +98,7 @@ export const typeDate: Type<Date> = {
|
|
|
109
98
|
};
|
|
110
99
|
|
|
111
100
|
/**
|
|
112
|
-
*
|
|
113
|
-
* constructor.
|
|
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}.
|
|
101
|
+
* Parses a string to `number` via `Number()`; `NaN` throws {@link TypoError}.
|
|
117
102
|
*
|
|
118
103
|
* @example
|
|
119
104
|
* ```ts
|
|
@@ -124,9 +109,9 @@ export const typeDate: Type<Date> = {
|
|
|
124
109
|
*/
|
|
125
110
|
export const typeNumber: Type<number> = {
|
|
126
111
|
content: "Number",
|
|
127
|
-
decoder(
|
|
112
|
+
decoder(input: string) {
|
|
128
113
|
try {
|
|
129
|
-
const parsed = Number(
|
|
114
|
+
const parsed = Number(input);
|
|
130
115
|
if (isNaN(parsed)) {
|
|
131
116
|
throw new Error();
|
|
132
117
|
}
|
|
@@ -135,7 +120,7 @@ export const typeNumber: Type<number> = {
|
|
|
135
120
|
throw new TypoError(
|
|
136
121
|
new TypoText(
|
|
137
122
|
new TypoString(`Unable to parse: `),
|
|
138
|
-
new TypoString(`"${
|
|
123
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
139
124
|
),
|
|
140
125
|
);
|
|
141
126
|
}
|
|
@@ -143,11 +128,8 @@ export const typeNumber: Type<number> = {
|
|
|
143
128
|
};
|
|
144
129
|
|
|
145
130
|
/**
|
|
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}.
|
|
131
|
+
* Parses an integer string to `bigint` via `BigInt()`.
|
|
132
|
+
* Floats and non-numeric strings throw {@link TypoError}.
|
|
151
133
|
*
|
|
152
134
|
* @example
|
|
153
135
|
* ```ts
|
|
@@ -158,14 +140,14 @@ export const typeNumber: Type<number> = {
|
|
|
158
140
|
*/
|
|
159
141
|
export const typeInteger: Type<bigint> = {
|
|
160
142
|
content: "Integer",
|
|
161
|
-
decoder(
|
|
143
|
+
decoder(input: string) {
|
|
162
144
|
try {
|
|
163
|
-
return BigInt(
|
|
145
|
+
return BigInt(input);
|
|
164
146
|
} catch {
|
|
165
147
|
throw new TypoError(
|
|
166
148
|
new TypoText(
|
|
167
149
|
new TypoString(`Unable to parse: `),
|
|
168
|
-
new TypoString(`"${
|
|
150
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
169
151
|
),
|
|
170
152
|
);
|
|
171
153
|
}
|
|
@@ -173,10 +155,8 @@ export const typeInteger: Type<bigint> = {
|
|
|
173
155
|
};
|
|
174
156
|
|
|
175
157
|
/**
|
|
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}.
|
|
158
|
+
* Parses an absolute URL string to a `URL` object.
|
|
159
|
+
* Relative or malformed URLs throw {@link TypoError}.
|
|
180
160
|
*
|
|
181
161
|
* @example
|
|
182
162
|
* ```ts
|
|
@@ -186,14 +166,14 @@ export const typeInteger: Type<bigint> = {
|
|
|
186
166
|
*/
|
|
187
167
|
export const typeUrl: Type<URL> = {
|
|
188
168
|
content: "Url",
|
|
189
|
-
decoder(
|
|
169
|
+
decoder(input: string) {
|
|
190
170
|
try {
|
|
191
|
-
return new URL(
|
|
171
|
+
return new URL(input);
|
|
192
172
|
} catch {
|
|
193
173
|
throw new TypoError(
|
|
194
174
|
new TypoText(
|
|
195
175
|
new TypoString(`Unable to parse: `),
|
|
196
|
-
new TypoString(`"${
|
|
176
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
197
177
|
),
|
|
198
178
|
);
|
|
199
179
|
}
|
|
@@ -201,9 +181,7 @@ export const typeUrl: Type<URL> = {
|
|
|
201
181
|
};
|
|
202
182
|
|
|
203
183
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* This is the simplest type and accepts any string value without validation.
|
|
184
|
+
* Identity decoder — passes the raw string through unchanged.
|
|
207
185
|
*
|
|
208
186
|
* @example
|
|
209
187
|
* ```ts
|
|
@@ -213,35 +191,27 @@ export const typeUrl: Type<URL> = {
|
|
|
213
191
|
*/
|
|
214
192
|
export const typeString: Type<string> = {
|
|
215
193
|
content: "String",
|
|
216
|
-
decoder(
|
|
217
|
-
return
|
|
194
|
+
decoder(input: string) {
|
|
195
|
+
return input;
|
|
218
196
|
},
|
|
219
197
|
};
|
|
220
198
|
|
|
221
199
|
/**
|
|
222
|
-
*
|
|
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.
|
|
200
|
+
* Chains `before`'s decoder with an `after` transformation.
|
|
201
|
+
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
228
202
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* string-keyed enum into a number).
|
|
203
|
+
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
204
|
+
* @typeParam After - Final type from `after.decoder`.
|
|
232
205
|
*
|
|
233
|
-
* @
|
|
234
|
-
* @
|
|
235
|
-
*
|
|
236
|
-
* @param
|
|
237
|
-
* @
|
|
238
|
-
* @param after.content - Human-readable name for the resulting type (shown in errors).
|
|
239
|
-
* @param after.decoder - Function that converts a `Before` value into `After`.
|
|
240
|
-
* @returns A new {@link Type}`<After>` whose `content` is `after.content`.
|
|
206
|
+
* @param before - Base decoder for the raw string.
|
|
207
|
+
* @param after - Transformation applied to the decoded value.
|
|
208
|
+
* @param after.content - Name for the resulting type (shown in errors).
|
|
209
|
+
* @param after.decoder - Converts a `Before` value to `After`.
|
|
210
|
+
* @returns A {@link Type}`<After>`.
|
|
241
211
|
*
|
|
242
212
|
* @example
|
|
243
213
|
* ```ts
|
|
244
|
-
* const typePort =
|
|
214
|
+
* const typePort = typeMapped(typeNumber, {
|
|
245
215
|
* content: "Port",
|
|
246
216
|
* decoder: (n) => {
|
|
247
217
|
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
@@ -252,16 +222,16 @@ export const typeString: Type<string> = {
|
|
|
252
222
|
* // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
|
|
253
223
|
* ```
|
|
254
224
|
*/
|
|
255
|
-
export function
|
|
225
|
+
export function typeMapped<Before, After>(
|
|
256
226
|
before: Type<Before>,
|
|
257
227
|
after: { content: string; decoder: (value: Before) => After },
|
|
258
228
|
): Type<After> {
|
|
259
229
|
return {
|
|
260
230
|
content: after.content,
|
|
261
|
-
decoder: (
|
|
231
|
+
decoder: (input: string) => {
|
|
262
232
|
return after.decoder(
|
|
263
233
|
TypoError.tryWithContext(
|
|
264
|
-
() => before.decoder(
|
|
234
|
+
() => before.decoder(input),
|
|
265
235
|
() =>
|
|
266
236
|
new TypoText(
|
|
267
237
|
new TypoString("from: "),
|
|
@@ -274,18 +244,12 @@ export function typeConverted<Before, After>(
|
|
|
274
244
|
}
|
|
275
245
|
|
|
276
246
|
/**
|
|
277
|
-
* Creates a {@link Type}`<string>` that only accepts a fixed set of
|
|
278
|
-
*
|
|
279
|
-
* The decoder performs an exact (case-sensitive) lookup in `values`. If the input is
|
|
280
|
-
* not in the set, a {@link TypoError} is thrown listing up to 5 of the valid options.
|
|
281
|
-
*
|
|
282
|
-
* Combine with {@link typeConverted} to map the accepted strings to a richer type.
|
|
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.
|
|
283
249
|
*
|
|
284
|
-
* @param content -
|
|
285
|
-
*
|
|
286
|
-
* @
|
|
287
|
-
* the error message preview.
|
|
288
|
-
* @returns A {@link Type}`<string>` that validates membership in `values`.
|
|
250
|
+
* @param content - Name shown in help and errors (e.g. `"Environment"`).
|
|
251
|
+
* @param values - Ordered list of accepted values.
|
|
252
|
+
* @returns A {@link Type}`<string>`.
|
|
289
253
|
*
|
|
290
254
|
* @example
|
|
291
255
|
* ```ts
|
|
@@ -294,16 +258,17 @@ export function typeConverted<Before, After>(
|
|
|
294
258
|
* typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
|
|
295
259
|
* ```
|
|
296
260
|
*/
|
|
297
|
-
export function typeOneOf(
|
|
261
|
+
export function typeOneOf<const Value extends string>(
|
|
298
262
|
content: string,
|
|
299
|
-
values: Array<
|
|
300
|
-
): Type<
|
|
301
|
-
const valuesSet = new Set(values);
|
|
263
|
+
values: Array<Value>,
|
|
264
|
+
): Type<Value> {
|
|
302
265
|
return {
|
|
303
266
|
content: content,
|
|
304
|
-
decoder(
|
|
305
|
-
|
|
306
|
-
|
|
267
|
+
decoder(input: string) {
|
|
268
|
+
for (const value of values) {
|
|
269
|
+
if (input === value) {
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
307
272
|
}
|
|
308
273
|
const valuesPreview = [];
|
|
309
274
|
for (const value of values) {
|
|
@@ -319,7 +284,7 @@ export function typeOneOf(
|
|
|
319
284
|
throw new TypoError(
|
|
320
285
|
new TypoText(
|
|
321
286
|
new TypoString(`Invalid value: `),
|
|
322
|
-
new TypoString(`"${
|
|
287
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
323
288
|
new TypoString(` (expected one of: `),
|
|
324
289
|
...valuesPreview,
|
|
325
290
|
new TypoString(`)`),
|
|
@@ -330,23 +295,14 @@ export function typeOneOf(
|
|
|
330
295
|
}
|
|
331
296
|
|
|
332
297
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
298
|
+
* Splits a delimited string into a typed tuple.
|
|
299
|
+
* Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
|
|
335
300
|
*
|
|
336
|
-
*
|
|
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.
|
|
301
|
+
* @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
|
|
340
302
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* @typeParam Elements - The tuple type of decoded element values (inferred from
|
|
345
|
-
* `elementTypes`).
|
|
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.
|
|
303
|
+
* @param elementTypes - One {@link Type} per tuple element, in order.
|
|
304
|
+
* @param separator - Delimiter (default `","`).
|
|
305
|
+
* @returns A {@link Type}`<Elements>`.
|
|
350
306
|
*
|
|
351
307
|
* @example
|
|
352
308
|
* ```ts
|
|
@@ -364,14 +320,14 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
364
320
|
content: elementTypes
|
|
365
321
|
.map((elementType) => elementType.content)
|
|
366
322
|
.join(separator),
|
|
367
|
-
decoder(
|
|
368
|
-
const splits =
|
|
323
|
+
decoder(input: string) {
|
|
324
|
+
const splits = input.split(separator, elementTypes.length);
|
|
369
325
|
if (splits.length !== elementTypes.length) {
|
|
370
326
|
throw new TypoError(
|
|
371
327
|
new TypoText(
|
|
372
328
|
new TypoString(`Found ${splits.length} splits: `),
|
|
373
329
|
new TypoString(`Expected ${elementTypes.length} splits from: `),
|
|
374
|
-
new TypoString(`"${
|
|
330
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
375
331
|
),
|
|
376
332
|
);
|
|
377
333
|
}
|
|
@@ -391,26 +347,14 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
391
347
|
}
|
|
392
348
|
|
|
393
349
|
/**
|
|
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.
|
|
406
|
-
*
|
|
407
|
-
* The `content` is formatted as `"<elementContent>[<sep><elementContent>]..."` to
|
|
408
|
-
* signal repeatability.
|
|
350
|
+
* Splits a delimited string into a typed array.
|
|
351
|
+
* Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
|
|
352
|
+
* Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
|
|
409
353
|
*
|
|
410
|
-
* @typeParam Value -
|
|
354
|
+
* @typeParam Value - Element type produced by `elementType.decoder`.
|
|
411
355
|
*
|
|
412
|
-
* @param elementType -
|
|
413
|
-
* @param separator -
|
|
356
|
+
* @param elementType - Decoder applied to each element.
|
|
357
|
+
* @param separator - Delimiter (default `","`).
|
|
414
358
|
* @returns A {@link Type}`<Array<Value>>`.
|
|
415
359
|
*
|
|
416
360
|
* @example
|
|
@@ -429,8 +373,8 @@ export function typeList<Value>(
|
|
|
429
373
|
): Type<Array<Value>> {
|
|
430
374
|
return {
|
|
431
375
|
content: `${elementType.content}[${separator}${elementType.content}]...`,
|
|
432
|
-
decoder(
|
|
433
|
-
const splits =
|
|
376
|
+
decoder(input: string) {
|
|
377
|
+
const splits = input.split(separator);
|
|
434
378
|
return splits.map((split, index) =>
|
|
435
379
|
TypoError.tryWithContext(
|
|
436
380
|
() => elementType.decoder(split),
|