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.
- package/.github/workflows/docs.yml +35 -0
- package/README.md +12 -1
- package/dist/index.d.ts +22 -28
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +41 -0
- package/docs/guide/01_getting_started.md +116 -0
- package/docs/guide/02_commands.md +157 -0
- package/docs/guide/03_options.md +111 -0
- package/docs/guide/04_positionals.md +118 -0
- package/docs/guide/05_types.md +134 -0
- package/docs/guide/06_run.md +161 -0
- package/docs/index.md +30 -0
- package/package.json +6 -3
- package/src/index.ts +0 -1
- package/src/lib/Command.ts +6 -7
- package/src/lib/Operation.ts +2 -2
- package/src/lib/Reader.ts +1 -1
- package/src/lib/Run.ts +23 -29
- package/src/lib/Type.ts +1 -0
- package/src/lib/Typo.ts +2 -3
- package/src/lib/Usage.ts +2 -3
- package/tests/unit.runner.cycle.ts +6 -6
- package/tests/unit.runner.errors.ts +33 -33
|
@@ -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
|
+
```
|