cli-kiss 0.2.5 → 0.2.7
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 +62 -4
- package/dist/index.d.ts +43 -20
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +2 -2
- package/docs/.vitepress/theme/style.css +6 -2
- package/docs/guide/02_commands.md +1 -1
- package/docs/guide/03_options.md +3 -3
- package/docs/guide/{05_types.md → 05_input_types.md} +19 -17
- package/docs/guide/{06_run.md → 06_run_as_cli.md} +11 -16
- package/docs/index.md +3 -2
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/Command.ts +21 -7
- package/src/lib/Operation.ts +1 -1
- package/src/lib/Option.ts +28 -32
- package/src/lib/Positional.ts +2 -1
- package/src/lib/Reader.ts +31 -12
- package/src/lib/Run.ts +16 -17
- package/src/lib/Similarity.ts +41 -0
- package/src/lib/Type.ts +64 -67
- package/src/lib/Typo.ts +65 -23
- package/src/lib/Usage.ts +4 -4
- package/tests/unit.command.execute.ts +1 -1
- package/tests/unit.command.usage.ts +4 -4
- package/tests/unit.fuzzed.alternatives.ts +34 -0
- package/tests/unit.runner.colors.ts +124 -121
- package/tests/unit.runner.cycle.ts +103 -22
- package/tests/unit.runner.errors.ts +6 -2
|
@@ -28,8 +28,8 @@ export default defineConfig({
|
|
|
28
28
|
{ text: "Commands", link: "/guide/02_commands" },
|
|
29
29
|
{ text: "Options", link: "/guide/03_options" },
|
|
30
30
|
{ text: "Positionals", link: "/guide/04_positionals" },
|
|
31
|
-
{ text: "Types", link: "/guide/
|
|
32
|
-
{ text: "Running your CLI", link: "/guide/
|
|
31
|
+
{ text: "Input Types", link: "/guide/05_input_types" },
|
|
32
|
+
{ text: "Running your CLI", link: "/guide/06_run_as_cli" },
|
|
33
33
|
],
|
|
34
34
|
},
|
|
35
35
|
],
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
--vp-home-hero-
|
|
2
|
+
/*
|
|
3
|
+
--vp-home-hero-name-color: transparent;
|
|
4
|
+
--vp-home-hero-name-background: linear-gradient(-50deg, #ff003caa 30%, #459900aa 70%);
|
|
5
|
+
*/
|
|
6
|
+
--vp-home-hero-image-background-image: linear-gradient( -50deg, #ff003c88 25%, #008732aa 60% );
|
|
7
|
+
--vp-home-hero-image-filter: blur(60px);
|
|
4
8
|
}
|
|
@@ -87,7 +87,7 @@ const authenticatedDeploy = commandChained(
|
|
|
87
87
|
long: "token",
|
|
88
88
|
type: type("secret"),
|
|
89
89
|
description: "API token",
|
|
90
|
-
|
|
90
|
+
defaultIfNotSpecified: function () {
|
|
91
91
|
const t = process.env.API_TOKEN;
|
|
92
92
|
if (!t) throw new Error("API_TOKEN env var is required");
|
|
93
93
|
return t;
|
package/docs/guide/03_options.md
CHANGED
|
@@ -44,7 +44,7 @@ const output = optionSingleValue({
|
|
|
44
44
|
short: "o",
|
|
45
45
|
type: typePath(),
|
|
46
46
|
description: "Output directory",
|
|
47
|
-
|
|
47
|
+
defaultIfNotSpecified: () => "dist/",
|
|
48
48
|
});
|
|
49
49
|
// --output dist/ → "dist/"
|
|
50
50
|
// --output=dist/ → "dist/"
|
|
@@ -59,8 +59,8 @@ const output = optionSingleValue({
|
|
|
59
59
|
| `type` | `Type<Value>` | Decoder for the value |
|
|
60
60
|
| `description` | `string?` | Help text |
|
|
61
61
|
| `hint` | `string?` | Short note in parentheses |
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
62
|
+
| `defaultIfNotSpecified` | `() => Value` | Value when option is absent — **throw** to make it required |
|
|
63
|
+
| `valueIfNothingInlined` | `() => Value?` | Value when option is present but has no inline value (e.g. `--output` alone) |
|
|
64
64
|
| `aliases` | `{ longs?, shorts? }` | Additional names |
|
|
65
65
|
|
|
66
66
|
## `optionRepeatable` — collect multiple values
|
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
# Types
|
|
1
|
+
# Input Types
|
|
2
2
|
|
|
3
3
|
A `Type<Value>` converts a raw CLI string into a typed value:
|
|
4
4
|
|
|
5
5
|
- Contains a `content` label about the type of data being decoded
|
|
6
6
|
- Paired with a `decoder` function that throws if the value is invalid.
|
|
7
7
|
|
|
8
|
+
A `Type<Value>` can then be used as a value for an `Option` or `Positional`
|
|
9
|
+
|
|
8
10
|
## Built-in types
|
|
9
11
|
|
|
10
|
-
All type factories accept an optional `name` parameter that overrides the label
|
|
12
|
+
All type factories accept an optional `name` parameter that overrides the label
|
|
13
|
+
shown in help/errors.
|
|
11
14
|
|
|
12
|
-
| Type factory | Content type | Accepts
|
|
13
|
-
| -------------- | ------------ |
|
|
14
|
-
| `type` | `string` | Any string
|
|
15
|
-
| `typeBoolean` | `boolean` | `true/yes/on/
|
|
16
|
-
| `typeNumber` | `number` | Integers, floats, scientific notation
|
|
17
|
-
| `typeInteger` | `bigint` | Integer strings only
|
|
18
|
-
| `typeDatetime` | `Date` | Any format accepted by `Date.parse` (ISO 8601 recommended)
|
|
19
|
-
| `typeUrl` | `URL` | Absolute URLs
|
|
20
|
-
| `typePath` | `string` | Non-empty path strings; optional sync existence check
|
|
15
|
+
| Type factory | Content type | Accepts |
|
|
16
|
+
| -------------- | ------------ | ------------------------------------------------------------------- |
|
|
17
|
+
| `type` | `string` | Any string |
|
|
18
|
+
| `typeBoolean` | `boolean` | `true/yes/on/y` → true, `false/no/off/n` → false (case-insensitive) |
|
|
19
|
+
| `typeNumber` | `number` | Integers, floats, scientific notation |
|
|
20
|
+
| `typeInteger` | `bigint` | Integer strings only |
|
|
21
|
+
| `typeDatetime` | `Date` | Any format accepted by `Date.parse` (ISO 8601 recommended) |
|
|
22
|
+
| `typeUrl` | `URL` | Absolute URLs |
|
|
23
|
+
| `typePath` | `string` | Non-empty path strings; optional sync existence check |
|
|
21
24
|
|
|
22
25
|
```ts
|
|
23
26
|
type("greeting").decoder("hello"); // → "hello"
|
|
@@ -32,8 +35,8 @@ typePath().decoder("/usr/bin"); // → "/usr/bin"
|
|
|
32
35
|
`typePath` also accepts a second argument for existence checks:
|
|
33
36
|
|
|
34
37
|
```ts
|
|
35
|
-
typePath("config", { checkSyncExistAs: "file" });
|
|
36
|
-
typePath("dir",
|
|
38
|
+
typePath("config", { checkSyncExistAs: "file" }); // throws if not a file
|
|
39
|
+
typePath("dir", { checkSyncExistAs: "directory" }); // throws if not a directory
|
|
37
40
|
```
|
|
38
41
|
|
|
39
42
|
## `typeChoice` — string enum
|
|
@@ -101,7 +104,7 @@ const typePort = typeConverted("port", typeNumber(), (n) => {
|
|
|
101
104
|
return n;
|
|
102
105
|
});
|
|
103
106
|
// "--port 8080" → 8080
|
|
104
|
-
// "--port 99999" →
|
|
107
|
+
// "--port 99999" → throws
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
## `typeRenamed` — rename a type
|
|
@@ -109,8 +112,7 @@ const typePort = typeConverted("port", typeNumber(), (n) => {
|
|
|
109
112
|
Wraps a type with a different label for clearer errors:
|
|
110
113
|
|
|
111
114
|
```ts
|
|
112
|
-
const
|
|
113
|
-
// errors show "user-id" instead of "integer"
|
|
115
|
+
const typeUserId = typeRenamed(typeInteger("u64"), "user-id");
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
## Custom types
|
|
@@ -124,7 +126,7 @@ const typeHexColor: Type<string> = {
|
|
|
124
126
|
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
125
127
|
return value;
|
|
126
128
|
}
|
|
127
|
-
throw new Error(`Not a valid color: "${value}"`);
|
|
129
|
+
throw new Error(`Not a valid hex color: "${value}"`);
|
|
128
130
|
},
|
|
129
131
|
};
|
|
130
132
|
// "#ff0000" → "#ff0000"
|
|
@@ -18,14 +18,14 @@ await runAndExit(cliName, cliArgs, context, command, options?);
|
|
|
18
18
|
|
|
19
19
|
### Options
|
|
20
20
|
|
|
21
|
-
| Option | Type
|
|
22
|
-
| -------------- |
|
|
23
|
-
| `buildVersion` | `string?`
|
|
24
|
-
| `usageOnHelp` | `boolean?`
|
|
25
|
-
| `usageOnError` | `boolean?`
|
|
26
|
-
| `colorSetup` | `
|
|
27
|
-
| `onError` | `(error: unknown) => void`
|
|
28
|
-
| `onExit` | `(code: number) => never`
|
|
21
|
+
| Option | Type | Default | Description |
|
|
22
|
+
| -------------- | ----------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `buildVersion` | `string?` | — | Enables `--version` flag; prints `<cliName> <buildVersion>` |
|
|
24
|
+
| `usageOnHelp` | `boolean?` | `true` | Enables `--help` flag |
|
|
25
|
+
| `usageOnError` | `boolean?` | `true` | Prints usage to stderr when parsing fails |
|
|
26
|
+
| `colorSetup` | `flag` / `env` / `always` / `never` | `"flag"` | Color mode: `"flag"` adds a `--color` option; `"env"` reads env vars; others force the mode |
|
|
27
|
+
| `onError` | `(error: unknown) => void` | — | Custom handler for parse and execution errors |
|
|
28
|
+
| `onExit` | `(code: number) => never` | `process.exit` | Override for testing |
|
|
29
29
|
|
|
30
30
|
### Exit codes
|
|
31
31
|
|
|
@@ -67,7 +67,7 @@ const rootCmd = commandWithSubcommands(
|
|
|
67
67
|
long: "db",
|
|
68
68
|
type: typeUrl(),
|
|
69
69
|
description: "Database URL",
|
|
70
|
-
|
|
70
|
+
defaultIfNotSpecified: () => new URL("postgres://localhost/mydb"),
|
|
71
71
|
}),
|
|
72
72
|
},
|
|
73
73
|
positionals: [],
|
|
@@ -118,17 +118,12 @@ Colors are auto-detected by default (`colorSetup: "flag"` adds a `--color`
|
|
|
118
118
|
option). Override:
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
|
+
// Read from env vars (FORCE_COLOR, NO_COLOR), same as `--color=auto`
|
|
122
|
+
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "env" });
|
|
121
123
|
// Force colors on
|
|
122
124
|
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "always" });
|
|
123
|
-
|
|
124
125
|
// Force colors off (useful in CI)
|
|
125
126
|
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "never" });
|
|
126
|
-
|
|
127
|
-
// Read from env vars (FORCE_COLOR, NO_COLOR, MOCK_COLOR)
|
|
128
|
-
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "env" });
|
|
129
|
-
|
|
130
|
-
// Deterministic mock output (useful in snapshot tests)
|
|
131
|
-
await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "mock" });
|
|
132
127
|
```
|
|
133
128
|
|
|
134
129
|
## Testing your CLI
|
package/docs/index.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
layout: home
|
|
3
3
|
|
|
4
4
|
hero:
|
|
5
|
-
name: CLI-
|
|
5
|
+
name: CLI-Kiss
|
|
6
6
|
text: CLI for TypeScript.
|
|
7
7
|
|
|
8
8
|
tagline:
|
|
9
|
-
No bloat, no dependencies.<br/>Standard behavior users expect.<br/>Keep It
|
|
9
|
+
No bloat, no dependencies.<br/>Standard behavior users expect.<br/>Keep It
|
|
10
|
+
Simple and Stupid, it just does the job.
|
|
10
11
|
|
|
11
12
|
image:
|
|
12
13
|
src: /hero.png
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/lib/Command.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Operation } from "./Operation";
|
|
2
2
|
import { ReaderArgs } from "./Reader";
|
|
3
|
+
import { similaritySort } from "./Similarity";
|
|
3
4
|
import {
|
|
4
5
|
TypoError,
|
|
5
6
|
TypoString,
|
|
7
|
+
typoStyleConstants,
|
|
6
8
|
typoStyleQuote,
|
|
7
9
|
typoStyleUserInput,
|
|
8
10
|
TypoText,
|
|
@@ -194,6 +196,10 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
194
196
|
operation: Operation<Context, Payload>,
|
|
195
197
|
subcommands: { [subcommand: string]: Command<Payload, Result> },
|
|
196
198
|
): Command<Context, Result> {
|
|
199
|
+
const subcommandNames = Object.keys(subcommands);
|
|
200
|
+
if (subcommandNames.length === 0) {
|
|
201
|
+
throw new Error("At least one subcommand is required");
|
|
202
|
+
}
|
|
197
203
|
return {
|
|
198
204
|
getInformation() {
|
|
199
205
|
return information;
|
|
@@ -212,13 +218,21 @@ export function commandWithSubcommands<Context, Payload, Result>(
|
|
|
212
218
|
}
|
|
213
219
|
const subcommandInput = subcommands[subcommandName];
|
|
214
220
|
if (subcommandInput === undefined) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
const text = new TypoText();
|
|
222
|
+
text.push(new TypoString(`<subcommand>`, typoStyleUserInput));
|
|
223
|
+
text.push(new TypoString(`: Unknown name: `));
|
|
224
|
+
text.push(new TypoString(`"${subcommandName}"`, typoStyleQuote));
|
|
225
|
+
const suggestions = similaritySort(
|
|
226
|
+
subcommandName,
|
|
227
|
+
subcommandNames.map((subcommandName) => ({
|
|
228
|
+
key: subcommandName,
|
|
229
|
+
value: new TypoString(subcommandName, typoStyleConstants),
|
|
230
|
+
})),
|
|
231
|
+
).slice(0, 3);
|
|
232
|
+
text.push(new TypoString(`: did you mean: `));
|
|
233
|
+
text.push(TypoText.join(suggestions, new TypoString(`, `)));
|
|
234
|
+
text.push(new TypoString(` ?`));
|
|
235
|
+
throw new TypoError(text);
|
|
222
236
|
}
|
|
223
237
|
const subcommandDecoder =
|
|
224
238
|
subcommandInput.consumeAndMakeDecoder(readerArgs);
|
package/src/lib/Operation.ts
CHANGED
|
@@ -83,7 +83,7 @@ export type OperationInterpreter<Context, Result> = {
|
|
|
83
83
|
* const greetOperation = operation(
|
|
84
84
|
* {
|
|
85
85
|
* options: {
|
|
86
|
-
* loud: optionFlag({ long: "loud", description: "Print in uppercase"
|
|
86
|
+
* loud: optionFlag({ long: "loud", description: "Print in uppercase" }),
|
|
87
87
|
* },
|
|
88
88
|
* positionals: [
|
|
89
89
|
* positionalRequired({ type: type("name"), description: "Name to greet" }),
|
package/src/lib/Option.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function optionFlag(definition: {
|
|
|
77
77
|
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
78
78
|
default?: boolean;
|
|
79
79
|
}): Option<boolean> {
|
|
80
|
-
const
|
|
80
|
+
const typeBool = typeBoolean("value");
|
|
81
81
|
const { long, short, description, hint, aliases } = definition;
|
|
82
82
|
return {
|
|
83
83
|
generateUsage() {
|
|
@@ -105,7 +105,7 @@ export function optionFlag(definition: {
|
|
|
105
105
|
const positiveResult = optionResults[0]!;
|
|
106
106
|
const value =
|
|
107
107
|
positiveResult.inlined === null ? "true" : positiveResult.inlined;
|
|
108
|
-
return decodeValue({ long,
|
|
108
|
+
return decodeValue({ long, type: typeBool, input: value });
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
},
|
|
@@ -126,8 +126,8 @@ export function optionFlag(definition: {
|
|
|
126
126
|
* @param definition.hint - Short note shown in parentheses.
|
|
127
127
|
* @param definition.aliases - Additional names.
|
|
128
128
|
* @param definition.type - Decoder for the raw string value.
|
|
129
|
-
* @param definition.
|
|
130
|
-
* @param definition.
|
|
129
|
+
* @param definition.defaultIfNotSpecified - Default value when the option is not specified at all.
|
|
130
|
+
* @param definition.valueIfNothingInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
|
|
131
131
|
* @returns An {@link Option}`<Value>`.
|
|
132
132
|
*
|
|
133
133
|
* @example
|
|
@@ -137,7 +137,7 @@ export function optionFlag(definition: {
|
|
|
137
137
|
* short: "o",
|
|
138
138
|
* type: typePath(),
|
|
139
139
|
* description: "Output directory",
|
|
140
|
-
*
|
|
140
|
+
* defaultIfNotSpecified: () => "dist",
|
|
141
141
|
* });
|
|
142
142
|
* // Usage:
|
|
143
143
|
* // my-cli → "dist"
|
|
@@ -152,8 +152,8 @@ export function optionSingleValue<Value>(definition: {
|
|
|
152
152
|
hint?: string;
|
|
153
153
|
aliases?: { longs?: Array<string>; shorts?: Array<string> };
|
|
154
154
|
type: Type<Value>;
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
defaultIfNotSpecified: () => Value;
|
|
156
|
+
valueIfNothingInlined?: () => Value;
|
|
157
157
|
}): Option<Value> {
|
|
158
158
|
const { long, short, description, hint, aliases, type } = definition;
|
|
159
159
|
const label = `<${type.content}>`;
|
|
@@ -170,7 +170,7 @@ export function optionSingleValue<Value>(definition: {
|
|
|
170
170
|
parsing: {
|
|
171
171
|
consumeShortGroup: true,
|
|
172
172
|
consumeNextArg(inlined, separated) {
|
|
173
|
-
if (definition.
|
|
173
|
+
if (definition.valueIfNothingInlined !== undefined) {
|
|
174
174
|
return false;
|
|
175
175
|
}
|
|
176
176
|
return inlined === null && separated.length === 0;
|
|
@@ -186,24 +186,26 @@ export function optionSingleValue<Value>(definition: {
|
|
|
186
186
|
const optionResult = optionResults[0];
|
|
187
187
|
if (optionResult === undefined) {
|
|
188
188
|
try {
|
|
189
|
-
return definition.
|
|
189
|
+
return definition.defaultIfNotSpecified();
|
|
190
190
|
} catch (error) {
|
|
191
|
-
|
|
191
|
+
const context = "Not specified";
|
|
192
|
+
throwFailedToGetDefaultValueError({ long, error, context });
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
if (optionResult.inlined) {
|
|
195
196
|
const inlined = optionResult.inlined;
|
|
196
|
-
return decodeValue({ long,
|
|
197
|
+
return decodeValue({ long, label, type, input: inlined });
|
|
197
198
|
}
|
|
198
|
-
if (definition.
|
|
199
|
+
if (definition.valueIfNothingInlined !== undefined) {
|
|
199
200
|
try {
|
|
200
|
-
return definition.
|
|
201
|
+
return definition.valueIfNothingInlined();
|
|
201
202
|
} catch (error) {
|
|
202
|
-
|
|
203
|
+
const context = "Nothing inlined";
|
|
204
|
+
throwFailedToGetDefaultValueError({ long, error, context });
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
const separated = optionResult.separated[0]!;
|
|
206
|
-
return decodeValue({ long,
|
|
208
|
+
return decodeValue({ long, label, type, input: separated });
|
|
207
209
|
},
|
|
208
210
|
};
|
|
209
211
|
},
|
|
@@ -269,7 +271,7 @@ export function optionRepeatable<Value>(definition: {
|
|
|
269
271
|
const optionResults = readerOptions.getOptionValues(key);
|
|
270
272
|
return optionResults.map((optionResult) => {
|
|
271
273
|
const input = optionResult.inlined ?? optionResult.separated[0]!;
|
|
272
|
-
return decodeValue({ long,
|
|
274
|
+
return decodeValue({ long, label, type, input });
|
|
273
275
|
});
|
|
274
276
|
},
|
|
275
277
|
};
|
|
@@ -279,7 +281,6 @@ export function optionRepeatable<Value>(definition: {
|
|
|
279
281
|
|
|
280
282
|
function decodeValue<Value>(params: {
|
|
281
283
|
long: string;
|
|
282
|
-
short?: string | undefined;
|
|
283
284
|
label?: string | undefined;
|
|
284
285
|
type: Type<Value>;
|
|
285
286
|
input: string;
|
|
@@ -288,10 +289,6 @@ function decodeValue<Value>(params: {
|
|
|
288
289
|
() => params.type.decoder(params.input),
|
|
289
290
|
() => {
|
|
290
291
|
const text = new TypoText();
|
|
291
|
-
if (params.short) {
|
|
292
|
-
text.push(new TypoString(`-${params.short}`, typoStyleConstants));
|
|
293
|
-
text.push(new TypoString(`, `));
|
|
294
|
-
}
|
|
295
292
|
text.push(new TypoString(`--${params.long}`, typoStyleConstants));
|
|
296
293
|
if (params.label) {
|
|
297
294
|
text.push(new TypoString(`: `));
|
|
@@ -336,16 +333,15 @@ function throwSetMultipleTimesError(long: string): never {
|
|
|
336
333
|
);
|
|
337
334
|
}
|
|
338
335
|
|
|
339
|
-
function throwFailedToGetDefaultValueError(
|
|
340
|
-
long: string
|
|
341
|
-
error: unknown
|
|
342
|
-
context: string
|
|
343
|
-
): never {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
),
|
|
349
|
-
error,
|
|
336
|
+
function throwFailedToGetDefaultValueError(params: {
|
|
337
|
+
long: string;
|
|
338
|
+
error: unknown;
|
|
339
|
+
context: string;
|
|
340
|
+
}): never {
|
|
341
|
+
const text = new TypoText();
|
|
342
|
+
text.push(new TypoString(`--${params.long}`, typoStyleConstants));
|
|
343
|
+
text.push(
|
|
344
|
+
new TypoString(`: ${params.context}: Failed to generate default value`),
|
|
350
345
|
);
|
|
346
|
+
throw new TypoError(text, params.error);
|
|
351
347
|
}
|
package/src/lib/Positional.ts
CHANGED
|
@@ -102,7 +102,8 @@ export function positionalRequired<Value>(definition: {
|
|
|
102
102
|
* ```ts
|
|
103
103
|
* const greeteePositional = positionalOptional({
|
|
104
104
|
* type: type("name"),
|
|
105
|
-
* description: "Name to greet
|
|
105
|
+
* description: "Name to greet",
|
|
106
|
+
* hint: "Defaults to \"world\"",
|
|
106
107
|
* default: () => "world",
|
|
107
108
|
* });
|
|
108
109
|
* // Usage:
|
package/src/lib/Reader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { similaritySort } from "./Similarity";
|
|
1
2
|
import {
|
|
2
3
|
TypoError,
|
|
3
4
|
TypoString,
|
|
@@ -243,12 +244,7 @@ export class ReaderArgs {
|
|
|
243
244
|
}
|
|
244
245
|
shortIndexEnd++;
|
|
245
246
|
}
|
|
246
|
-
|
|
247
|
-
new TypoText(
|
|
248
|
-
new TypoString(`Unexpected unknown option(s): `),
|
|
249
|
-
new TypoString(`-${arg.slice(shortIndexStart)}`, typoStyleQuote),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
247
|
+
this.#throwUnknownOptionError(`-${arg.slice(shortIndexStart)}`);
|
|
252
248
|
}
|
|
253
249
|
return false;
|
|
254
250
|
}
|
|
@@ -259,12 +255,7 @@ export class ReaderArgs {
|
|
|
259
255
|
if (optionContext !== undefined) {
|
|
260
256
|
return this.#consumeOptionValues(optionContext, constant, inlined);
|
|
261
257
|
}
|
|
262
|
-
|
|
263
|
-
new TypoText(
|
|
264
|
-
new TypoString(`Unexpected unknown option: `),
|
|
265
|
-
new TypoString(constant, typoStyleQuote),
|
|
266
|
-
),
|
|
267
|
-
);
|
|
258
|
+
this.#throwUnknownOptionError(constant);
|
|
268
259
|
}
|
|
269
260
|
|
|
270
261
|
#tryConsumeOptionShort(
|
|
@@ -342,6 +333,34 @@ export class ReaderArgs {
|
|
|
342
333
|
#isValidOptionName(name: string): boolean {
|
|
343
334
|
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
344
335
|
}
|
|
336
|
+
|
|
337
|
+
#throwUnknownOptionError(constant: string): never {
|
|
338
|
+
const candidatesConstants = [];
|
|
339
|
+
for (const optionLong of this.#optionContextByLong.keys()) {
|
|
340
|
+
candidatesConstants.push(`--${optionLong}`);
|
|
341
|
+
}
|
|
342
|
+
for (const optionShort of this.#optionContextByShort.keys()) {
|
|
343
|
+
candidatesConstants.push(`-${optionShort}`);
|
|
344
|
+
}
|
|
345
|
+
const text = new TypoText();
|
|
346
|
+
text.push(new TypoString(`Unknown option: `));
|
|
347
|
+
text.push(new TypoString(`"${constant}"`, typoStyleQuote));
|
|
348
|
+
if (candidatesConstants.length > 0) {
|
|
349
|
+
const suggestionsConstants = similaritySort(
|
|
350
|
+
constant,
|
|
351
|
+
candidatesConstants.map((candidateConstant) => ({
|
|
352
|
+
key: candidateConstant,
|
|
353
|
+
value: new TypoString(candidateConstant, typoStyleConstants),
|
|
354
|
+
})),
|
|
355
|
+
).slice(0, 3);
|
|
356
|
+
text.push(new TypoString(`: did you mean: `));
|
|
357
|
+
text.push(TypoText.join(suggestionsConstants, new TypoString(`, `)));
|
|
358
|
+
text.push(new TypoString(` ?`));
|
|
359
|
+
} else {
|
|
360
|
+
text.push(new TypoString(`, no options are registered`));
|
|
361
|
+
}
|
|
362
|
+
throw new TypoError(text);
|
|
363
|
+
}
|
|
345
364
|
}
|
|
346
365
|
|
|
347
366
|
type ReaderOptionContext = {
|
package/src/lib/Run.ts
CHANGED
|
@@ -5,6 +5,11 @@ import { typeChoice } from "./Type";
|
|
|
5
5
|
import { TypoSupport } from "./Typo";
|
|
6
6
|
import { usageToStyledLines } from "./Usage";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Color selection modes availables
|
|
10
|
+
*/
|
|
11
|
+
export type RunColorMode = "env" | "always" | "never" | "mock";
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Main entry point: parses CLI arguments, executes the matched command, and exits.
|
|
10
15
|
* Handles `--help`, `--version`, usage-on-error, and exit codes.
|
|
@@ -53,7 +58,7 @@ export async function runAndExit<Context>(
|
|
|
53
58
|
context: Context,
|
|
54
59
|
command: Command<Context, void>,
|
|
55
60
|
options?: {
|
|
56
|
-
colorSetup?: "flag" |
|
|
61
|
+
colorSetup?: "flag" | RunColorMode | undefined;
|
|
57
62
|
usageOnHelp?: boolean | undefined;
|
|
58
63
|
usageOnError?: boolean | undefined;
|
|
59
64
|
buildVersion?: string | undefined;
|
|
@@ -68,14 +73,12 @@ export async function runAndExit<Context>(
|
|
|
68
73
|
let typoSupport = TypoSupport.none();
|
|
69
74
|
const colorSetup = options?.colorSetup ?? "flag";
|
|
70
75
|
if (colorSetup === "flag") {
|
|
71
|
-
const colorOption = optionSingleValue<"auto" |
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
).registerAndMakeDecoder(readerArgs);
|
|
76
|
+
const colorOption = optionSingleValue<"auto" | RunColorMode>({
|
|
77
|
+
long: "color",
|
|
78
|
+
type: typeChoice("color-mode", ["auto", "always", "never", "mock"]),
|
|
79
|
+
defaultIfNotSpecified: () => "auto",
|
|
80
|
+
valueIfNothingInlined: () => "always",
|
|
81
|
+
}).registerAndMakeDecoder(readerArgs);
|
|
79
82
|
preprocessors.push(() => {
|
|
80
83
|
try {
|
|
81
84
|
typoSupport = computeTypoSupport(colorOption.getAndDecodeValue());
|
|
@@ -86,11 +89,7 @@ export async function runAndExit<Context>(
|
|
|
86
89
|
return undefined;
|
|
87
90
|
});
|
|
88
91
|
} else {
|
|
89
|
-
|
|
90
|
-
typoSupport = TypoSupport.inferFromEnv();
|
|
91
|
-
} else {
|
|
92
|
-
typoSupport = computeTypoSupport(colorSetup);
|
|
93
|
-
}
|
|
92
|
+
typoSupport = computeTypoSupport(colorSetup);
|
|
94
93
|
}
|
|
95
94
|
if (options?.usageOnHelp ?? true) {
|
|
96
95
|
const helpOption = optionFlag({ long: "help" }).registerAndMakeDecoder(
|
|
@@ -182,12 +181,12 @@ function computeUsageString<Context, Result>(
|
|
|
182
181
|
}).join("\n");
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
function computeTypoSupport(
|
|
186
|
-
colorMode: "auto" | "always" | "never" | "mock",
|
|
187
|
-
): TypoSupport {
|
|
184
|
+
function computeTypoSupport(colorMode: "auto" | RunColorMode): TypoSupport {
|
|
188
185
|
switch (colorMode) {
|
|
189
186
|
case "auto":
|
|
190
187
|
return TypoSupport.inferFromEnv();
|
|
188
|
+
case "env":
|
|
189
|
+
return TypoSupport.inferFromEnv();
|
|
191
190
|
case "always":
|
|
192
191
|
return TypoSupport.tty();
|
|
193
192
|
case "never":
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function similaritySort<Value>(
|
|
2
|
+
reference: string,
|
|
3
|
+
candidates: { [key: string]: Value } | Array<{ key: string; value: Value }>,
|
|
4
|
+
): Array<Value> {
|
|
5
|
+
let entries = Array.isArray(candidates)
|
|
6
|
+
? candidates.map(({ key, value }) => [key, value] as const)
|
|
7
|
+
: Object.entries(candidates);
|
|
8
|
+
const ranked = entries.map(([key, value]) => {
|
|
9
|
+
const score =
|
|
10
|
+
damerauLevenshtein(reference, key) /
|
|
11
|
+
Math.max(reference.length, key.length);
|
|
12
|
+
return { key, value, score };
|
|
13
|
+
});
|
|
14
|
+
return ranked.sort((a, b) => a.score - b.score).map((v) => v.value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function damerauLevenshtein(a: string, b: string): number {
|
|
18
|
+
const m = a.length;
|
|
19
|
+
const n = b.length;
|
|
20
|
+
const dp = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0));
|
|
21
|
+
for (let i = 0; i <= m; i++) {
|
|
22
|
+
dp[i]![0] = i;
|
|
23
|
+
}
|
|
24
|
+
for (let j = 0; j <= n; j++) {
|
|
25
|
+
dp[0]![j] = j;
|
|
26
|
+
}
|
|
27
|
+
for (let i = 1; i <= m; i++) {
|
|
28
|
+
for (let j = 1; j <= n; j++) {
|
|
29
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
30
|
+
dp[i]![j] = Math.min(
|
|
31
|
+
dp[i - 1]![j]! + 1,
|
|
32
|
+
dp[i]![j - 1]! + 1,
|
|
33
|
+
dp[i - 1]![j - 1]! + cost,
|
|
34
|
+
);
|
|
35
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
36
|
+
dp[i]![j] = Math.min(dp[i]![j]!, dp[i - 2]![j - 2]! + cost);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return dp[m]![n]!;
|
|
41
|
+
}
|