cli-kiss 0.2.5 → 0.2.6

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
  }
@@ -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
 
@@ -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.6",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "devDependencies": {
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
+ defaultWhenNotDefined: () => "auto",
80
+ defaultWhenNotInlined: () => "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":
package/src/lib/Type.ts CHANGED
@@ -51,23 +51,19 @@ export function typeBoolean(name?: string): Type<boolean> {
51
51
  content: name ?? "boolean",
52
52
  decoder(input: string) {
53
53
  const lower = input.toLowerCase();
54
- if (booleanValuesTrue.has(lower)) {
54
+ if (typeBooleanValuesTrue.has(lower)) {
55
55
  return true;
56
56
  }
57
- if (booleanValuesFalse.has(lower)) {
57
+ if (typeBooleanValuesFalse.has(lower)) {
58
58
  return false;
59
59
  }
60
- throw new TypoError(
61
- new TypoText(
62
- new TypoString(`Not a boolean: `),
63
- new TypoString(`"${input}"`, typoStyleQuote),
64
- ),
65
- );
60
+ throwInvalidValue("a boolean", input);
66
61
  },
67
62
  };
68
63
  }
69
- const booleanValuesTrue = new Set(["true", "yes", "on", "1", "y", "t"]);
70
- const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
64
+
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
- throw new TypoError(
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 TypoError
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
- throw new TypoError(
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 TypoError
145
- * typeInteger("my-integer").decoder("abc") // throws TypoError
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
- throw new TypoError(
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 TypoError
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
- throw new TypoError(
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
- const stats = statSync(input);
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 'file' but found '${preview}': `),
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 'directory' but found '${preview}': `),
305
+ new TypoString(`Expected a directory but found: ${preview}: `),
317
306
  new TypoString(`"${input}"`, typoStyleQuote),
318
307
  ),
319
308
  );
@@ -474,3 +463,12 @@ export function typeList<Value>(
474
463
  },
475
464
  };
476
465
  }
466
+
467
+ function throwInvalidValue(kind: string, input: string): never {
468
+ throw new TypoError(
469
+ new TypoText(
470
+ new TypoString(`Not ${kind}: `),
471
+ new TypoString(`"${input}"`, typoStyleQuote),
472
+ ),
473
+ );
474
+ }
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).
@@ -358,27 +360,38 @@ export class TypoSupport {
358
360
  * Auto-detects styling mode from the process environment on best-effort basis.
359
361
  */
360
362
  static inferFromEnv(): TypoSupport {
361
- if (!process || !process.env) {
363
+ /*
364
+ console.warn({
365
+ no: readEnvVar("NO_COLOR"),
366
+ force: readEnvVar("FORCE_COLOR"),
367
+ mock: readEnvVar("MOCK_COLOR"),
368
+ term: readEnvVar("TERM"),
369
+ tty: process.stdout.isTTY,
370
+ });
371
+ */
372
+ if (!process || !process.env || !process.stdout) {
362
373
  return TypoSupport.none();
363
374
  }
364
- function readEnvVar(name: string) {
365
- if (!(name in process.env)) {
366
- return undefined;
367
- }
368
- return process.env[name];
375
+ if (readEnvVar("NO_COLOR")) {
376
+ return TypoSupport.none();
369
377
  }
370
378
  const envForceColor = readEnvVar("FORCE_COLOR");
371
379
  if (envForceColor === "0") {
372
380
  return TypoSupport.none();
373
381
  }
374
382
  if (envForceColor !== undefined) {
375
- TypoSupport.tty();
383
+ if (!typeBooleanValuesFalse.has(envForceColor.toLowerCase())) {
384
+ return TypoSupport.tty();
385
+ }
386
+ }
387
+ if (readEnvVar("MOCK_COLOR")) {
388
+ return TypoSupport.mock();
376
389
  }
377
- if (readEnvVar("NO_COLOR") !== undefined) {
390
+ if (!process.stdout.isTTY) {
378
391
  return TypoSupport.none();
379
392
  }
380
- if (readEnvVar("MOCK_COLOR") !== undefined) {
381
- return TypoSupport.mock();
393
+ if (readEnvVar("TERM")?.toLowerCase() === "dumb") {
394
+ return TypoSupport.none();
382
395
  }
383
396
  return TypoSupport.tty();
384
397
  }
@@ -496,3 +509,10 @@ const ttyCodeBgColors: Record<TypoColor, string> = {
496
509
  brightCyan: "\x1b[106m",
497
510
  brightWhite: "\x1b[107m",
498
511
  };
512
+
513
+ function readEnvVar(name: string) {
514
+ if (!(name in process.env)) {
515
+ return undefined;
516
+ }
517
+ return process.env[name];
518
+ }
package/src/lib/Usage.ts CHANGED
@@ -116,16 +116,16 @@ export type UsageOption = {
116
116
  * <detail lines...>
117
117
  *
118
118
  * Positionals:
119
- * <LABEL> <description> (<hint>)
119
+ * <label> <description> (<hint>)
120
120
  *
121
121
  * Subcommands:
122
122
  * <name> <description> (<hint>)
123
123
  *
124
124
  * Options:
125
- * -s, --long <LABEL><annotation> <description> (<hint>)
125
+ * -s, --long <label><annotation> <description> (<hint>)
126
126
  *
127
127
  * Examples:
128
- * <description>
128
+ * <explanation>
129
129
  * <command line>
130
130
  *
131
131
  * ```