cli-kiss 0.2.4 → 0.2.5

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.
@@ -8,24 +8,24 @@
8
8
  await runAndExit(cliName, cliArgs, context, command, options?);
9
9
  ```
10
10
 
11
- | Parameter | Type | Description |
12
- | --------- | ---------------------------------- | ------------------------------------------------- |
13
- | `cliName` | `Lowercase<string>` | Program name used in help and `--version` output |
14
- | `cliArgs` | `ReadonlyArray<string>` | Raw arguments — typically `process.argv.slice(2)` |
15
- | `context` | `Context` | Value forwarded to every command handler |
16
- | `command` | `Command<Context, void>` | The root command |
17
- | `options` | `object?` | See below |
11
+ | Parameter | Type | Description |
12
+ | --------- | ------------------------ | ------------------------------------------------- |
13
+ | `cliName` | `string` | Program name used in help and `--version` output |
14
+ | `cliArgs` | `ReadonlyArray<string>` | Raw arguments — typically `process.argv.slice(2)` |
15
+ | `context` | `Context` | Value forwarded to every command handler |
16
+ | `command` | `Command<Context, void>` | The root command |
17
+ | `options` | `object?` | See below |
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
- | `useTtyColors` | `boolean \| "mock"?` | auto | Controls ANSI color output |
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"\|"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 |
29
29
 
30
30
  ### Exit codes
31
31
 
@@ -37,18 +37,6 @@ await runAndExit(cliName, cliArgs, context, command, options?);
37
37
  ## Full example
38
38
 
39
39
  ```ts
40
- import {
41
- command,
42
- commandWithSubcommands,
43
- operation,
44
- optionFlag,
45
- optionSingleValue,
46
- positionalRequired,
47
- runAndExit,
48
- typeString,
49
- typeUrl,
50
- } from "cli-kiss";
51
-
52
40
  type Ctx = { db: string };
53
41
 
54
42
  const deployCmd = command(
@@ -77,9 +65,9 @@ const rootCmd = commandWithSubcommands(
77
65
  options: {
78
66
  dbUrl: optionSingleValue({
79
67
  long: "db",
80
- type: typeUrl,
68
+ type: typeUrl(),
81
69
  description: "Database URL",
82
- default: () => new URL("postgres://localhost/mydb"),
70
+ defaultWhenNotDefined: () => new URL("postgres://localhost/mydb"),
83
71
  }),
84
72
  },
85
73
  positionals: [],
@@ -103,7 +91,7 @@ my-cli --help
103
91
  ```
104
92
 
105
93
  ```text
106
- Usage: my-cli <SUBCOMMAND>
94
+ Usage: my-cli <subcommand>
107
95
 
108
96
  My deployment CLI
109
97
 
@@ -111,7 +99,7 @@ Subcommands:
111
99
  deploy Deploy to production
112
100
 
113
101
  Options:
114
- --db <URL> Database URL
102
+ --db <url> Database URL
115
103
  ```
116
104
 
117
105
  Try it
@@ -126,17 +114,21 @@ my-cli deploy --dry-run
126
114
 
127
115
  ## Color control
128
116
 
129
- Colors are auto-detected. Override:
117
+ Colors are auto-detected by default (`colorSetup: "flag"` adds a `--color`
118
+ option). Override:
130
119
 
131
120
  ```ts
132
121
  // Force colors on
133
- await runAndExit("my-cli", args, ctx, cmd, { useTtyColors: true });
122
+ await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "always" });
134
123
 
135
124
  // Force colors off (useful in CI)
136
- await runAndExit("my-cli", args, ctx, cmd, { useTtyColors: false });
125
+ 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" });
137
129
 
138
130
  // Deterministic mock output (useful in snapshot tests)
139
- await runAndExit("my-cli", args, ctx, cmd, { useTtyColors: "mock" });
131
+ await runAndExit("my-cli", args, ctx, cmd, { colorSetup: "mock" });
140
132
  ```
141
133
 
142
134
  ## Testing your CLI
@@ -144,17 +136,13 @@ await runAndExit("my-cli", args, ctx, cmd, { useTtyColors: "mock" });
144
136
  Override `onExit` to prevent process exit during tests:
145
137
 
146
138
  ```ts
147
- import { runAndExit } from "cli-kiss";
148
-
149
139
  const exitCodes: number[] = [];
