cli-kiss 0.1.9 → 0.2.1

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.
@@ -0,0 +1,41 @@
1
+ import { defineConfig } from "vitepress";
2
+
3
+ export default defineConfig({
4
+ description: "Full-featured TypeScript CLI builder. No bloat, no dependency.",
5
+ title: "cli-kiss 💋",
6
+ base: "/cli-kiss/",
7
+ head: [
8
+ [
9
+ "style",
10
+ {},
11
+ `
12
+ .VPDoc div[class*="language-"] code { font-size: 0.8em; line-height: 1.6; }
13
+ `,
14
+ ],
15
+ ],
16
+ themeConfig: {
17
+ nav: [
18
+ { text: "Guide", link: "/guide/01_getting_started" },
19
+ { text: "npm", link: "https://www.npmjs.com/package/cli-kiss" },
20
+ ],
21
+ sidebar: [
22
+ {
23
+ text: "Guide",
24
+ items: [
25
+ { text: "Getting Started", link: "/guide/01_getting_started" },
26
+ { text: "Commands", link: "/guide/02_commands" },
27
+ { text: "Options", link: "/guide/03_options" },
28
+ { text: "Positionals", link: "/guide/04_positionals" },
29
+ { text: "Types", link: "/guide/05_types" },
30
+ { text: "Running your CLI", link: "/guide/06_run" },
31
+ ],
32
+ },
33
+ ],
34
+ socialLinks: [
35
+ { icon: "github", link: "https://github.com/crypto-vincent/cli-kiss" },
36
+ ],
37
+ footer: {
38
+ message: "CLI: Keep It Simple, Stupid. (KISS)",
39
+ },
40
+ },
41
+ });
@@ -0,0 +1,116 @@
1
+ # Getting Started
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ npm install cli-kiss
7
+ ```
8
+
9
+ ## Your first CLI
10
+
11
+ Here is a minimal "greet" CLI that takes:
12
+
13
+ - a required `NAME` positional
14
+ - optional `--loud` flag:
15
+
16
+ ```ts
17
+ import {
18
+ command,
19
+ operation,
20
+ optionFlag,
21
+ positionalRequired,
22
+ runAndExit,
23
+ typeString,
24
+ } from "cli-kiss";
25
+
26
+ const greetCommand = command(
27
+ { description: "Greet someone" },
28
+ operation(
29
+ {
30
+ options: {
31
+ loud: optionFlag({ long: "loud", description: "Print in uppercase" }),
32
+ },
33
+ positionals: [
34
+ positionalRequired({
35
+ type: typeString,
36
+ label: "NAME",
37
+ description: "The name to greet",
38
+ }),
39
+ ],
40
+ },
41
+ async (_ctx, { options: { loud }, positionals: [name] }) => {
42
+ const message = `Hello, ${name}!`;
43
+ console.log(loud ? message.toUpperCase() : message);
44
+ },
45
+ ),
46
+ );
47
+
48
+ await runAndExit("greet", process.argv.slice(2), undefined, greetCommand, {
49
+ buildVersion: "1.0.0",
50
+ });
51
+ ```
52
+
53
+ Run it:
54
+
55
+ ```sh
56
+ $ greet Alice
57
+ ```
58
+
59
+ ```text
60
+ Hello, Alice!
61
+ ```
62
+
63
+ Pass some flags:
64
+
65
+ ```sh
66
+ $ greet --loud Alice
67
+ ```
68
+
69
+ ```text
70
+ HELLO, ALICE!
71
+ ```
72
+
73
+ Get some help (built-in)
74
+
75
+ ```sh
76
+ $ greet --help
77
+ ```
78
+
79
+ ```text
80
+ Usage: greet <NAME>
81
+
82
+ Greet someone
83
+
84
+ Positionals:
85
+ <NAME> The name to greet
86
+
87
+ Options:
88
+ --loud[=no] Print in uppercase
89
+ ```
90
+
91
+ Get the version (built-in)
92
+
93
+ ```sh
94
+ $ greet --version
95
+ ```
96
+
97
+ ```text
98
+ greet 1.0.0
99
+ ```
100
+
101
+ ## Project structure
102
+
103
+ A typical `cli-kiss` project looks like this:
104
+
105
+ ```
106
+ my-cli/
107
+ ├── src/
108
+ │ ├── index.ts ← entry point: calls runAndExit
109
+ │ └── commands/
110
+ │ ├── deploy.ts ← command(...) definitions
111
+ │ └── rollback.ts
112
+ └── package.json
113
+ ```
114
+
115
+ Each command lives in its own file and is composed together in the entry point.
116
+ See the [Commands](./02_commands) guide to learn how.
@@ -0,0 +1,157 @@
1
+ # Commands
2
+
3
+ Commands are the building blocks of a `cli-kiss` CLI.
4
+
5
+ Three factory functions cover every use-case.
6
+
7
+ ## `command` — leaf command
8
+
9
+ A leaf command has no subcommands. It directly runs an operation.
10
+
11
+ ```ts
12
+ import { command, operation, positionalRequired, typeString } from "cli-kiss";
13
+
14
+ const greet = command(
15
+ { description: "Greet a user" },
16
+ operation(
17
+ {
18
+ options: {},
19
+ positionals: [positionalRequired({ type: typeString, label: "NAME" })],
20
+ },
21
+ async (_ctx, { positionals: [name] }) => {
22
+ console.log(`Hello, ${name}!`);
23
+ },
24
+ ),
25
+ );
26
+ ```
27
+
28
+ ### `CommandInformation`
29
+
30
+ Every command accepts a metadata object:
31
+
32
+ | Field | Type | Description |
33
+ | ------------- | ----------- | ------------------------------------------------- |
34
+ | `description` | `string` | Short description shown in help output |
35
+ | `hint` | `string?` | Note shown in parentheses next to the description |
36
+ | `details` | `string[]?` | Extra lines printed below the description |
37
+
38
+ ```ts
39
+ command(
40
+ {
41
+ description: "Deploy the application",
42
+ hint: "experimental",
43
+ details: [
44
+ "Pushes to the configured remote.",
45
+ "Runs migrations after push.",
46
+ ],
47
+ },
48
+ deployOperation,
49
+ );
50
+ ```
51
+
52
+ ## `commandWithSubcommands` — dispatch to a subcommand
53
+
54
+ Use this when the user must pick one of several sub-actions.
55
+
56
+ ```ts
57
+ import {
58
+ command,
59
+ commandWithSubcommands,
60
+ operation,
61
+ runAndExit,
62
+ } from "cli-kiss";
63
+
64
+ const rootCmd = commandWithSubcommands(
65
+ { description: "My deployment CLI" },
66
+ // This operation runs before the subcommand is selected.
67
+ // Its return value becomes the subcommand's context.
68
+ operation({ options: {}, positionals: [] }, async (_ctx) => ({
69
+ db: "postgres://localhost/mydb",
70
+ })),
71
+ {
72
+ deploy: command(
73
+ { description: "Deploy the latest build" },
74
+ operation({ options: {}, positionals: [] }, async (ctx) => {
75
+ console.log(`Deploying with DB: ${ctx.db}`);
76
+ }),
77
+ ),
78
+ rollback: command(
79
+ { description: "Rollback to the previous release" },
80
+ operation({ options: {}, positionals: [] }, async (ctx) => {
81
+ console.log(`Rolling back, DB: ${ctx.db}`);
82
+ }),
83
+ ),
84
+ },
85
+ );
86
+
87
+ await runAndExit("deploy-cli", process.argv.slice(2), undefined, rootCmd);
88
+ ```
89
+
90
+ Check it:
91
+
92
+ ```sh
93
+ $ deploy-cli --help
94
+ ```
95
+
96
+ ```text
97
+ Usage: deploy-cli <SUBCOMMAND>
98
+
99
+ My deployment CLI
100
+
101
+ Subcommands:
102
+ deploy Deploy the latest build
103
+ rollback Rollback to the previous release
104
+ ```
105
+
106
+ ### Subcommand names
107
+
108
+ The keys of the subcommand map are the literal tokens users type. They must be
109
+ lowercase strings.
110
+
111
+ ## `commandChained` — sequential stages
112
+
113
+ Use this to split a command into reusable steps without introducing a
114
+ user-visible subcommand token.
115
+
116
+ ```ts
117
+ import {
118
+ command,
119
+ commandChained,
120
+ operation,
121
+ optionSingleValue,
122
+ typeString,
123
+ } from "cli-kiss";
124
+
125
+ const authenticatedDeploy = commandChained(
126
+ { description: "Authenticate then deploy" },
127
+ // Stage 1: parse a --token option and forward the token as context
128
+ operation(
129
+ {
130
+ options: {
131
+ token: optionSingleValue({
132
+ long: "token",
133
+ type: typeString,
134
+ description: "API token",
135
+ default: () => {
136
+ const t = process.env.API_TOKEN;
137
+ if (!t) throw new Error("API_TOKEN env var is required");
138
+ return t;
139
+ },
140
+ }),
141
+ },
142
+ positionals: [],
143
+ },
144
+ async (_ctx, { options: { token } }) => ({ token }),
145
+ ),
146
+ // Stage 2: receives { token } as context
147
+ command(
148
+ { description: "Deploy with auth token" },
149
+ operation({ options: {}, positionals: [] }, async ({ token }) => {
150
+ console.log(`Deploying with token: ${token}`);
151
+ }),
152
+ ),
153
+ );
154
+ ```
155
+
156
+ The two stages' options and positionals are merged into a single flat usage
157
+ output — the user sees one combined command.
@@ -0,0 +1,111 @@
1
+ # Options
2
+
3
+ Options are named arguments prefixed with `--` (or `-` for short forms). Declare
4
+ them in the `options` map of [`operation`](/guide/02_commands).
5
+
6
+ ## `optionFlag` — boolean toggle
7
+
8
+ A flag that is either present or absent. The user can also pass `--flag=yes` /
9
+ `--flag=no`.
10
+
11
+ ```ts
12
+ import { optionFlag } from "cli-kiss";
13
+
14
+ const verbose = optionFlag({
15
+ long: "verbose",
16
+ short: "v",
17
+ description: "Enable verbose output",
18
+ });
19
+ // --verbose → true
20
+ // --verbose=yes → true
21
+ // --verbose=no → false
22
+ // (absent) → false
23
+ ```
24
+
25
+ | Parameter | Type | Description |
26
+ | ------------- | --------------------- | -------------------------------------------- |
27
+ | `long` | `Lowercase<string>` | Long flag name (without `--`) |
28
+ | `short` | `string?` | Short flag name (without `-`) |
29
+ | `description` | `string?` | Help text |
30
+ | `hint` | `string?` | Short note in parentheses |
31
+ | `default` | `() => boolean` | Default when absent (default: `() => false`) |
32
+ | `aliases` | `{ longs?, shorts? }` | Additional names for the flag |
33
+
34
+ ::: tip A flag specified more than once triggers a parse error. Use
35
+ [`optionRepeatable`](#optionrepeatable-collect-multiple-values) if you need
36
+ multiple values.
37
+
38
+ :::
39
+
40
+ ## `optionSingleValue` — one typed value
41
+
42
+ Accepts exactly one value. Use any [`Type`](/guide/05_types) to decode it.
43
+
44
+ ```ts
45
+ import { optionSingleValue, typeString } from "cli-kiss";
46
+
47
+ const output = optionSingleValue({
48
+ long: "output",
49
+ short: "o",
50
+ type: typeString,
51
+ label: "PATH",
52
+ description: "Output directory",
53
+ default: () => "dist/",
54
+ });
55
+ // --output dist/ → "dist/"
56
+ // --output=dist/ → "dist/"
57
+ // -o dist/ → "dist/"
58
+ // (absent) → "dist/"
59
+ ```
60
+
61
+ | Parameter | Type | Description |
62
+ | ------------- | --------------------- | ----------------------------------------------------------- |
63
+ | `long` | `Lowercase<string>` | Long option name |
64
+ | `short` | `string?` | Short option name |
65
+ | `type` | `Type<Value>` | Decoder for the value |
66
+ | `label` | `Uppercase<string>?` | Placeholder in help (defaults to uppercased type content) |
67
+ | `description` | `string?` | Help text |
68
+ | `hint` | `string?` | Short note in parentheses |
69
+ | `default` | `() => Value` | Default when absent — **throw** to make the option required |
70
+ | `aliases` | `{ longs?, shorts? }` | Additional names |
71
+
72
+ ## `optionRepeatable` — collect multiple values
73
+
74
+ Collects every occurrence into an array. Safe to specify zero or many times.
75
+
76
+ ```ts
77
+ import { optionRepeatable, typeString } from "cli-kiss";
78
+
79
+ const files = optionRepeatable({
80
+ long: "file",
81
+ short: "f",
82
+ type: typeString,
83
+ label: "PATH",
84
+ description: "Input file (may be repeated)",
85
+ });
86
+ // --file a.ts --file b.ts → ["a.ts", "b.ts"]
87
+ // (absent) → []
88
+ ```
89
+
90
+ | Parameter | Type | Description |
91
+ | ------------- | --------------------- | ---------------------------------- |
92
+ | `long` | `Lowercase<string>` | Long option name |
93
+ | `short` | `string?` | Short option name |
94
+ | `type` | `Type<Value>` | Decoder applied to each occurrence |
95
+ | `label` | `Uppercase<string>?` | Placeholder in help |
96
+ | `description` | `string?` | Help text |
97
+ | `hint` | `string?` | Short note in parentheses |
98
+ | `aliases` | `{ longs?, shorts? }` | Additional names |
99
+
100
+ ## Aliases
101
+
102
+ All three option creators accept an `aliases` field for alternative names:
103
+
104
+ ```ts
105
+ optionFlag({
106
+ long: "dry-run",
107
+ aliases: { longs: ["dryrun"], shorts: ["n"] },
108
+ description: "Print actions without executing them",
109
+ });
110
+ // --dry-run, --dryrun, and -n all work
111
+ ```
@@ -0,0 +1,118 @@
1
+ # Positionals
2
+
3
+ Positionals are bare (non-option) arguments passed by position. Declare them in
4
+ order in the `positionals` array of [`operation`](/guide/02_commands).
5
+
6
+ ## `positionalRequired` — must be present
7
+
8
+ Fails with a parse error if the argument is missing.
9
+
10
+ ```ts
11
+ import { positionalRequired, typeString } from "cli-kiss";
12
+
13
+ const name = positionalRequired({
14
+ type: typeString,
15
+ label: "NAME",
16
+ description: "The name to greet",
17
+ });
18
+ // my-cli Alice → "Alice"
19
+ // my-cli → Error: <NAME>: Is required, but was not provided
20
+ ```
21
+
22
+ | Parameter | Type | Description |
23
+ | ------------- | -------------------- | --------------------------------------------------------- |
24
+ | `type` | `Type<Value>` | Decoder for the raw string token |
25
+ | `label` | `Uppercase<string>?` | Placeholder in help (defaults to uppercased type content) |
26
+ | `description` | `string?` | Help text |
27
+ | `hint` | `string?` | Short note in parentheses |
28
+
29
+ ## `positionalOptional` — may be absent
30
+
31
+ Falls back to a default value when the argument is not provided.
32
+
33
+ ```ts
34
+ import { positionalOptional, typeString } from "cli-kiss";
35
+
36
+ const greeting = positionalOptional({
37
+ type: typeString,
38
+ label: "GREETING",
39
+ description: "Custom greeting (default: Hello)",
40
+ default: () => "Hello",
41
+ });
42
+ // my-cli → "Hello"
43
+ // my-cli Howdy → "Howdy"
44
+ ```
45
+
46
+ | Parameter | Type | Description |
47
+ | ------------- | -------------------- | ------------------------------------------------- |
48
+ | `type` | `Type<Value>` | Decoder for the raw string token |
49
+ | `label` | `Uppercase<string>?` | Placeholder in help |
50
+ | `description` | `string?` | Help text |
51
+ | `hint` | `string?` | Short note in parentheses |
52
+ | `default` | `() => Value` | Value when absent — **throw** to make it required |
53
+
54
+ ## `positionalVariadics` — zero or more
55
+
56
+ Greedily consumes all remaining positional tokens into an array.
57
+
58
+ ```ts
59
+ import { positionalVariadics, typeString } from "cli-kiss";
60
+
61
+ const files = positionalVariadics({
62
+ type: typeString,
63
+ label: "FILE",
64
+ description: "Files to process",
65
+ });
66
+ // my-cli a.ts b.ts c.ts → ["a.ts", "b.ts", "c.ts"]
67
+ // my-cli → []
68
+ ```
69
+
70
+ ### End delimiter
71
+
72
+ Optionally stop collecting at a specific sentinel token:
73
+
74
+ ```ts
75
+ const args = positionalVariadics({
76
+ type: typeString,
77
+ label: "ARG",
78
+ endDelimiter: "STOP",
79
+ description: "Arguments (end with STOP)",
80
+ });
81
+ // my-cli foo bar STOP ignored → ["foo", "bar"]
82
+ ```
83
+
84
+ | Parameter | Type | Description |
85
+ | -------------- | -------------------- | ------------------------------------ |
86
+ | `type` | `Type<Value>` | Decoder applied to each token |
87
+ | `label` | `Uppercase<string>?` | Placeholder in help |
88
+ | `description` | `string?` | Help text |
89
+ | `hint` | `string?` | Short note in parentheses |
90
+ | `endDelimiter` | `string?` | Sentinel token that stops collection |
91
+
92
+ ## Ordering rules
93
+
94
+ Positionals are consumed **in declaration order**. Required positionals should
95
+ come first; variadics should be last.
96
+
97
+ ```ts
98
+ operation(
99
+ {
100
+ options: {},
101
+ positionals: [
102
+ positionalRequired({ type: typeString, label: "SOURCE" }),
103
+ positionalRequired({ type: typeString, label: "DEST" }),
104
+ positionalOptional({
105
+ type: typeString,
106
+ label: "TAG",
107
+ default: () => "latest",
108
+ }),
109
+ positionalVariadics({ type: typeString, label: "EXTRA" }),
110
+ ],
111
+ },
112
+ async (_ctx, { positionals: [source, dest, tag, extras] }) => {
113
+ /* ... */
114
+ },
115
+ );
116
+ // my-cli src/ dst/ → source="src/", dest="dst/", tag="latest", extras=[]
117
+ // my-cli src/ dst/ v2 a b c → source="src/", dest="dst/", tag="v2", extras=["a","b","c"]
118
+ ```
@@ -0,0 +1,134 @@
1
+ # Types
2
+
3
+ A `Type<Value>` is a pair of a human-readable `content` string and a `decoder`
4
+ function. It tells cli-kiss how to convert a raw CLI string into a typed value.
5
+
6
+ ## Built-in types
7
+
8
+ | Export | TypeScript type | Accepts |
9
+ | ------------- | --------------- | ---------------------------------------------------------- |
10
+ | `typeString` | `string` | Any string |
11
+ | `typeBoolean` | `boolean` | `true`, `yes`, `false`, `no` (case-insensitive) |
12
+ | `typeNumber` | `number` | Integers, floats, scientific notation |
13
+ | `typeInteger` | `bigint` | Integer strings only |
14
+ | `typeDate` | `Date` | Any format accepted by `Date.parse` (ISO 8601 recommended) |
15
+ | `typeUrl` | `URL` | Absolute URLs |
16
+
17
+ ```ts
18
+ import {
19
+ typeBoolean,
20
+ typeDate,
21
+ typeInteger,
22
+ typeNumber,
23
+ typeString,
24
+ typeUrl,
25
+ } from "cli-kiss";
26
+
27
+ typeString.decoder("hello"); // → "hello"
28
+ typeBoolean.decoder("yes"); // → true
29
+ typeNumber.decoder("3.14"); // → 3.14
30
+ typeInteger.decoder("9007199254740993"); // → 9007199254740993n
31
+ typeDate.decoder("2024-01-15"); // → Date object
32
+ typeUrl.decoder("https://example.com/path"); // → URL object
33
+ ```
34
+
35
+ ## `typeOneOf` — string enum
36
+
37
+ Accept only a fixed set of strings:
38
+
39
+ ```ts
40
+ import { typeOneOf } from "cli-kiss";
41
+
42
+ const typeEnv = typeOneOf("Environment", ["dev", "staging", "prod"]);
43
+
44
+ typeEnv.decoder("prod"); // → "prod"
45
+ typeEnv.decoder("unknown");
46
+ // Error: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
47
+ ```
48
+
49
+ ## `typeConverted` — transform an existing type
50
+
51
+ Chain a `before` type with an `after` transformation:
52
+
53
+ ```ts
54
+ import { typeConverted, typeNumber } from "cli-kiss";
55
+
56
+ const typePort = typeConverted(typeNumber, {
57
+ content: "Port",
58
+ decoder: (n) => {
59
+ if (n < 1 || n > 65535) throw new Error("Out of range");
60
+ return n;
61
+ },
62
+ });
63
+ // "--port 8080" → 8080
64
+ // "--port 99999" → Error: --port: <PORT>: Port: Out of range
65
+ ```
66
+
67
+ Errors from the `before` decoder are automatically prefixed with
68
+ `from: <content>` for easy debugging.
69
+
70
+ ## `typeTuple` — fixed-length delimited value
71
+
72
+ Split a single string into a fixed-length typed tuple:
73
+
74
+ ```ts
75
+ import { typeTuple, typeNumber } from "cli-kiss";
76
+
77
+ const typePoint = typeTuple([typeNumber, typeNumber]);
78
+
79
+ typePoint.decoder("3.14,2.71"); // → [3.14, 2.71]
80
+ typePoint.decoder("x,2"); // → Error: at 0: Number: Unable to parse: "x"
81
+ ```
82
+
83
+ The default separator is `","`. Pass a second argument to change it:
84
+
85
+ ```ts
86
+ typeTuple([typeString, typeNumber], ":");
87
+ // "foo:42" → ["foo", 42]
88
+ ```
89
+
90
+ ## `typeList` — variable-length delimited value
91
+
92
+ Split a single string into an array of homogeneous values:
93
+
94
+ ```ts
95
+ import { typeList, typeNumber } from "cli-kiss";
96
+
97
+ const typeNumbers = typeList(typeNumber);
98
+
99
+ typeNumbers.decoder("1,2,3"); // → [1, 2, 3]
100
+ typeNumbers.decoder("1,x,3"); // → Error: at 1: Number: Unable to parse: "x"
101
+ ```
102
+
103
+ Custom separator:
104
+
105
+ ```ts
106
+ const typePaths = typeList(typeString, ":");
107
+ typePaths.decoder("/usr/bin:/usr/local/bin"); // → ["/usr/bin", "/usr/local/bin"]
108
+ ```
109
+
110
+ ::: tip Prefer
111
+ [`optionRepeatable`](/guide/03_options#optionrepeatable-collect-multiple-values)
112
+ over `typeList` when users should pass multiple values as separate flags
113
+ (`--file a --file b` rather than `--files a,b`).
114
+
115
+ :::
116
+
117
+ ## Custom types
118
+
119
+ Implement the `Type<Value>` interface directly:
120
+
121
+ ```ts
122
+ import type { Type } from "cli-kiss";
123
+
124
+ const typeHexColor: Type<string> = {
125
+ content: "HexColor",
126
+ decoder(value) {
127
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) return value;
128
+ throw new Error(`Not a valid value: "${value}"`);
129
+ },
130
+ };
131
+
132
+ // "--color #ff0000" → "#ff0000"
133
+ // "--color red" → Error: --color: <HEXCOLOR>: HexColor: Not a valid value: "red"
134
+ ```