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
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,24 +51,19 @@ export function typeBoolean(name?: string): Type<boolean> {
|
|
|
50
51
|
return {
|
|
51
52
|
content: name ?? "boolean",
|
|
52
53
|
decoder(input: string) {
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
54
|
+
const lowerInput = input.toLowerCase();
|
|
55
|
+
if (typeBooleanValuesTrue.has(lowerInput)) {
|
|
55
56
|
return true;
|
|
56
57
|
}
|
|
57
|
-
if (
|
|
58
|
+
if (typeBooleanValuesFalse.has(lowerInput)) {
|
|
58
59
|
return false;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
new TypoText(
|
|
62
|
-
new TypoString(`Not a boolean: `),
|
|
63
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
61
|
+
throwInvalidValue("a boolean", input);
|
|
66
62
|
},
|
|
67
63
|
};
|
|
68
64
|
}
|
|
69
|
-
const
|
|
70
|
-
const
|
|
65
|
+
export const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
|
|
66
|
+
export const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
|
|
71
67
|
|
|
72
68
|
/**
|
|
73
69
|
* Parses a date/time string via `Date.parse`.
|
|
@@ -91,12 +87,7 @@ export function typeDatetime(name?: string): Type<Date> {
|
|
|
91
87
|
}
|
|
92
88
|
return new Date(timestampMs);
|
|
93
89
|
} catch {
|
|
94
|
-
|
|
95
|
-
new TypoText(
|
|
96
|
-
new TypoString(`Not a valid ISO_8601 datetime: `),
|
|
97
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
98
|
-
),
|
|
99
|
-
);
|
|
90
|
+
throwInvalidValue("a valid ISO_8601 datetime", input);
|
|
100
91
|
}
|
|
101
92
|
},
|
|
102
93
|
};
|
|
@@ -109,7 +100,7 @@ export function typeDatetime(name?: string): Type<Date> {
|
|
|
109
100
|
* ```ts
|
|
110
101
|
* typeNumber("my-number").decoder("3.14") // → 3.14
|
|
111
102
|
* typeNumber("my-number").decoder("-1") // → -1
|
|
112
|
-
* typeNumber("my-number").decoder("hello") // throws
|
|
103
|
+
* typeNumber("my-number").decoder("hello") // throws
|
|
113
104
|
* ```
|
|
114
105
|
*/
|
|
115
106
|
export function typeNumber(name?: string): Type<number> {
|
|
@@ -123,12 +114,7 @@ export function typeNumber(name?: string): Type<number> {
|
|
|
123
114
|
}
|
|
124
115
|
return parsed;
|
|
125
116
|
} catch {
|
|
126
|
-
|
|
127
|
-
new TypoText(
|
|
128
|
-
new TypoString(`Not a number: `),
|
|
129
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
130
|
-
),
|
|
131
|
-
);
|
|
117
|
+
throwInvalidValue("a number", input);
|
|
132
118
|
}
|
|
133
119
|
},
|
|
134
120
|
};
|
|
@@ -141,8 +127,8 @@ export function typeNumber(name?: string): Type<number> {
|
|
|
141
127
|
* @example
|
|
142
128
|
* ```ts
|
|
143
129
|
* typeInteger("my-integer").decoder("42") // → 42n
|
|
144
|
-
* typeInteger("my-integer").decoder("3.14") // throws
|
|
145
|
-
* typeInteger("my-integer").decoder("abc") // throws
|
|
130
|
+
* typeInteger("my-integer").decoder("3.14") // throws
|
|
131
|
+
* typeInteger("my-integer").decoder("abc") // throws
|
|
146
132
|
* ```
|
|
147
133
|
*/
|
|
148
134
|
export function typeInteger(name?: string): Type<bigint> {
|
|
@@ -152,12 +138,7 @@ export function typeInteger(name?: string): Type<bigint> {
|
|
|
152
138
|
try {
|
|
153
139
|
return BigInt(input);
|
|
154
140
|
} catch {
|
|
155
|
-
|
|
156
|
-
new TypoText(
|
|
157
|
-
new TypoString(`Not an integer: `),
|
|
158
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
159
|
-
),
|
|
160
|
-
);
|
|
141
|
+
throwInvalidValue("an integer", input);
|
|
161
142
|
}
|
|
162
143
|
},
|
|
163
144
|
};
|
|
@@ -170,7 +151,7 @@ export function typeInteger(name?: string): Type<bigint> {
|
|
|
170
151
|
* @example
|
|
171
152
|
* ```ts
|
|
172
153
|
* typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
173
|
-
* typeUrl("my-url").decoder("not-a-url") // throws
|
|
154
|
+
* typeUrl("my-url").decoder("not-a-url") // throws
|
|
174
155
|
* ```
|
|
175
156
|
*/
|
|
176
157
|
export function typeUrl(name?: string): Type<URL> {
|
|
@@ -180,12 +161,7 @@ export function typeUrl(name?: string): Type<URL> {
|
|
|
180
161
|
try {
|
|
181
162
|
return new URL(input);
|
|
182
163
|
} catch {
|
|
183
|
-
|
|
184
|
-
new TypoText(
|
|
185
|
-
new TypoString(`Not an URL: `),
|
|
186
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
187
|
-
),
|
|
188
|
-
);
|
|
164
|
+
throwInvalidValue("an URL", input);
|
|
189
165
|
}
|
|
190
166
|
},
|
|
191
167
|
};
|
|
@@ -296,7 +272,20 @@ export function typePath(
|
|
|
296
272
|
throw new Error(`Path cannot contain null characters`);
|
|
297
273
|
}
|
|
298
274
|
if (checks?.checkSyncExistAs !== undefined) {
|
|
299
|
-
|
|
275
|
+
function safeStatSync(path: string) {
|
|
276
|
+
try {
|
|
277
|
+
return statSync(path);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new TypoError(
|
|
280
|
+
new TypoText(
|
|
281
|
+
new TypoString(`Path does not exist: `),
|
|
282
|
+
new TypoString(`"${path}"`, typoStyleQuote),
|
|
283
|
+
),
|
|
284
|
+
error,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const stats = safeStatSync(input);
|
|
300
289
|
const preview = stats.isDirectory()
|
|
301
290
|
? "directory"
|
|
302
291
|
: stats.isFile()
|
|
@@ -305,7 +294,7 @@ export function typePath(
|
|
|
305
294
|
if (checks.checkSyncExistAs === "file" && !stats.isFile()) {
|
|
306
295
|
throw new TypoError(
|
|
307
296
|
new TypoText(
|
|
308
|
-
new TypoString(`Expected a
|
|
297
|
+
new TypoString(`Expected a file but found: ${preview}: `),
|
|
309
298
|
new TypoString(`"${input}"`, typoStyleQuote),
|
|
310
299
|
),
|
|
311
300
|
);
|
|
@@ -313,7 +302,7 @@ export function typePath(
|
|
|
313
302
|
if (checks.checkSyncExistAs === "directory" && !stats.isDirectory()) {
|
|
314
303
|
throw new TypoError(
|
|
315
304
|
new TypoText(
|
|
316
|
-
new TypoString(`Expected a
|
|
305
|
+
new TypoString(`Expected a directory but found: ${preview}: `),
|
|
317
306
|
new TypoString(`"${input}"`, typoStyleQuote),
|
|
318
307
|
),
|
|
319
308
|
);
|
|
@@ -344,38 +333,37 @@ export function typeChoice<const Value extends string>(
|
|
|
344
333
|
values: Array<Value>,
|
|
345
334
|
caseSensitive: boolean = false,
|
|
346
335
|
): Type<Value> {
|
|
336
|
+
if (values.length === 0) {
|
|
337
|
+
throw new Error("At least one value is required");
|
|
338
|
+
}
|
|
347
339
|
const normalize = caseSensitive
|
|
348
340
|
? (s: string) => s
|
|
349
341
|
: (s: string) => s.toLowerCase();
|
|
350
|
-
const
|
|
342
|
+
const valueByNormalizedKey = new Map(
|
|
343
|
+
values.map((value) => [normalize(value), value]),
|
|
344
|
+
);
|
|
351
345
|
return {
|
|
352
346
|
content: name,
|
|
353
347
|
decoder(input: string) {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
return
|
|
348
|
+
const normalizedKey = normalize(input);
|
|
349
|
+
const value = valueByNormalizedKey.get(normalizedKey);
|
|
350
|
+
if (value !== undefined) {
|
|
351
|
+
return value;
|
|
358
352
|
}
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
374
|
-
new TypoString(` (expected one of: `),
|
|
375
|
-
...valuesPreview,
|
|
376
|
-
new TypoString(`)`),
|
|
377
|
-
),
|
|
378
|
-
);
|
|
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);
|
|
379
367
|
},
|
|
380
368
|
};
|
|
381
369
|
}
|
|
@@ -474,3 +462,12 @@ export function typeList<Value>(
|
|
|
474
462
|
},
|
|
475
463
|
};
|
|
476
464
|
}
|
|
465
|
+
|
|
466
|
+
function throwInvalidValue(kind: string, input: string): never {
|
|
467
|
+
throw new TypoError(
|
|
468
|
+
new TypoText(
|
|
469
|
+
new TypoString(`Not ${kind}: `),
|
|
470
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
}
|
package/src/lib/Typo.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { typeBooleanValuesFalse } from "./Type";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Color names for terminal styling, used by {@link TypoStyle}.
|
|
3
5
|
* `dark*` = standard ANSI (30–37); `bright*` = high-intensity (90–97).
|
|
@@ -123,12 +125,12 @@ export const typoStyleRegularWeaker: TypoStyle = {
|
|
|
123
125
|
*/
|
|
124
126
|
export class TypoString {
|
|
125
127
|
#value: string;
|
|
126
|
-
#typoStyle: TypoStyle;
|
|
128
|
+
#typoStyle: TypoStyle | undefined;
|
|
127
129
|
/**
|
|
128
130
|
* @param value - Raw text content.
|
|
129
|
-
* @param typoStyle - Style to apply when rendering. Defaults to `
|
|
131
|
+
* @param typoStyle - Style to apply when rendering. Defaults to `undefined` (no style).
|
|
130
132
|
*/
|
|
131
|
-
constructor(value: string, typoStyle
|
|
133
|
+
constructor(value: string, typoStyle?: TypoStyle) {
|
|
132
134
|
this.#value = value;
|
|
133
135
|
this.#typoStyle = typoStyle;
|
|
134
136
|
}
|
|
@@ -148,6 +150,11 @@ export class TypoString {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
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
|
+
|
|
151
158
|
/**
|
|
152
159
|
* Mutable sequence of {@link TypoString} segments.
|
|
153
160
|
*/
|
|
@@ -156,12 +163,10 @@ export class TypoText {
|
|
|
156
163
|
/**
|
|
157
164
|
* @param segments - Initial text segments
|
|
158
165
|
*/
|
|
159
|
-
constructor(
|
|
160
|
-
...segments: Array<TypoText | Array<TypoString> | TypoString | string>
|
|
161
|
-
) {
|
|
166
|
+
constructor(...segments: TypoSegment[]) {
|
|
162
167
|
this.#typoStrings = [];
|
|
163
|
-
for (const
|
|
164
|
-
this.push(
|
|
168
|
+
for (const segment of segments) {
|
|
169
|
+
this.push(segment);
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
/**
|
|
@@ -169,14 +174,14 @@ export class TypoText {
|
|
|
169
174
|
*
|
|
170
175
|
* @param segment - Text segment(s) to append.
|
|
171
176
|
*/
|
|
172
|
-
push(segment:
|
|
177
|
+
push(segment: TypoSegment) {
|
|
173
178
|
if (typeof segment === "string") {
|
|
174
179
|
this.#typoStrings.push(new TypoString(segment));
|
|
175
180
|
} else if (segment instanceof TypoText) {
|
|
176
181
|
this.#typoStrings.push(...segment.#typoStrings);
|
|
177
182
|
} else if (Array.isArray(segment)) {
|
|
178
183
|
for (const typoString of segment) {
|
|
179
|
-
this
|
|
184
|
+
this.push(typoString);
|
|
180
185
|
}
|
|
181
186
|
} else {
|
|
182
187
|
this.#typoStrings.push(segment);
|
|
@@ -209,6 +214,20 @@ export class TypoText {
|
|
|
209
214
|
}
|
|
210
215
|
return length;
|
|
211
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
|
+
}
|
|
212
231
|
}
|
|
213
232
|
|
|
214
233
|
/**
|
|
@@ -332,8 +351,8 @@ export class TypoError extends Error {
|
|
|
332
351
|
* Controls ANSI terminal styling. Create via the static factory methods.
|
|
333
352
|
*/
|
|
334
353
|
export class TypoSupport {
|
|
335
|
-
#kind:
|
|
336
|
-
private constructor(kind:
|
|
354
|
+
#kind: TypoSupportKind;
|
|
355
|
+
private constructor(kind: TypoSupportKind) {
|
|
337
356
|
this.#kind = kind;
|
|
338
357
|
}
|
|
339
358
|
/**
|
|
@@ -358,27 +377,38 @@ export class TypoSupport {
|
|
|
358
377
|
* Auto-detects styling mode from the process environment on best-effort basis.
|
|
359
378
|
*/
|
|
360
379
|
static inferFromEnv(): TypoSupport {
|
|
361
|
-
|
|
380
|
+
/*
|
|
381
|
+
console.warn({
|
|
382
|
+
no: readEnvVar("NO_COLOR"),
|
|
383
|
+
force: readEnvVar("FORCE_COLOR"),
|
|
384
|
+
mock: readEnvVar("MOCK_COLOR"),
|
|
385
|
+
term: readEnvVar("TERM"),
|
|
386
|
+
tty: process.stdout.isTTY,
|
|
387
|
+
});
|
|
388
|
+
*/
|
|
389
|
+
if (!process || !process.env || !process.stdout) {
|
|
362
390
|
return TypoSupport.none();
|
|
363
391
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return undefined;
|
|
367
|
-
}
|
|
368
|
-
return process.env[name];
|
|
392
|
+
if (readEnvVar("NO_COLOR")) {
|
|
393
|
+
return TypoSupport.none();
|
|
369
394
|
}
|
|
370
395
|
const envForceColor = readEnvVar("FORCE_COLOR");
|
|
371
396
|
if (envForceColor === "0") {
|
|
372
397
|
return TypoSupport.none();
|
|
373
398
|
}
|
|
374
399
|
if (envForceColor !== undefined) {
|
|
375
|
-
|
|
400
|
+
if (!typeBooleanValuesFalse.has(envForceColor.toLowerCase())) {
|
|
401
|
+
return TypoSupport.tty();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (readEnvVar("MOCK_COLOR")) {
|
|
405
|
+
return TypoSupport.mock();
|
|
376
406
|
}
|
|
377
|
-
if (
|
|
407
|
+
if (!process.stdout.isTTY) {
|
|
378
408
|
return TypoSupport.none();
|
|
379
409
|
}
|
|
380
|
-
if (readEnvVar("
|
|
381
|
-
return TypoSupport.
|
|
410
|
+
if (readEnvVar("TERM")?.toLowerCase() === "dumb") {
|
|
411
|
+
return TypoSupport.none();
|
|
382
412
|
}
|
|
383
413
|
return TypoSupport.tty();
|
|
384
414
|
}
|
|
@@ -389,7 +419,10 @@ export class TypoSupport {
|
|
|
389
419
|
* @param typoStyle - Style to apply.
|
|
390
420
|
* @returns Styled string.
|
|
391
421
|
*/
|
|
392
|
-
computeStyledString(value: string, typoStyle: TypoStyle): string {
|
|
422
|
+
computeStyledString(value: string, typoStyle: TypoStyle | undefined): string {
|
|
423
|
+
if (typoStyle === undefined) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
393
426
|
let styledValue = value;
|
|
394
427
|
if (typoStyle.case === "upper") {
|
|
395
428
|
styledValue = styledValue.toUpperCase();
|
|
@@ -496,3 +529,12 @@ const ttyCodeBgColors: Record<TypoColor, string> = {
|
|
|
496
529
|
brightCyan: "\x1b[106m",
|
|
497
530
|
brightWhite: "\x1b[107m",
|
|
498
531
|
};
|
|
532
|
+
|
|
533
|
+
function readEnvVar(name: string) {
|
|
534
|
+
if (!(name in process.env)) {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
return process.env[name];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
type TypoSupportKind = "none" | "tty" | "mock";
|
package/src/lib/Usage.ts
CHANGED
|
@@ -116,16 +116,16 @@ export type UsageOption = {
|
|
|
116
116
|
* <detail lines...>
|
|
117
117
|
*
|
|
118
118
|
* Positionals:
|
|
119
|
-
* <
|
|
119
|
+
* <label> <description> (<hint>)
|
|
120
120
|
*
|
|
121
121
|
* Subcommands:
|
|
122
122
|
* <name> <description> (<hint>)
|
|
123
123
|
*
|
|
124
124
|
* Options:
|
|
125
|
-
* -s, --long <
|
|
125
|
+
* -s, --long <label><annotation> <description> (<hint>)
|
|
126
126
|
*
|
|
127
127
|
* Examples:
|
|
128
|
-
* <
|
|
128
|
+
* <explanation>
|
|
129
129
|
* <command line>
|
|
130
130
|
*
|
|
131
131
|
* ```
|
|
@@ -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
|
}),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { it } from "@jest/globals";
|
|
2
|
+
import { similaritySort } from "../src/lib/Similarity";
|
|
3
|
+
|
|
4
|
+
it("run", async function () {
|
|
5
|
+
expect(
|
|
6
|
+
orderBySimilarity("--inst", ["--flag", "--blah", "--install"]),
|
|
7
|
+
).toStrictEqual(["--install", "--flag", "--blah"]);
|
|
8
|
+
|
|
9
|
+
expect(
|
|
10
|
+
orderBySimilarity("instlal", ["install", "dudu", "--blah"]),
|
|
11
|
+
).toStrictEqual(["install", "--blah", "dudu"]);
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
orderBySimilarity("cat", ["cats", "catz", "cut", "kat", "hello", "world"]),
|
|
15
|
+
).toStrictEqual(["cats", "catz", "cut", "kat", "hello", "world"]);
|
|
16
|
+
|
|
17
|
+
expect(orderBySimilarity("cat", ["cut", "kat"])).toStrictEqual([
|
|
18
|
+
"cut",
|
|
19
|
+
"kat",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
expect(orderBySimilarity("acb", ["abc", "ac", "ab"])).toStrictEqual([
|
|
23
|
+
"abc",
|
|
24
|
+
"ac",
|
|
25
|
+
"ab",
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function orderBySimilarity(reference: string, candidates: Array<string>) {
|
|
30
|
+
return similaritySort(
|
|
31
|
+
reference,
|
|
32
|
+
candidates.map((key) => ({ key, value: key })),
|
|
33
|
+
);
|
|
34
|
+
}
|