150
-
151
140
  await runAndExit("my-cli", ["--help"], undefined, myCommand, {
152
- useTtyColors: false,
141
+ colorSetup: "never",
153
142
  onExit: (code) => {
154
143
  exitCodes.push(code);
155
144
  return undefined as never;
156
145
  },
157
146
  });
158
-
159
147
  console.assert(exitCodes[0] === 0, "expected exit 0 for --help");
160
148
  ```
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-kiss",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "devDependencies": {
package/src/index.ts CHANGED
@@ -7,5 +7,3 @@ export * from "./lib/Run";
7
7
  export * from "./lib/Type";
8
8
  export * from "./lib/Typo";
9
9
  export * from "./lib/Usage";
10
-
11
- // TODO - maybe add a parsed option recap on error
@@ -7,7 +7,7 @@ import {
7
7
  typoStyleUserInput,
8
8
  TypoText,
9
9
  } from "./Typo";
10
- import { UsageCommand, UsageSegment } from "./Usage";
10
+ import { UsageCommand } from "./Usage";
11
11
 
12
12
  /**
13
13
  * A CLI command. Created with {@link command}, {@link commandWithSubcommands}, or {@link commandChained}.
@@ -93,8 +93,8 @@ export type CommandInformation = {
93
93
  | { subcommand: string }
94
94
  | {
95
95
  option:
96
- | { long: string; value?: string }
97
- | { short: string; value?: string };
96
+ | { long: string; inlined?: string; separated?: Array<string> }
97
+ | { short: string; inlined?: string; separated?: Array<string> };
98
98
  }
99
99
  >;
100
100
  }>;
@@ -115,7 +115,7 @@ export type CommandInformation = {
115
115
  * const greet = command(
116
116
  * { description: "Greet a user" },
117
117
  * operation(
118
- * { options: {}, positionals: [positionalRequired({ type: typeString, label: "NAME" })] },
118
+ * { options: {}, positionals: [positionalRequired({ type: type("name") })] },
119
119
  * async (_ctx, { positionals: [name] }) => console.log(`Hello, ${name}!`),
120
120
  * ),
121
121
  * );
@@ -192,7 +192,7 @@ export function command<Context, Result>(
192
192
  export function commandWithSubcommands<Context, Payload, Result>(
193
193
  information: CommandInformation,
194
194
  operation: Operation<Context, Payload>,
195
- subcommands: { [subcommand: Lowercase<string>]: Command<Payload, Result> },
195
+ subcommands: { [subcommand: string]: Command<Payload, Result> },
196
196
  ): Command<Context, Result> {
197
197
  return {
198
198
  getInformation() {
@@ -205,17 +205,16 @@ export function commandWithSubcommands<Context, Payload, Result>(
205
205
  if (subcommandName === undefined) {
206
206
  throw new TypoError(
207
207
  new TypoText(
208
- new TypoString(`<SUBCOMMAND>`, typoStyleUserInput),
208
+ new TypoString(`<subcommand>`, typoStyleUserInput),
209
209
  new TypoString(`: Is required, but was not provided`),
210
210
  ),
211
211
  );
212
212
  }
213
- const subcommandInput =
214
- subcommands[subcommandName as Lowercase<string>];
213
+ const subcommandInput = subcommands[subcommandName];
215
214
  if (subcommandInput === undefined) {
216
215
  throw new TypoError(
217
216
  new TypoText(
218
- new TypoString(`<SUBCOMMAND>`, typoStyleUserInput),
217
+ new TypoString(`<subcommand>`, typoStyleUserInput),
219
218
  new TypoString(`: Invalid value: `),
220
219
  new TypoString(`"${subcommandName}"`, typoStyleQuote),
221
220
  ),
@@ -227,7 +226,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
227
226
  generateUsage() {
228
227
  const subcommandUsage = subcommandDecoder.generateUsage();
229
228
  const currentUsage = generateUsageLeaf(information, operation);
230
- currentUsage.segments.push(segmentSubcommand(subcommandName));
229
+ currentUsage.segments.push({ subcommand: subcommandName });
231
230
  currentUsage.segments.push(...subcommandUsage.segments);
232
231
  currentUsage.information = subcommandUsage.information;
233
232
  currentUsage.positionals.push(...subcommandUsage.positionals);
@@ -253,7 +252,7 @@ export function commandWithSubcommands<Context, Payload, Result>(
253
252
  return {
254
253
  generateUsage() {
255
254
  const currentUsage = generateUsageLeaf(information, operation);
256
- currentUsage.segments.push(segmentPositional("<SUBCOMMAND>"));
255
+ currentUsage.segments.push({ positional: "<subcommand>" });
257
256
  for (const [name, subcommand] of Object.entries(subcommands)) {
258
257
  const { description, hint } = subcommand.getInformation();
259
258
  currentUsage.subcommands.push({ name, description, hint });
@@ -281,18 +280,6 @@ export function commandWithSubcommands<Context, Payload, Result>(
281
280
  * @param operation - Runs first; output becomes `subcommand`'s context.
282
281
  * @param subcommand - Runs after `operation`.
283
282
  * @returns A {@link Command} composing both stages.
284
- *
285
- * @example
286
- * ```ts
287
- * const authenticatedDeploy = commandChained(
288
- * { description: "Authenticate then deploy" },
289
- * operation(
290
- * { options: { token: optionSingleValue({ long: "token", type: typeString, default: () => "" }) }, positionals: [] },
291
- * async (_ctx, { options: { token } }) => ({ token }),
292
- * ),
293
- * command({ description: "Deploy" }, deployOperation),
294
- * );
295
- * ```
296
283
  */
297
284
  export function commandChained<Context, Payload, Result>(
298
285
  information: CommandInformation,
@@ -336,7 +323,7 @@ export function commandChained<Context, Payload, Result>(
336
323
  return {
337
324
  generateUsage() {
338
325
  const currentUsage = generateUsageLeaf(information, operation);
339
- currentUsage.segments.push(segmentPositional("[REST]..."));
326
+ currentUsage.segments.push({ positional: "[REST]..." });
340
327
  return currentUsage;
341
328
  },
342
329
  decodeAndMakeInterpreter() {
@@ -348,23 +335,15 @@ export function commandChained<Context, Payload, Result>(
348
335
  };
349
336
  }
350
337
 
351
- function segmentPositional(value: string): UsageSegment {
352
- return { positional: value };
353
- }
354
-
355
- function segmentSubcommand(value: string): UsageSegment {
356
- return { subcommand: value };
357
- }
358
-
359
338
  function generateUsageLeaf(
360
339
  information: CommandInformation,
361
340
  operation: Operation<any, any>,
362
341
  ): UsageCommand {
363
342
  const { positionals, options } = operation.generateUsage();
364
343
  return {
365
- segments: positionals.map((positional) =>
366
- segmentPositional(positional.label),
367
- ),
344
+ segments: positionals.map((positional) => ({
345
+ positional: positional.label,
346
+ })),
368
347
  information,
369
348
  positionals,
370
349
  subcommands: [],
@@ -1,7 +1,7 @@
1
1
  import { Option, OptionDecoder } from "./Option";
2
2
  import { Positional, PositionalDecoder } from "./Positional";
3
3
  import { ReaderArgs } from "./Reader";
4
- import { UsageOperation, UsageOption, UsagePositional } from "./Usage";
4
+ import { UsageOption, UsagePositional } from "./Usage";
5
5
 
6
6
  /**
7
7
  * Options, positionals, and an async handler that together form the logic of a CLI command.
@@ -16,7 +16,16 @@ export type Operation<Context, Result> = {
16
16
  /**
17
17
  * Returns usage metadata without consuming any arguments.
18
18
  */
19
- generateUsage(): UsageOperation;
19
+ generateUsage(): {
20
+ /**
21
+ * Registered options.
22
+ */
23
+ options: Array<UsageOption>;
24
+ /**
25
+ * Declared positionals, in order.
26
+ */
27
+ positionals: Array<UsagePositional>;
28
+ };
20
29
  /**
21
30
  * Consumes args from `readerArgs` and returns an {@link OperationDecoder}.
22
31
  */
@@ -74,10 +83,10 @@ export type OperationInterpreter<Context, Result> = {
74
83
  * const greetOperation = operation(
75
84
  * {
76
85
  * options: {
77
- * loud: optionFlag({ long: "loud", description: "Print in uppercase" }),
86
+ * loud: optionFlag({ long: "loud", description: "Print in uppercase", default: false }),
78
87
  * },
79
88
  * positionals: [
80
- * positionalRequired({ type: typeString, label: "NAME", description: "Name to greet" }),
89
+ * positionalRequired({ type: type("name"), description: "Name to greet" }),
81
90
  * ],
82
91
  * },
83
92
  * async function (_ctx, { options: { loud }, positionals: [name] }) {