cli-kiss 0.2.7 → 0.2.8

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.
Files changed (39) hide show
  1. package/dist/index.d.ts +127 -137
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/docs/.vitepress/config.mts +1 -1
  5. package/docs/.vitepress/theme/Layout.vue +16 -0
  6. package/docs/.vitepress/theme/index.ts +5 -1
  7. package/docs/.vitepress/theme/style.css +5 -1
  8. package/docs/guide/02_commands.md +1 -1
  9. package/docs/guide/03_options.md +11 -11
  10. package/docs/guide/05_input_types.md +9 -10
  11. package/docs/guide/06_run_as_cli.md +1 -1
  12. package/docs/index.md +2 -2
  13. package/docs/public/favicon.ico +0 -0
  14. package/docs/public/logo.png +0 -0
  15. package/package.json +1 -1
  16. package/src/index.ts +1 -1
  17. package/src/lib/Command.ts +45 -39
  18. package/src/lib/Operation.ts +28 -20
  19. package/src/lib/Option.ts +196 -127
  20. package/src/lib/Positional.ts +44 -23
  21. package/src/lib/Reader.ts +194 -226
  22. package/src/lib/Run.ts +19 -8
  23. package/src/lib/Suggest.ts +78 -0
  24. package/src/lib/Type.ts +36 -37
  25. package/src/lib/Typo.ts +58 -55
  26. package/src/lib/Usage.ts +12 -12
  27. package/tests/unit.Reader.commons.ts +92 -116
  28. package/tests/unit.Reader.parsings.ts +14 -26
  29. package/tests/unit.Reader.shortBig.ts +81 -96
  30. package/tests/unit.command.aliases.ts +100 -0
  31. package/tests/unit.command.execute.ts +1 -1
  32. package/tests/unit.command.usage.ts +12 -6
  33. package/tests/unit.fuzzed.alternatives.ts +35 -26
  34. package/tests/unit.runner.colors.ts +8 -33
  35. package/tests/unit.runner.cycle.ts +118 -146
  36. package/tests/unit.runner.errors.ts +25 -22
  37. package/docs/public/hero.png +0 -0
  38. package/src/lib/Similarity.ts +0 -41
  39. package/tests/unit.Reader.aliases.ts +0 -62
@@ -2,7 +2,7 @@ import { defineConfig } from "vitepress";
2
2
 
