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.
@@ -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/05_types" },
32
- { text: "Running your CLI", link: "/guide/06_run" },
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
- --vp-home-hero-image-background-image: linear-gradient( -50deg, #ff003cff 0%, #00000000 50%, #459900ff 100% );
3
- --vp-home-hero-image-filter: blur(150px);
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
- defaultWhenNotDefined: function () {
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;
@@ -44,7 +44,7 @@ const output = optionSingleValue({
44
44
  short: "o",
45
45
  type: typePath(),
46
46
  description: "Output directory",
47
- defaultWhenNotDefined: () => "dist/",
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
- | `defaultWhenNotDefined` | `() => Value` | Value when option is absent — **throw** to make it required |
63
- | `defaultWhenNotInlined` | `() => Value?` | Value when option is present but has no inline value (e.g. `--output` alone) |
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 shown in help/errors.
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/1/y/t` → true, `false/no/off/0/n/f` → false (case-insensitive) |
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" }); // throws if not a file
36
- typePath("dir", { checkSyncExistAs: "directory" }); // throws if not a directory
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" → TypoError
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 typeId = typeRenamed(typeInteger(), "user-id");
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 | 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"\|"mock"?` | `"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 |
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
- defaultWhenNotDefined: () => new URL("postgres://localhost/mydb"),
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-kiss
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 Simple and Stupid, it just does the job.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-kiss",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "devDependencies": {
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./lib/Option";
4
4
  export * from "./lib/Positional";
5
5
  export * from "./lib/Reader";
6
6
  export * from "./lib/Run";
7
+ export * from "./lib/Similarity";
7
8
  export * from "./lib/Type";
8
9
  export * from "./lib/Typo";
9
10
  export * from "./lib/Usage";
@@ -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
- throw new TypoError(
216
- new TypoText(
217
- new TypoString(`<subcommand>`, typoStyleUserInput),
218
- new TypoString(`: Invalid value: `),
219
- new TypoString(`"${subcommandName}"`, typoStyleQuote),
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);
@@ -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", default: false }),
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 type = typeBoolean("value");
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, short, type, input: value });
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.valueWhenNotDefined - Default value when the option is not specified at all.
130
- * @param definition.valueWhenNotInlined - Default value when the option is specified without an inline value (e.g. `--option` or `-o`).
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
- * valueWhenNotDefined: () => "dist",
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
- defaultWhenNotDefined: () => Value;
156
- defaultWhenNotInlined?: () => Value;
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.defaultWhenNotInlined !== undefined) {
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.defaultWhenNotDefined();
189
+ return definition.defaultIfNotSpecified();
190
190
  } catch (error) {
191
- throwFailedToGetDefaultValueError(long, error, "not set");
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, short, label, type, input: inlined });
197
+ return decodeValue({ long, label, type, input: inlined });
197
198
  }
198
- if (definition.defaultWhenNotInlined !== undefined) {
199
+ if (definition.valueIfNothingInlined !== undefined) {
199
200
  try {
200
- return definition.defaultWhenNotInlined();
201
+ return definition.valueIfNothingInlined();
201
202
  } catch (error) {
202
- throwFailedToGetDefaultValueError(long, error, "not inlined");
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, short, label, type, input: separated });
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, short, label, type, input });
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
- throw new TypoError(
345
- new TypoText(
346
- new TypoString(`--${long}`, typoStyleConstants),
347
- new TypoString(`: Failed to get default value (${context})`),
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
  }
@@ -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 (default: world)",
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
- throw new TypoError(
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
- throw new TypoError(
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" | "env" | "always" | "never" | "mock" | undefined;
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" | "always" | "never" | "mock">(
72
- {
73
- long: "color",
74
- type: typeChoice("color-mode", ["auto", "always", "never", "mock"]),
75
- defaultWhenNotDefined: () => "auto",
76
- defaultWhenNotInlined: () => "always",
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
- if (colorSetup === "env") {
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
+ }