cli-kiss 0.2.6 → 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 +29 -12
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/guide/02_commands.md +1 -1
- package/docs/guide/03_options.md +3 -3
- package/docs/guide/06_run_as_cli.md +1 -1
- 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 +2 -2
- package/src/lib/Similarity.ts +41 -0
- package/src/lib/Type.ts +28 -29
- package/src/lib/Typo.ts +35 -13
- package/src/lib/Usage.ts +1 -1
- 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 +9 -8
- package/tests/unit.runner.cycle.ts +103 -22
- package/tests/unit.runner.errors.ts +6 -2
|
@@ -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
|
|
@@ -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: [],
|
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
|
@@ -76,8 +76,8 @@ export async function runAndExit<Context>(
|
|
|
76
76
|
const colorOption = optionSingleValue<"auto" | RunColorMode>({
|
|
77
77
|
long: "color",
|
|
78
78
|
type: typeChoice("color-mode", ["auto", "always", "never", "mock"]),
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
defaultIfNotSpecified: () => "auto",
|
|
80
|
+
valueIfNothingInlined: () => "always",
|
|
81
81
|
}).registerAndMakeDecoder(readerArgs);
|
|
82
82
|
preprocessors.push(() => {
|
|
83
83
|
try {
|
|
@@ -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
|
+
}
|
package/src/lib/Type.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { statSync } from "fs";
|
|
2
|
+
import { similaritySort } from "./Similarity";
|
|
2
3
|
import {
|
|
3
4
|
TypoError,
|
|
4
5
|
TypoString,
|
|
@@ -50,18 +51,17 @@ export function typeBoolean(name?: string): Type<boolean> {
|
|
|
50
51
|
return {
|
|
51
52
|
content: name ?? "boolean",
|
|
52
53
|
decoder(input: string) {
|
|
53
|
-
const
|
|
54
|
-
if (typeBooleanValuesTrue.has(
|
|
54
|
+
const lowerInput = input.toLowerCase();
|
|
55
|
+
if (typeBooleanValuesTrue.has(lowerInput)) {
|
|
55
56
|
return true;
|
|
56
57
|
}
|
|
57
|
-
if (typeBooleanValuesFalse.has(
|
|
58
|
+
if (typeBooleanValuesFalse.has(lowerInput)) {
|
|
58
59
|
return false;
|
|
59
60
|
}
|
|
60
61
|
throwInvalidValue("a boolean", input);
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
|
-
|
|
65
65
|
export const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
|
|
66
66
|
export const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
|
|
67
67
|
|
|
@@ -333,38 +333,37 @@ export function typeChoice<const Value extends string>(
|
|
|
333
333
|
values: Array<Value>,
|
|
334
334
|
caseSensitive: boolean = false,
|
|
335
335
|
): Type<Value> {
|
|
336
|
+
if (values.length === 0) {
|
|
337
|
+
throw new Error("At least one value is required");
|
|
338
|
+
}
|
|
336
339
|
const normalize = caseSensitive
|
|
337
340
|
? (s: string) => s
|
|
338
341
|
: (s: string) => s.toLowerCase();
|
|
339
|
-
const
|
|
342
|
+
const valueByNormalizedKey = new Map(
|
|
343
|
+
values.map((value) => [normalize(value), value]),
|
|
344
|
+
);
|
|
340
345
|
return {
|
|
341
346
|
content: name,
|
|
342
347
|
decoder(input: string) {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
return
|
|
348
|
+
const normalizedKey = normalize(input);
|
|
349
|
+
const value = valueByNormalizedKey.get(normalizedKey);
|
|
350
|
+
if (value !== undefined) {
|
|
351
|
+
return value;
|
|
347
352
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
363
|
-
new TypoString(` (expected one of: `),
|
|
364
|
-
...valuesPreview,
|
|
365
|
-
new TypoString(`)`),
|
|
366
|
-
),
|
|
367
|
-
);
|
|
353
|
+
const text = new TypoText();
|
|
354
|
+
text.push(new TypoString(`Unknown value: `));
|
|
355
|
+
text.push(new TypoString(`"${input}"`, typoStyleQuote));
|
|
356
|
+
const suggestions = similaritySort(
|
|
357
|
+
normalizedKey,
|
|
358
|
+
[...valueByNormalizedKey.entries()].map(([normalizedKey, value]) => ({
|
|
359
|
+
key: normalizedKey,
|
|
360
|
+
value: new TypoString(`"${value}"`, typoStyleQuote),
|
|
361
|
+
})),
|
|
362
|
+
).slice(0, 3);
|
|
363
|
+
text.push(new TypoString(`: did you mean: `));
|
|
364
|
+
text.push(TypoText.join(suggestions, new TypoString(`, `)));
|
|
365
|
+
text.push(new TypoString(` ?`));
|
|
366
|
+
throw new TypoError(text);
|
|
368
367
|
},
|
|
369
368
|
};
|
|
370
369
|
}
|
package/src/lib/Typo.ts
CHANGED
|
@@ -125,12 +125,12 @@ export const typoStyleRegularWeaker: TypoStyle = {
|
|
|
125
125
|
*/
|
|
126
126
|
export class TypoString {
|
|
127
127
|
#value: string;
|
|
128
|
-
#typoStyle: TypoStyle;
|
|
128
|
+
#typoStyle: TypoStyle | undefined;
|
|
129
129
|
/**
|
|
130
130
|
* @param value - Raw text content.
|
|
131
|
-
* @param typoStyle - Style to apply when rendering. Defaults to `
|
|
131
|
+
* @param typoStyle - Style to apply when rendering. Defaults to `undefined` (no style).
|
|
132
132
|
*/
|
|
133
|
-
constructor(value: string, typoStyle
|
|
133
|
+
constructor(value: string, typoStyle?: TypoStyle) {
|
|
134
134
|
this.#value = value;
|
|
135
135
|
this.#typoStyle = typoStyle;
|
|
136
136
|
}
|
|
@@ -150,6 +150,11 @@ export class TypoString {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* A segment of styled text, a string, or an array of segments.
|
|
155
|
+
*/
|
|
156
|
+
export type TypoSegment = TypoText | TypoString | string | Array<TypoSegment>;
|
|
157
|
+
|
|
153
158
|
/**
|
|
154
159
|
* Mutable sequence of {@link TypoString} segments.
|
|
155
160
|
*/
|
|
@@ -158,12 +163,10 @@ export class TypoText {
|
|
|
158
163
|
/**
|
|
159
164
|
* @param segments - Initial text segments
|
|
160
165
|
*/
|
|
161
|
-
constructor(
|
|
162
|
-
...segments: Array<TypoText | Array<TypoString> | TypoString | string>
|
|
163
|
-
) {
|
|
166
|
+
constructor(...segments: TypoSegment[]) {
|
|
164
167
|
this.#typoStrings = [];
|
|
165
|
-
for (const
|
|
166
|
-
this.push(
|
|
168
|
+
for (const segment of segments) {
|
|
169
|
+
this.push(segment);
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
/**
|
|
@@ -171,14 +174,14 @@ export class TypoText {
|
|
|
171
174
|
*
|
|
172
175
|
* @param segment - Text segment(s) to append.
|
|
173
176
|
*/
|
|
174
|
-
push(segment:
|
|
177
|
+
push(segment: TypoSegment) {
|
|
175
178
|
if (typeof segment === "string") {
|
|
176
179
|
this.#typoStrings.push(new TypoString(segment));
|
|
177
180
|
} else if (segment instanceof TypoText) {
|
|
178
181
|
this.#typoStrings.push(...segment.#typoStrings);
|
|
179
182
|
} else if (Array.isArray(segment)) {
|
|
180
183
|
for (const typoString of segment) {
|
|
181
|
-
this
|
|
184
|
+
this.push(typoString);
|
|
182
185
|
}
|
|
183
186
|
} else {
|
|
184
187
|
this.#typoStrings.push(segment);
|
|
@@ -211,6 +214,20 @@ export class TypoText {
|
|
|
211
214
|
}
|
|
212
215
|
return length;
|
|
213
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Joins multiple segments with a separator.
|
|
219
|
+
* @returns A new {@link TypoText} containing the joined segments.
|
|
220
|
+
*/
|
|
221
|
+
static join(segments: Array<TypoSegment>, separator: TypoSegment): TypoText {
|
|
222
|
+
const result = new TypoText();
|
|
223
|
+
for (let index = 0; index < segments.length; index++) {
|
|
224
|
+
if (index > 0) {
|
|
225
|
+
result.push(separator);
|
|
226
|
+
}
|
|
227
|
+
result.push(segments[index]!);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
/**
|
|
@@ -334,8 +351,8 @@ export class TypoError extends Error {
|
|
|
334
351
|
* Controls ANSI terminal styling. Create via the static factory methods.
|
|
335
352
|
*/
|
|
336
353
|
export class TypoSupport {
|
|
337
|
-
#kind:
|
|
338
|
-
private constructor(kind:
|
|
354
|
+
#kind: TypoSupportKind;
|
|
355
|
+
private constructor(kind: TypoSupportKind) {
|
|
339
356
|
this.#kind = kind;
|
|
340
357
|
}
|
|
341
358
|
/**
|
|
@@ -402,7 +419,10 @@ export class TypoSupport {
|
|
|
402
419
|
* @param typoStyle - Style to apply.
|
|
403
420
|
* @returns Styled string.
|
|
404
421
|
*/
|
|
405
|
-
computeStyledString(value: string, typoStyle: TypoStyle): string {
|
|
422
|
+
computeStyledString(value: string, typoStyle: TypoStyle | undefined): string {
|
|
423
|
+
if (typoStyle === undefined) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
406
426
|
let styledValue = value;
|
|
407
427
|
if (typoStyle.case === "upper") {
|
|
408
428
|
styledValue = styledValue.toUpperCase();
|
|
@@ -516,3 +536,5 @@ function readEnvVar(name: string) {
|
|
|
516
536
|
}
|
|
517
537
|
return process.env[name];
|
|
518
538
|
}
|
|
539
|
+
|
|
540
|
+
type TypoSupportKind = "none" | "tty" | "mock";
|
package/src/lib/Usage.ts
CHANGED
|
@@ -262,7 +262,7 @@ export function usageToStyledLines(params: {
|
|
|
262
262
|
for (const commandArg of example.commandArgs) {
|
|
263
263
|
commandLineText.push(textDelimiter(" "));
|
|
264
264
|
if (typeof commandArg === "string") {
|
|
265
|
-
commandLineText.push(commandArg);
|
|
265
|
+
commandLineText.push(new TypoString(commandArg));
|
|
266
266
|
} else if ("positional" in commandArg) {
|
|
267
267
|
commandLineText.push(textUserInput(commandArg.positional));
|
|
268
268
|
} else if ("subcommand" in commandArg) {
|
|
@@ -39,8 +39,8 @@ const rootCommand = commandChained<any, any, any>(
|
|
|
39
39
|
long: "choice-option",
|
|
40
40
|
type: typeChoice("choice", ["unset", "empty", "choice1", "choice2"]),
|
|
41
41
|
description: "choice-option description",
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
valueIfNothingInlined: () => "empty",
|
|
43
|
+
defaultIfNotSpecified: () => "unset",
|
|
44
44
|
}),
|
|
45
45
|
booleanFlag: optionFlag({
|
|
46
46
|
short: "b",
|
|
@@ -81,7 +81,7 @@ const rootCommand = commandChained<any, any, any>(
|
|
|
81
81
|
short: "s",
|
|
82
82
|
long: "string-option",
|
|
83
83
|
type: type("cool-stuff"),
|
|
84
|
-
|
|
84
|
+
defaultIfNotSpecified: () => undefined,
|
|
85
85
|
description: "string-option description",
|
|
86
86
|
}),
|
|
87
87
|
complexOption: optionRepeatable({
|
|
@@ -174,7 +174,7 @@ const rootCommand = commandChained<any, any, any>(
|
|
|
174
174
|
duduValue: optionSingleValue({
|
|
175
175
|
long: "dudu",
|
|
176
176
|
type: type("dudu-value"),
|
|
177
|
-
|
|
177
|
+
defaultIfNotSpecified: () => "duduDefault",
|
|
178
178
|
hint: "Dudu option hint",
|
|
179
179
|
description: "Dudu option description",
|
|
180
180
|
}),
|