3
3
  export default defineConfig({
4
4
  description: "Full-featured TypeScript CLI builder. No bloat, no dependency.",
5
- title: "cli-kiss 💋",
5
+ title: "cli-kiss",
6
6
  base: "/cli-kiss/",
7
7
  head: [
8
8
  ["link", { rel: "icon", href: "/cli-kiss/favicon.ico" }],
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import DefaultTheme from 'vitepress/theme'
3
+ const { Layout } = DefaultTheme
4
+ </script>
5
+
6
+ <template>
7
+ <Layout>
8
+ <template #nav-bar-title-before>
9
+ <img
10
+ src="/logo.png"
11
+ alt="logo"
12
+ style="width:32px;height:32px;margin-right:8px;display:block;"
13
+ >
14
+ </template>
15
+ </Layout>
16
+ </template>
@@ -1,4 +1,8 @@
1
1
  import DefaultTheme from "vitepress/theme";
2
+ import MyLayout from "./Layout.vue";
2
3
  import "./style.css";
3
4
 
4
- export default DefaultTheme;
5
+ export default {
6
+ extends: DefaultTheme,
7
+ Layout: MyLayout,
8
+ };
@@ -3,6 +3,10 @@
3
3
  --vp-home-hero-name-color: transparent;
4
4
  --vp-home-hero-name-background: linear-gradient(-50deg, #ff003caa 30%, #459900aa 70%);
5
5
  */
6
- --vp-home-hero-image-background-image: linear-gradient( -50deg, #ff003c88 25%, #008732aa 60% );
6
+ --vp-home-hero-image-background-image: linear-gradient(
7
+ -50deg,
8
+ #ff003c88 25%,
9
+ #008732aa 60%
10
+ );
7
11
  --vp-home-hero-image-filter: blur(60px);
8
12
  }
@@ -87,7 +87,7 @@ const authenticatedDeploy = commandChained(
87
87
  long: "token",
88
88
  type: type("secret"),
89
89
  description: "API token",
90
- defaultIfNotSpecified: function () {
90
+ fallbackValueIfAbsent: 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
- defaultIfNotSpecified: () => "dist/",
47
+ fallbackValueIfAbsent: () => "dist/",
48
48
  });
49
49
  // --output dist/ → "dist/"
50
50
  // --output=dist/ → "dist/"
@@ -52,16 +52,16 @@ const output = optionSingleValue({
52
52
  // (absent) → "dist/"
53
53
  ```
54
54
 
55
- | Parameter | Type | Description |
56
- | ----------------------- | --------------------- | ---------------------------------------------------------------------------- |
57
- | `long` | `string` | Long option name |
58
- | `short` | `string?` | Short option name |
59
- | `type` | `Type<Value>` | Decoder for the value |
60
- | `description` | `string?` | Help text |
61
- | `hint` | `string?` | Short note in parentheses |
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
- | `aliases` | `{ longs?, shorts? }` | Additional names |
55
+ | Parameter | Type | Description |
56
+ | -------------------------- | --------------------- | ---------------------------------------------------------------------------- |
57
+ | `long` | `string` | Long option name |
58
+ | `short` | `string?` | Short option name |
59
+ | `type` | `Type<Value>` | Decoder for the value |
60
+ | `description` | `string?` | Help text |
61
+ | `hint` | `string?` | Short note in parentheses |
62
+ | `fallbackValueIfAbsent` | `() => Value` | Value when option is absent — **throw** to make it required |
63
+ | `impliedValueIfNotInlined` | `() => Value?` | Value when option is present but has no inline value (e.g. `--output` alone) |
64
+ | `aliases` | `{ longs?, shorts? }` | Additional names |
65
65
 
66
66
  ## `optionRepeatable` — collect multiple values
67
67
 
@@ -47,8 +47,7 @@ Accepts only a fixed set of strings (case-insensitive by default):
47
47
  const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
48
48
  typeEnv.decoder("prod"); // → "prod"
49
49
  typeEnv.decoder("PROD"); // → "prod" (case-insensitive)
50
- typeEnv.decoder("unknown");
51
- // Error: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
50
+ typeEnv.decoder("unknown"); // Error: Invalid value: "unknown"
52
51
  ```
53
52
 
54
53
  Pass `true` as third argument to make matching case-sensitive.
@@ -58,9 +57,9 @@ Pass `true` as third argument to make matching case-sensitive.
58
57
  Splits a string into a fixed-length typed tuple:
59
58
 
60
59
  ```ts
61
- const typePoint = typeTuple([typeNumber(), typeNumber()]);
60
+ const typePoint = typeTuple([typeNumber("a"), typeNumber("b")]);
62
61
  typePoint.decoder("3.14,2.71"); // → [3.14, 2.71]
63
- typePoint.decoder("x,2"); // → Error: at 0: Number: Unable to parse: "x"
62
+ typePoint.decoder("x,2"); // → Error: at 0: a: Unable to parse: "x"
64
63
  ```
65
64
 
66
65
  The default separator is `","`. Pass a second argument to change it:
@@ -75,9 +74,9 @@ typeTuple([type("name"), typeNumber()], ":");
75
74
  Splits a string into an array of typed values:
76
75
 
77
76
  ```ts
78
- const typeNumbers = typeList(typeNumber());
77
+ const typeNumbers = typeList(typeNumber("v"));
79
78
  typeNumbers.decoder("1,2,3"); // → [1, 2, 3]
80
- typeNumbers.decoder("1,x,3"); // → Error: at 1: Number: Unable to parse: "x"
79
+ typeNumbers.decoder("1,x,3"); // → Error: at 1: v: Unable to parse: "x"
81
80
  ```
82
81
 
83
82
  Custom separator:
@@ -103,8 +102,8 @@ const typePort = typeConverted("port", typeNumber(), (n) => {
103
102
  if (n < 1 || n > 65535) throw new Error("Out of range");
104
103
  return n;
105
104
  });
106
- // "--port 8080" 8080
107
- // "--port 99999" throws
105
+ typePort.decoder("8080"); // 8080
106
+ typePort.decoder("99999"); // Error: Out of range
108
107
  ```
109
108
 
110
109
  ## `typeRenamed` — rename a type
@@ -129,6 +128,6 @@ const typeHexColor: Type<string> = {
129
128
  throw new Error(`Not a valid hex color: "${value}"`);
130
129
  },
131
130
  };
132
- // "#ff0000" "#ff0000"
133
- // "red" Error: HexColor: Not a valid value: "red"
131
+ typeHexColor.decoder("#ff0000"); // "#ff0000"
132
+ typeHexColor.decoder("red"); // Error: Not a valid hex color: "red"
134
133
  ```
@@ -67,7 +67,7 @@ const rootCmd = commandWithSubcommands(
67
67
  long: "db",
68
68
  type: typeUrl(),
69
69
  description: "Database URL",
70
- defaultIfNotSpecified: () => new URL("postgres://localhost/mydb"),
70
+ fallbackValueIfAbsent: () => new URL("postgres://localhost/mydb"),
71
71
  }),
72
72
  },
73
73
  positionals: [],
package/docs/index.md CHANGED
@@ -2,7 +2,7 @@
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:
@@ -10,7 +10,7 @@ hero:
10
10
  Simple and Stupid, it just does the job.
11
11
 
12
12
  image:
13
- src: /hero.png
13
+ src: /logo.png
14
14
 
15
15
  actions:
16
16
  - theme: brand
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-kiss",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "devDependencies": {
package/src/index.ts CHANGED
@@ -4,7 +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
+ export * from "./lib/Suggest";
8
8
  export * from "./lib/Type";
9
9
  export * from "./lib/Typo";
10
10
  export * from "./lib/Usage";
@@ -1,6 +1,6 @@
1
1
  import { Operation } from "./Operation";
2
2
  import { ReaderArgs } from "./Reader";
3
- import { similaritySort } from "./Similarity";
3
+ import { suggestTextPushMessage } from "./Suggest";
4
4
  import {
5
5
  TypoError,
6
6
  TypoString,
@@ -44,7 +44,7 @@ export type CommandDecoder<Context, Result> = {
44
44
  /**
45
45
  * Creates a ready-to-execute {@link CommandInterpreter}.
46
46
  *
47
- * @throws {@link TypoError} if parsing or decoding failed.
47
+ * @throws if parsing or decoding failed.
48
48
  */
49
49
  decodeAndMakeInterpreter(): CommandInterpreter<Context, Result>;
50
50
  };
@@ -82,6 +82,7 @@ export type CommandInformation = {
82
82
  * Shown in the `Examples:` section.
83
83
  */
84
84
  examples?: Array<{
85
+ // TODO - a nicer example system, maybe with --help=example support
85
86
  /**
86
87
  * Explanation shown above the example.
87
88
  */
@@ -117,13 +118,13 @@ export type CommandInformation = {
117
118
  * const greet = command(
118
119
  * { description: "Greet a user" },
119
120
  * operation(
120
- * { options: {}, positionals: [positionalRequired({ type: type("name") })] },
121
+ * { positionals: [positionalRequired({ type: type("name") })] },
121
122
  * async (_ctx, { positionals: [name] }) => console.log(`Hello, ${name}!`),
122
123
  * ),
123
124
  * );
124
125
  * ```
125
126
  */
126
- export function command<Context, Result>(
127
+ export function command<Context, Result = void>(
127
128
  information: CommandInformation,
128
129
  operation: Operation<Context, Result>,
129
130
  ): Command<Context, Result> {
@@ -136,12 +137,11 @@ export function command<Context, Result>(
136
137
  const operationDecoder = operation.consumeAndMakeDecoder(readerArgs);
137
138
  const endPositional = readerArgs.consumePositional();
138
139
  if (endPositional !== undefined) {
139
- throw new TypoError(
140
- new TypoText(
141
- new TypoString(`Unexpected argument: `),
142
- new TypoString(`"${endPositional}"`, typoStyleQuote),
143
- ),
144
- );
140
+ const errorText = new TypoText();
141
+ errorText.push(new TypoString(`Unexpected argument: `));
142
+ errorText.push(new TypoString(`"${endPositional}"`, typoStyleQuote));
143
+ errorText.push(new TypoString(`.`));
144
+ throw new TypoError(errorText);
145
145
  }
146
146
  return {
147
147
  generateUsage: () => generateUsageLeaf(information, operation),
@@ -168,7 +168,8 @@ export function command<Context, Result>(
168
168
  }
169
169
 
170
170
  /**
171
- * Creates a command that runs `operation` first, then dispatches to a named subcommand.
171
+ * Creates a command that runs `operation` first,
172
+ * then dispatches result to a named subcommand.
172
173
  *
173
174
  * @typeParam Context - Context accepted by `operation`.
174
175
  * @typeParam Payload - Output of `operation`; becomes the subcommand's context.
@@ -191,11 +192,12 @@ export function command<Context, Result>(
191
192
  * );
192
193
  * ```
193
194
  */
194
- export function commandWithSubcommands<Context, Payload, Result>(
195
+ export function commandWithSubcommands<Context, Payload, Result = void>(
195
196
  information: CommandInformation,
196
197
  operation: Operation<Context, Payload>,
197
198
  subcommands: { [subcommand: string]: Command<Payload, Result> },
198
199
  ): Command<Context, Result> {
200
+ // TODO - forbid subcommands that start with a "-" ?
199
201
  const subcommandNames = Object.keys(subcommands);
200
202
  if (subcommandNames.length === 0) {
201
203
  throw new Error("At least one subcommand is required");
@@ -209,30 +211,21 @@ export function commandWithSubcommands<Context, Payload, Result>(
209
211
  const operationDecoder = operation.consumeAndMakeDecoder(readerArgs);
210
212
  const subcommandName = readerArgs.consumePositional();
211
213
  if (subcommandName === undefined) {
212
- throw new TypoError(
213
- new TypoText(
214
- new TypoString(`<subcommand>`, typoStyleUserInput),
215
- new TypoString(`: Is required, but was not provided`),
216
- ),
217
- );
214
+ const errorText = new TypoText();
215
+ errorText.push(new TypoString(`<subcommand>`, typoStyleUserInput));
216
+ errorText.push(new TypoString(`: Missing argument.`));
217
+ suggestSubcommandNames(errorText, "", subcommandNames);
218
+ throw new TypoError(errorText);
218
219
  }
219
220
  const subcommandInput = subcommands[subcommandName];
220
221
  if (subcommandInput === undefined) {
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
+ const errorText = new TypoText();
223
+ errorText.push(new TypoString(`<subcommand>`, typoStyleUserInput));
224
+ errorText.push(new TypoString(`: Unknown name: `));
225
+ errorText.push(new TypoString(`"${subcommandName}"`, typoStyleQuote));
226
+ errorText.push(new TypoString(`.`));
227
+ suggestSubcommandNames(errorText, subcommandName, subcommandNames);
228
+ throw new TypoError(errorText);
236
229
  }
237
230
  const subcommandDecoder =
238
231
  subcommandInput.consumeAndMakeDecoder(readerArgs);
@@ -283,8 +276,8 @@ export function commandWithSubcommands<Context, Payload, Result>(
283
276
  }
284
277
 
285
278
  /**
286
- * Chains an {@link Operation} and a {@link Command}: `operation` runs first, its
287
- * output becomes `subcommand`'s context. No token is consumed for routing.
279
+ * Chains an {@link Operation} and a {@link Command}: `operation` runs first,
280
+ * its output becomes `subcommand`'s context. No token is consumed for routing.
288
281
  *
289
282
  * @typeParam Context - Context accepted by `operation`.
290
283
  * @typeParam Payload - Output of `operation`; becomes `subcommand`'s context.
@@ -295,7 +288,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
295
288
  * @param subcommand - Runs after `operation`.
296
289
  * @returns A {@link Command} composing both stages.
297
290
  */
298
- export function commandChained<Context, Payload, Result>(
291
+ export function commandChained<Context, Payload, Result = void>(
299
292
  information: CommandInformation,
300
293
  operation: Operation<Context, Payload>,
301
294
  subcommand: Command<Payload, Result>,
@@ -355,12 +348,25 @@ function generateUsageLeaf(
355
348
  ): UsageCommand {
356
349
  const { positionals, options } = operation.generateUsage();
357
350
  return {
358
- segments: positionals.map((positional) => ({
359
- positional: positional.label,
360
- })),
351
+ segments: positionals.map((p) => ({ positional: p.label })),
361
352
  information,
362
353
  positionals,
363
354
  subcommands: [],
364
355
  options,
365
356
  };
366
357
  }
358
+
359
+ function suggestSubcommandNames(
360
+ errorText: TypoText,
361
+ input: string,
362
+ subcommandNames: Array<string> = [],
363
+ ) {
364
+ suggestTextPushMessage(
365
+ errorText,
366
+ input,
367
+ subcommandNames.map((subcommandName) => ({
368
+ reference: subcommandName,
369
+ hint: new TypoString(subcommandName, typoStyleConstants),
370
+ })),
371
+ );
372
+ }
@@ -44,7 +44,7 @@ export type OperationDecoder<Context, Result> = {
44
44
  /**
45
45
  * Creates a ready-to-execute {@link OperationInterpreter}.
46
46
  *
47
- * @throws {@link TypoError} if parsing or decoding failed.
47
+ * @throws if parsing or decoding failed.
48
48
  */
49
49
  decodeAndMakeInterpreter(): OperationInterpreter<Context, Result>;
50
50
  };
@@ -99,46 +99,54 @@ export type OperationInterpreter<Context, Result> = {
99
99
  export function operation<
100
100
  Context,
101
101
  Result,
102
- Options extends { [option: string]: any },
103
- const Positionals extends Array<any>,
102
+ const Options extends { [option: string]: any } = {},
103
+ const Positionals extends Array<any> = [],
104
104
  >(
105
105
  inputs: {
106
- options: { [K in keyof Options]: Option<Options[K]> };
107
- positionals: { [K in keyof Positionals]: Positional<Positionals[K]> };
106
+ options?: { [K in keyof Options]: Option<Options[K]> };
107
+ positionals?: { [K in keyof Positionals]: Positional<Positionals[K]> };
108
108
  },
109
109
  handler: (
110
110
  context: Context,
111
111
  inputs: {
112
- options: Options;
113
- positionals: Positionals;
112
+ options: { [K in keyof Options]: Options[K] };
113
+ positionals: { [K in keyof Positionals]: Positionals[K] };
114
114
  },
115
115
  ) => Promise<Result>,
116
116
  ): Operation<Context, Result> {
117
117
  return {
118
118
  generateUsage() {
119
119
  const optionsUsage = new Array<UsageOption>();
120
- for (const optionKey in inputs.options) {
121
- const optionInput = inputs.options[optionKey]!;
122
- optionsUsage.push(optionInput.generateUsage());
120
+ if (inputs.options !== undefined) {
121
+ for (const optionKey in inputs.options) {
122
+ const optionInput = inputs.options[optionKey]!;
123
+ optionsUsage.push(optionInput.generateUsage());
124
+ }
123
125
  }
124
126
  const positionalsUsage = new Array<UsagePositional>();
125
- for (const positionalInput of inputs.positionals) {
126
- positionalsUsage.push(positionalInput.generateUsage());
127
+ if (inputs.positionals !== undefined) {
128
+ for (const positionalInput of inputs.positionals) {
129
+ positionalsUsage.push(positionalInput.generateUsage());
130
+ }
127
131
  }
128
132
  return { options: optionsUsage, positionals: positionalsUsage };
129
133
  },
130
134
  consumeAndMakeDecoder(readerArgs: ReaderArgs) {
131
135
  const optionsDecoders: Record<string, OptionDecoder<any>> = {};
132
- for (const optionKey in inputs.options) {
133
- const optionInput = inputs.options[optionKey]!;
134
- optionsDecoders[optionKey] =
135
- optionInput.registerAndMakeDecoder(readerArgs);
136
+ if (inputs.options !== undefined) {
137
+ for (const optionKey in inputs.options) {
138
+ const optionInput = inputs.options[optionKey]!;
139
+ optionsDecoders[optionKey] =
140
+ optionInput.registerAndMakeDecoder(readerArgs);
141
+ }
136
142
  }
137
143
  const positionalsDecoders: Array<PositionalDecoder<any>> = [];
138
- for (const positionalInput of inputs.positionals) {
139
- positionalsDecoders.push(
140
- positionalInput.consumeAndMakeDecoder(readerArgs),
141
- );
144
+ if (inputs.positionals !== undefined) {
145
+ for (const positionalInput of inputs.positionals) {
146
+ positionalsDecoders.push(
147
+ positionalInput.consumeAndMakeDecoder(readerArgs),
148
+ );
149
+ }
142
150
  }
143
151
  return {
144
152
  decodeAndMakeInterpreter() {