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.
@@ -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
@@ -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: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-kiss",
3
- "version": "0.2.6",
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
@@ -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
- defaultWhenNotDefined: () => "auto",
80
- defaultWhenNotInlined: () => "always",
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 lower = input.toLowerCase();
54
- if (typeBooleanValuesTrue.has(lower)) {
54
+ const lowerInput = input.toLowerCase();
55
+ if (typeBooleanValuesTrue.has(lowerInput)) {
55
56
  return true;
56
57
  }
57
- if (typeBooleanValuesFalse.has(lower)) {
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 valueMap = new Map(values.map((value) => [normalize(value), value]));
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 normalized = normalize(input);
344
- const original = valueMap.get(normalized);
345
- if (original !== undefined) {
346
- return original;
348
+ const normalizedKey = normalize(input);
349
+ const value = valueByNormalizedKey.get(normalizedKey);
350
+ if (value !== undefined) {
351
+ return value;
347
352
  }
348
- const valuesPreview = [];
349
- for (const value of values) {
350
- if (valuesPreview.length >= 5) {
351
- valuesPreview.push(new TypoString(`...`));
352
- break;
353
- }
354
- if (valuesPreview.length > 0) {
355
- valuesPreview.push(new TypoString(` | `));
356
- }
357
- valuesPreview.push(new TypoString(`"${value}"`, typoStyleQuote));
358
- }
359
- throw new TypoError(
360
- new TypoText(
361
- new TypoString(`Invalid value: `),
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 `{}` (no style).
131
+ * @param typoStyle - Style to apply when rendering. Defaults to `undefined` (no style).
132
132
  */
133
- constructor(value: string, typoStyle: 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 typoPart of segments) {
166
- this.push(typoPart);
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: TypoText | Array<TypoString> | TypoString | string) {
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.#typoStrings.push(typoString);
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: "none" | "tty" | "mock";
338
- private constructor(kind: "none" | "tty" | "mock") {
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) {
@@ -47,7 +47,7 @@ const rootCommand = commandChained(
47
47
  string: optionSingleValue({
48
48
  long: "string-option",
49
49
  type: type(),
50
- defaultWhenNotDefined: () => undefined,
50
+ defaultIfNotSpecified: () => undefined,
51
51
  }),
52
52
  number: optionRepeatable({
53
53
  long: "number-option",
@@ -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
- defaultWhenNotInlined: () => "empty",
43
- defaultWhenNotDefined: () => "unset",
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
- defaultWhenNotDefined: () => undefined,
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
- defaultWhenNotDefined: () => "duduDefault",
177
+ defaultIfNotSpecified: () => "duduDefault",
178
178
  hint: "Dudu option hint",
179
179
  description: "Dudu option description",
180
180
  }),