@clo4/cli 2.0.0 → 3.0.0
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/README.md +957 -42
- package/dist/index.d.ts +69 -16
- package/dist/index.js +1077 -668
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +89 -30
- package/dist/testing.js +1218 -217
- package/dist/testing.js.map +1 -1
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -1,97 +1,1012 @@
|
|
|
1
|
-
#
|
|
1
|
+
# clo4's CLI SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@clo4/cli` is a TypeScript library for building command-line tools. Commands are plain objects that describe accepted arguments and options. Each command can have an action: a function that receives the parsed input.
|
|
4
|
+
|
|
5
|
+
Parsing, help and completion need to follow the same argument and option rules. The library reads those rules from the command definitions, with application logic in the action functions.
|
|
6
|
+
|
|
7
|
+
Startup time matters for commands that exit quickly and for shell completion, which starts the CLI on each request.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @clo4/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
[Example](#example) · [Getting started](#getting-started) · [Help and completion](#help-and-completion) · [Testing](#testing-your-spec) · [Parser settings](#parser-settings) · [Performance](#performance) · [API](#api-and-compatibility) · [License](#license)
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
This example defines `demo build`, with an input directory, an output option and a watch flag. The action prints the selected paths and whether watch mode was requested.
|
|
20
|
+
|
|
21
|
+
Save this as `demo.ts` in a project with `"type": "module"` in its `package.json`. Run the examples with Node.js 24 or later using its [built-in TypeScript support](https://nodejs.org/api/typescript.html#type-stripping):
|
|
4
22
|
|
|
5
23
|
```ts
|
|
6
24
|
import * as CLI from "@clo4/cli";
|
|
7
25
|
|
|
8
26
|
const spec: CLI.Spec = {
|
|
9
27
|
name: "demo",
|
|
10
|
-
description: "
|
|
28
|
+
description: "A small build tool.",
|
|
11
29
|
options: [CLI.help],
|
|
12
30
|
subcommands: [
|
|
13
31
|
{
|
|
14
32
|
name: "build",
|
|
15
33
|
description: "Build a project.",
|
|
16
|
-
args: {
|
|
34
|
+
args: {
|
|
35
|
+
name: "input",
|
|
36
|
+
description: "Source directory.",
|
|
37
|
+
isOptional: true,
|
|
38
|
+
default: ".",
|
|
39
|
+
template: "folders",
|
|
40
|
+
},
|
|
17
41
|
options: [
|
|
18
|
-
{
|
|
42
|
+
{
|
|
43
|
+
name: ["-w", "--watch"],
|
|
44
|
+
description: "Watch for changes.",
|
|
45
|
+
},
|
|
19
46
|
{
|
|
20
47
|
name: ["-o", "--output"],
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
description: "Output directory.",
|
|
49
|
+
args: {
|
|
50
|
+
name: "dir",
|
|
51
|
+
default: "dist",
|
|
52
|
+
template: "folders",
|
|
53
|
+
},
|
|
24
54
|
},
|
|
25
55
|
],
|
|
26
56
|
action({ args: [input], options }) {
|
|
27
57
|
console.log(`${input} -> ${options.get("--output")}`);
|
|
58
|
+
if (options.has("--watch")) {
|
|
59
|
+
console.log("Watch mode enabled.");
|
|
60
|
+
}
|
|
28
61
|
},
|
|
29
62
|
},
|
|
63
|
+
CLI.helpCommand,
|
|
64
|
+
CLI.completion,
|
|
30
65
|
],
|
|
31
66
|
};
|
|
32
67
|
|
|
33
68
|
process.exitCode = await CLI.run(spec);
|
|
34
69
|
```
|
|
35
70
|
|
|
36
|
-
|
|
71
|
+
`build` defaults to `.` for input and `dist` for output:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
$ node demo.ts build
|
|
75
|
+
. -> dist
|
|
76
|
+
$ node demo.ts build src -o public --watch
|
|
77
|
+
src -> public
|
|
78
|
+
Watch mode enabled.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In the remaining command examples, `demo` stands for `node demo.ts`.
|
|
82
|
+
|
|
83
|
+
Adding `CLI.help` to the root command's `options` makes `-h` and `--help` available on all its subcommands. The generated help lists the command's arguments and options:
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
$ demo build --help
|
|
87
|
+
Build a project.
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
demo build [flags] [input]
|
|
91
|
+
|
|
92
|
+
Arguments:
|
|
93
|
+
input Source directory. (default: .)
|
|
94
|
+
|
|
95
|
+
Flags:
|
|
96
|
+
-w, --watch Watch for changes.
|
|
97
|
+
-o, --output <dir> Output directory. (default: dist)
|
|
98
|
+
|
|
99
|
+
Global flags:
|
|
100
|
+
-h, --help Print a help message
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Running `demo` without a subcommand displays the CLI's help text, including the available commands and global options. This is the default behavior for a command without an `action`.
|
|
104
|
+
|
|
105
|
+
`demo help build` displays help for the `build` subcommand. `demo completion <shell>` prints a completion script for the specified shell.
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
<summary>Command-line errors</summary>
|
|
109
|
+
<br>
|
|
110
|
+
|
|
111
|
+
When parsing fails, `CLI.run` writes an error message and usage to stderr and returns exit code 1. The example assigns that result to `process.exitCode`. For misspelled commands or options, the output may suggest a matching name:
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
$ demo bild
|
|
115
|
+
Error: Unknown command 'bild'
|
|
116
|
+
|
|
117
|
+
Did you mean 'build'?
|
|
118
|
+
|
|
119
|
+
Usage:
|
|
120
|
+
demo [flags]
|
|
121
|
+
|
|
122
|
+
Commands:
|
|
123
|
+
build Build a project.
|
|
124
|
+
help Print a help message
|
|
125
|
+
completion Print a shell completion script
|
|
126
|
+
|
|
127
|
+
Global flags:
|
|
128
|
+
-h, --help Print a help message
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
<hr>
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
[↑ Contents](#contents) · [Getting started →](#getting-started)
|
|
135
|
+
|
|
136
|
+
## Getting started
|
|
137
|
+
|
|
138
|
+
A **spec** is a plain object describing your CLI. The top-level (root) command has type `CLI.Spec`. Entries in a command's `subcommands` array have type `CLI.Subcommand`.
|
|
37
139
|
|
|
38
|
-
|
|
140
|
+
For a command line like `demo build src --output dist --watch`:
|
|
39
141
|
|
|
40
|
-
|
|
142
|
+
- `build` selects the command to run.
|
|
143
|
+
- `args` describes positional arguments: here, `src` is the input directory.
|
|
144
|
+
- `options` describes named inputs: `--output` takes the value `dist`, and `--watch` is a flag with no value.
|
|
145
|
+
- The `build` command's `action` receives these parsed arguments and options and runs after parsing succeeds.
|
|
41
146
|
|
|
42
|
-
|
|
147
|
+
In the spec, `args` and `options` define what a command accepts. Inside an action, `args` is an array of positional values, and `options` has methods to read the named inputs. `CLI.run` reads the command line, validates argument counts and option rules, and calls the selected action.
|
|
43
148
|
|
|
44
|
-
|
|
149
|
+
The flag example below is a complete `demo.ts`. Subsequent examples replace its `build` definition. As in the opening example, `demo` stands for `node demo.ts` in command examples.
|
|
150
|
+
|
|
151
|
+
### Flags
|
|
152
|
+
|
|
153
|
+
A flag is an option that takes no value. Add it to the command's `options` array, then use `options.has()` in the action to check whether it was given.
|
|
45
154
|
|
|
46
155
|
```ts
|
|
156
|
+
import * as CLI from "@clo4/cli";
|
|
157
|
+
|
|
158
|
+
const build: CLI.Subcommand = {
|
|
159
|
+
name: "build",
|
|
160
|
+
options: [
|
|
161
|
+
{
|
|
162
|
+
name: "--debug",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
action({ options }) {
|
|
166
|
+
console.log(options.has("--debug"));
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
47
170
|
const spec: CLI.Spec = {
|
|
48
171
|
name: "demo",
|
|
49
|
-
|
|
50
|
-
|
|
172
|
+
subcommands: [build],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
process.exitCode = await CLI.run(spec);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```console
|
|
179
|
+
$ demo build --debug
|
|
180
|
+
true
|
|
181
|
+
$ demo build
|
|
182
|
+
false
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
<br>
|
|
186
|
+
|
|
187
|
+
### Names
|
|
188
|
+
|
|
189
|
+
An option's `name` accepts a string or an array of aliases. This replacement for `build` accepts both `-v` and `--verbose`; keep the import, root spec and `CLI.run` call from the preceding example:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const build: CLI.Subcommand = {
|
|
193
|
+
name: "build",
|
|
194
|
+
options: [
|
|
195
|
+
{
|
|
196
|
+
name: ["-v", "--verbose"],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
action({ options }) {
|
|
200
|
+
console.log(options.has("--verbose"));
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```console
|
|
206
|
+
$ demo build -v
|
|
207
|
+
true
|
|
208
|
+
$ demo build --verbose
|
|
209
|
+
true
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Include the prefix in each name, such as `-v` or `--verbose`. Names are case-sensitive. Both aliases work when reading an option: `options.has("-v")` and `options.has("--verbose")` return the same result.
|
|
213
|
+
|
|
214
|
+
Short flags can be grouped: `-abc` is equivalent to `-a -b -c` when all three are flags. An option's `description` appears beside its names in help.
|
|
215
|
+
|
|
216
|
+
<br>
|
|
217
|
+
|
|
218
|
+
### Option arguments
|
|
219
|
+
|
|
220
|
+
An option's `args` describes the value it accepts. Here, `file` labels that value in help. The action reads the supplied value with `options.get()`:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const build: CLI.Subcommand = {
|
|
224
|
+
name: "build",
|
|
225
|
+
options: [
|
|
226
|
+
{
|
|
227
|
+
name: ["-o", "--output"],
|
|
228
|
+
args: {
|
|
229
|
+
name: "file",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
action({ options }) {
|
|
234
|
+
console.log(options.get("--output"));
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```console
|
|
240
|
+
$ demo build -o out.js
|
|
241
|
+
out.js
|
|
242
|
+
$ demo build
|
|
243
|
+
undefined
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The parser accepts `-o out.js`, `-oout.js` and `--output=out.js`. Supplying `--output` without a value is an error. Setting `isRequired: true` on the option also makes omitting the option an error.
|
|
247
|
+
|
|
248
|
+
<br>
|
|
249
|
+
|
|
250
|
+
### Positional arguments
|
|
251
|
+
|
|
252
|
+
A command's `args` describes positional arguments: values supplied without an option name. It accepts one argument definition or an array. Arguments are required by default; `isOptional: true` allows an argument to be omitted. The action receives their values in order:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const build: CLI.Subcommand = {
|
|
256
|
+
name: "build",
|
|
257
|
+
args: [
|
|
258
|
+
{
|
|
259
|
+
name: "source",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "destination",
|
|
263
|
+
isOptional: true,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
action({ args: [source, destination] }) {
|
|
267
|
+
console.log(source, destination);
|
|
268
|
+
},
|
|
51
269
|
};
|
|
52
270
|
```
|
|
53
271
|
|
|
272
|
+
```console
|
|
273
|
+
$ demo build src dist
|
|
274
|
+
src dist
|
|
275
|
+
$ demo build src
|
|
276
|
+
src undefined
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Required arguments must come before optional arguments. Help shows this example as `<source> [destination]`.
|
|
280
|
+
|
|
281
|
+
The `--` separator makes all remaining words positional arguments, including words that match options or subcommands. For example, `demo build -- --help` passes `"--help"` as the source.
|
|
282
|
+
|
|
283
|
+
<br>
|
|
284
|
+
|
|
285
|
+
### Defaults
|
|
286
|
+
|
|
287
|
+
The remaining examples in Getting started show fields inside `build`. Replace its existing `args`, `options` and `action` with the fields shown. Examples that change the root spec identify it explicitly.
|
|
288
|
+
|
|
289
|
+
Defaults are set on argument definitions. The opening example gives its optional `input` argument the default `"."`. For an option with a required value, the default applies when the option is omitted:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
options: [
|
|
293
|
+
{
|
|
294
|
+
name: "--port",
|
|
295
|
+
args: {
|
|
296
|
+
name: "port",
|
|
297
|
+
default: "8080",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
action({ options }) {
|
|
302
|
+
console.log(options.get("--port"));
|
|
303
|
+
},
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```console
|
|
307
|
+
$ demo build
|
|
308
|
+
8080
|
|
309
|
+
$ demo build --port 3000
|
|
310
|
+
3000
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
<details>
|
|
314
|
+
<summary>Defaults for optional option values</summary>
|
|
315
|
+
<br>
|
|
316
|
+
|
|
317
|
+
When `--port` is omitted, `options.get("--port")` returns `"8080"`. The option still counts as absent: `options.has("--port")` is `false` and `options.count("--port")` is `0`. Supplying `--port` without a value is an error.
|
|
318
|
+
|
|
319
|
+
With `isOptional: true` on an option argument, the option can be supplied without a value. Its default then supplies the missing value. When the option is omitted entirely, `options.get()` returns `undefined`:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
options: [
|
|
323
|
+
{
|
|
324
|
+
name: "--inspect",
|
|
325
|
+
args: {
|
|
326
|
+
name: "port",
|
|
327
|
+
isOptional: true,
|
|
328
|
+
default: "9229",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
action({ options }) {
|
|
333
|
+
console.log(options.get("--inspect"));
|
|
334
|
+
},
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```console
|
|
338
|
+
$ demo build --inspect
|
|
339
|
+
9229
|
|
340
|
+
$ demo build --inspect=9000
|
|
341
|
+
9000
|
|
342
|
+
$ demo build
|
|
343
|
+
undefined
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Defaults are strings and appear in help automatically. A positional argument with a default must be optional. Defaults for an omitted option don't satisfy required options or dependencies. [Spec validation](#testing-your-spec) catches defaults the parser cannot use.
|
|
347
|
+
|
|
348
|
+
<hr>
|
|
349
|
+
</details>
|
|
350
|
+
|
|
351
|
+
<br>
|
|
352
|
+
|
|
353
|
+
### Variadic arguments
|
|
354
|
+
|
|
355
|
+
A variadic argument accepts one or more values. Set `isVariadic: true` on the last argument; its values appear in the same array as the other positional arguments.
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
args: {
|
|
359
|
+
name: "files",
|
|
360
|
+
isVariadic: true,
|
|
361
|
+
},
|
|
362
|
+
action({ args }) {
|
|
363
|
+
console.log(args.join(", "));
|
|
364
|
+
},
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```console
|
|
368
|
+
$ demo build index.ts utils.ts
|
|
369
|
+
index.ts, utils.ts
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
An argument can be both optional and variadic to accept zero or more values. Option arguments support the same fields, including `isOptional` and `isVariadic`.
|
|
373
|
+
|
|
374
|
+
<br>
|
|
375
|
+
|
|
376
|
+
### Repeated options
|
|
377
|
+
|
|
378
|
+
Setting `isRepeatable: true` on an option allows it to be supplied any number of times. `options.all()` returns the collected values in order.
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
options: [
|
|
382
|
+
{
|
|
383
|
+
name: "--tag",
|
|
384
|
+
args: {
|
|
385
|
+
name: "tag",
|
|
386
|
+
},
|
|
387
|
+
isRepeatable: true,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
action({ options }) {
|
|
391
|
+
console.log(JSON.stringify(options.all("--tag") ?? []));
|
|
392
|
+
},
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```console
|
|
396
|
+
$ demo build --tag dev --tag latest
|
|
397
|
+
["dev","latest"]
|
|
398
|
+
$ demo build
|
|
399
|
+
[]
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Setting `isRepeatable` to an integer greater than 1 limits how many times the option can be supplied. For example, `isRepeatable: 2` allows at most two `--tag` options:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
options: [
|
|
406
|
+
{
|
|
407
|
+
name: "--tag",
|
|
408
|
+
description: "Add a tag.",
|
|
409
|
+
args: {
|
|
410
|
+
name: "tag",
|
|
411
|
+
},
|
|
412
|
+
isRepeatable: 2,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
action({ options }) {
|
|
416
|
+
console.log(JSON.stringify(options.all("--tag") ?? []));
|
|
417
|
+
},
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```console
|
|
421
|
+
$ demo build --tag dev --tag latest
|
|
422
|
+
["dev","latest"]
|
|
423
|
+
$ demo build --tag dev --tag latest --tag extra
|
|
424
|
+
Error: Too many repetitions
|
|
425
|
+
|
|
426
|
+
Usage:
|
|
427
|
+
demo build [flags]
|
|
428
|
+
|
|
429
|
+
Flags:
|
|
430
|
+
--tag <tag> Add a tag.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Without `isRepeatable`, repeating an option replaces its previous values.
|
|
434
|
+
|
|
435
|
+
For options with arguments, `options.count(name)` counts collected values. For repeatable flags without arguments, it counts occurrences of the flag.
|
|
436
|
+
|
|
437
|
+
<table><tbody><tr><td>💡</td><td>
|
|
438
|
+
<!-- <tip> -->
|
|
439
|
+
Use a repeatable <code>-v</code> flag to let users increase logging detail: <code>-v</code> for diagnostics, <code>-vv</code> for debug output, and <code>-vvv</code> for tracing. Use <code>options.count("-v")</code> to select the logging level in your action.
|
|
440
|
+
<!-- </tip> -->
|
|
441
|
+
</td></tr></tbody></table><br>
|
|
442
|
+
|
|
443
|
+
### Subcommands
|
|
444
|
+
|
|
445
|
+
Each subcommand has its own name, arguments, options and action. Its definition goes in the parent command's `subcommands` array. This example adds `clean` to the root spec, with `c` as an alias:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
subcommands: [
|
|
449
|
+
{
|
|
450
|
+
name: ["clean", "c"],
|
|
451
|
+
description: "Remove build output.",
|
|
452
|
+
action() {
|
|
453
|
+
console.log("Cleaning up.");
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
```console
|
|
460
|
+
$ demo clean
|
|
461
|
+
Cleaning up.
|
|
462
|
+
$ demo c
|
|
463
|
+
Cleaning up.
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
A command without an action prints its help. Set `hidden: true` to leave a command or option out of help and completion; typing its exact name still works.
|
|
467
|
+
|
|
468
|
+
Setting `requiresSubcommand: true` on a command prevents it from accepting positional arguments. When no subcommand is supplied, the command's action runs if defined; otherwise, it displays help.
|
|
469
|
+
|
|
470
|
+
<br>
|
|
471
|
+
|
|
472
|
+
### Global options
|
|
473
|
+
|
|
474
|
+
An option normally belongs to the command that defines it. With `isPersistent: true`, descendant commands inherit it. Here, `--verbose` is defined on the root spec and is also available to `build`:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
options: [
|
|
478
|
+
{
|
|
479
|
+
name: "--verbose",
|
|
480
|
+
isPersistent: true,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
subcommands: [
|
|
484
|
+
{
|
|
485
|
+
name: "build",
|
|
486
|
+
action({ options }) {
|
|
487
|
+
console.log(options.has("--verbose"));
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```console
|
|
494
|
+
$ demo --verbose build
|
|
495
|
+
true
|
|
496
|
+
$ demo build --verbose
|
|
497
|
+
true
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Help lists persistent options under “Global flags”. When `CLI.help` is added to the root spec, every descendant command inherits `--help`.
|
|
501
|
+
|
|
502
|
+
<br>
|
|
503
|
+
|
|
504
|
+
### Option relationships
|
|
505
|
+
|
|
506
|
+
`dependsOn` lists options that must also be supplied. `exclusiveOn` lists options that cannot be used together.
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
options: [
|
|
510
|
+
{
|
|
511
|
+
name: "--json",
|
|
512
|
+
exclusiveOn: ["--quiet"],
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "--quiet",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: "--sign",
|
|
519
|
+
dependsOn: ["--output"],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: "--output",
|
|
523
|
+
args: {
|
|
524
|
+
name: "file",
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`--json --quiet` fails in either order. `--sign` fails unless the user also supplies `--output`. Names in these lists include their prefixes. Defaults don't count as supplied options.
|
|
531
|
+
|
|
532
|
+
<br>
|
|
533
|
+
|
|
534
|
+
### Actions & errors
|
|
535
|
+
|
|
536
|
+
Actions receive argument values as strings. Conversion and validation happen inside the action. If it throws an `Error`, `CLI.run` prints its message to stderr and returns exit code 1.
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
options: [
|
|
540
|
+
{
|
|
541
|
+
name: "--port",
|
|
542
|
+
args: {
|
|
543
|
+
name: "port",
|
|
544
|
+
default: "8080",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
action({ options }) {
|
|
549
|
+
const port = Number(options.get("--port"));
|
|
550
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
551
|
+
throw new Error("Port must be an integer between 1 and 65535");
|
|
552
|
+
}
|
|
553
|
+
console.log(`Port: ${port}`);
|
|
554
|
+
},
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
```console
|
|
558
|
+
$ demo build --port 3000
|
|
559
|
+
Port: 3000
|
|
560
|
+
$ demo build --port nope
|
|
561
|
+
Error: Port must be an integer between 1 and 65535
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
An action can return a number to set the exit code, or return nothing for success (0). Async actions are awaited. `CLI.run` resolves to the exit code, limited to the range 0–255. Assign the awaited result to `process.exitCode` as in the opening example.
|
|
565
|
+
|
|
566
|
+
<details>
|
|
567
|
+
<summary>Option actions and additional action parameters</summary>
|
|
568
|
+
<br>
|
|
569
|
+
|
|
570
|
+
An option can have an action, too. This runs **instead of** the command's action:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
options: [
|
|
574
|
+
{
|
|
575
|
+
name: "--version",
|
|
576
|
+
action: () => console.log("1.0.0"),
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
When several supplied options have actions, the last one is selected. It bypasses checks for positional argument counts, required options and option relationships. This allows `--help` to display usage without supplying required inputs. Unknown options and malformed option values still produce parsing errors.
|
|
582
|
+
|
|
583
|
+
Actions also receive:
|
|
584
|
+
|
|
585
|
+
- `path`: the command objects from the root spec to the selected command.
|
|
586
|
+
- `argSeparatorIndex`: the index in `args` where values after `--` begin, or `-1` if `--` was not supplied.
|
|
587
|
+
- `help(config?)`: a function that returns help text for the selected command.
|
|
588
|
+
|
|
589
|
+
`options.options` exposes the underlying `Map<string, string[]>`. Supplied options have entries for every alias; omitted options have no entry, including those with defaults.
|
|
590
|
+
|
|
591
|
+
To report an error without throwing, call `CLI.printError("message")` and return 1.
|
|
592
|
+
|
|
593
|
+
The action's `help()` function accepts `description: false` to omit the command description and `usage: false` to omit the usage line. `didYouMean: { input, choices }` adds a spelling suggestion. To render help for another command, pass its array of command objects as `path`.
|
|
594
|
+
|
|
595
|
+
`CLI.run` reads `process.argv.slice(2)` by default. Pass a second argument to supply the command line directly, for example `CLI.run(spec, ["build", "src"])`. It never calls `process.exit`.
|
|
596
|
+
|
|
597
|
+
<hr>
|
|
598
|
+
</details>
|
|
599
|
+
|
|
600
|
+
[← Example](#example) · [↑ Contents](#contents) · [Help and completion →](#help-and-completion)
|
|
601
|
+
|
|
602
|
+
## Help and completion
|
|
603
|
+
|
|
604
|
+
`CLI.help` is a help option; `CLI.helpCommand` and `CLI.completion` are subcommands. Add them to the root spec's `options` and `subcommands` arrays:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
options: [CLI.help],
|
|
608
|
+
subcommands: [CLI.helpCommand, CLI.completion, /* your commands */],
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Running `demo --help` displays help for the top-level command. Both `demo build --help` and `demo help build` display help for the `build` subcommand. For nested subcommands, include the full command path: `demo help remote add`.
|
|
612
|
+
|
|
613
|
+
The built-ins are plain objects. To customise one, copy it with object spread: `{ ...CLI.help, description: "Show help" }`. Setting `isPersistent: true` on `CLI.helpCommand` makes it available within descendant commands, so `demo build help` displays help for `build`.
|
|
614
|
+
|
|
615
|
+
Persistent subcommands match exact names and are omitted from descendant help. If a descendant defines a subcommand with the same name, that local definition takes precedence.
|
|
616
|
+
|
|
617
|
+
To pass an inherited command's name as the first positional argument, put `--` before it. For example, `demo build -- help` passes `"help"` as the input directory.
|
|
618
|
+
|
|
619
|
+
<br>
|
|
620
|
+
|
|
621
|
+
### Completion suggestions
|
|
622
|
+
|
|
623
|
+
An argument's `suggestions` field supplies values offered by shell completion. It works on both positional and option arguments:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
options: [
|
|
627
|
+
{
|
|
628
|
+
name: "--mode",
|
|
629
|
+
args: {
|
|
630
|
+
name: "mode",
|
|
631
|
+
suggestions: ["debug", "release"],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Suggestions don't restrict accepted values. Validate allowed values in the action if the option must accept only a fixed set.
|
|
638
|
+
|
|
639
|
+
On an argument, `template: "filepaths"` completes files and directories; `template: "folders"` completes directories only.
|
|
640
|
+
|
|
641
|
+
Dynamic suggestions come from `generators.custom`. Here, `listTargets(tokens)` is an application function that returns the currently available targets:
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
options: [
|
|
645
|
+
{
|
|
646
|
+
name: "--target",
|
|
647
|
+
args: {
|
|
648
|
+
name: "target",
|
|
649
|
+
generators: {
|
|
650
|
+
custom: async (tokens) => listTargets(tokens),
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Static suggestions and generator results can be strings or `{ name, description }` objects. A generator receives the words after the program name, ending with the partial value being completed. For `--target=prod`, its last token is `"prod"`. An argument can have multiple generators by supplying an array.
|
|
658
|
+
|
|
659
|
+
Each completion request starts a new CLI process. The completion resolver follows the parser's rules for option scope, conflicts and repeat limits. Put application initialisation inside actions to avoid running it on completion requests.
|
|
660
|
+
|
|
661
|
+
<details>
|
|
662
|
+
<summary>Install shell completion</summary>
|
|
663
|
+
<br>
|
|
664
|
+
|
|
665
|
+
`demo completion` lists the supported shells. Use `demo completion <shell>` to print a script, replacing `demo` with your installed command's name.
|
|
666
|
+
|
|
667
|
+
**Bash:** save the script and source it from `~/.bashrc`.
|
|
668
|
+
|
|
54
669
|
```sh
|
|
55
|
-
demo completion bash >
|
|
56
|
-
|
|
670
|
+
demo completion bash > ~/.demo-completion.bash
|
|
671
|
+
# Add to ~/.bashrc: source ~/.demo-completion.bash
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Zsh:** save into a directory on `fpath`, before `compinit` runs.
|
|
675
|
+
|
|
676
|
+
```zsh
|
|
677
|
+
mkdir -p ~/.zsh/completions
|
|
678
|
+
demo completion zsh > ~/.zsh/completions/_demo
|
|
679
|
+
# Add to ~/.zshrc before compinit: fpath=(~/.zsh/completions $fpath)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
If needed, enable [Zsh's completion system](https://zsh.sourceforge.io/Doc/Release/Completion-System.html#Initialization) with `autoload -Uz compinit; compinit` after setting `fpath`.
|
|
683
|
+
|
|
684
|
+
**Fish:** save into its completion directory.
|
|
685
|
+
|
|
686
|
+
```fish
|
|
687
|
+
mkdir -p ~/.config/fish/completions
|
|
57
688
|
demo completion fish > ~/.config/fish/completions/demo.fish
|
|
58
689
|
```
|
|
59
690
|
|
|
60
|
-
|
|
691
|
+
**PowerShell:** append to [your profile](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles), creating it if needed.
|
|
692
|
+
|
|
693
|
+
```powershell
|
|
694
|
+
if (!(Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
|
|
695
|
+
demo completion powershell >> $PROFILE
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Nushell:** save into a [user autoload directory](https://www.nushell.sh/book/configuration.html#configuration-overview).
|
|
699
|
+
|
|
700
|
+
```nu
|
|
701
|
+
mkdir ($nu.user-autoload-dirs | first)
|
|
702
|
+
demo completion nushell | save -f ($nu.user-autoload-dirs | first | path join demo.nu)
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Start a new shell after installing.
|
|
706
|
+
|
|
707
|
+
<hr>
|
|
708
|
+
</details>
|
|
709
|
+
|
|
710
|
+
[← Getting started](#getting-started) · [↑ Contents](#contents) · [Testing your spec →](#testing-your-spec)
|
|
711
|
+
|
|
712
|
+
## Testing your spec
|
|
713
|
+
|
|
714
|
+
`testSpec` validates the structure of a spec, including names, argument order, defaults and option references. These checks catch mistakes that TypeScript's types cannot express.
|
|
715
|
+
|
|
716
|
+
Export your spec from a module that does not call `CLI.run`, such as `src/spec.ts`, so importing it in a test does not run the CLI. A Vitest test can import the spec and validate it:
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import { test } from "vitest";
|
|
720
|
+
import { testSpec } from "@clo4/cli/testing";
|
|
721
|
+
import { spec } from "../src/spec.ts";
|
|
722
|
+
|
|
723
|
+
test("CLI spec is valid", () => testSpec(spec)());
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
`testSpec(spec)` returns an async function that runs the checks and throws on failure. Return its promise from your test callback, as above, or await it inside an async test.
|
|
727
|
+
|
|
728
|
+
The examples above leave out some descriptions to keep them short. By default, validation checks that every command and option has a description and that description lines fit within 68 characters.
|
|
729
|
+
|
|
730
|
+
<details>
|
|
731
|
+
<summary>Validation options</summary>
|
|
732
|
+
<br>
|
|
733
|
+
|
|
734
|
+
The second argument configures validation. `allowNoDescription: true` disables the check for missing command and option descriptions:
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
await testSpec(spec, {
|
|
738
|
+
allowNoDescription: true,
|
|
739
|
+
})();
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Three other options disable individual checks:
|
|
743
|
+
|
|
744
|
+
- `allowLongDescriptionLines`: descriptions can have lines over 68 characters.
|
|
745
|
+
- `allowShadowingPersistentOptions`: a subcommand can replace an inherited option.
|
|
746
|
+
- `allowMatchingSubcommandPrefixAndArgs`: a command can combine positional arguments with subcommands matched by prefix.
|
|
747
|
+
|
|
748
|
+
All four default to `false`. `namingStyle: "function"` uses assertion names in step-based test output.
|
|
749
|
+
|
|
750
|
+
The testing entry point also exports `test` as an alias of `testSpec`, the individual assertions, and their [types and options](src/testing.ts).
|
|
751
|
+
|
|
752
|
+
<hr>
|
|
753
|
+
</details>
|
|
754
|
+
|
|
755
|
+
[← Help and completion](#help-and-completion) · [↑ Contents](#contents) · [Parser settings →](#parser-settings)
|
|
756
|
+
|
|
757
|
+
## Parser settings
|
|
758
|
+
|
|
759
|
+
A command's `parserDirectives` field controls how the parser reads its command line. By default, options can appear before or after positional arguments, subcommands match exact names or aliases, and attached option values use `=`.
|
|
760
|
+
|
|
761
|
+
A setting applies to the command that defines it and is inherited by its descendants. A subcommand can override a boolean setting with `false`, or provide its own `optionArgSeparators`.
|
|
762
|
+
|
|
763
|
+
Each example below defines a separate root `spec`. Keep the `import * as CLI from "@clo4/cli"` and `process.exitCode = await CLI.run(spec)` from the opening example.
|
|
764
|
+
|
|
765
|
+
### Options before arguments
|
|
766
|
+
|
|
767
|
+
Normally, `demo node --verbose` reads `--verbose` as an option and `node` as a positional argument. For a wrapper around another program, you may want every word after `node` passed through instead.
|
|
768
|
+
|
|
769
|
+
With `optionsMustPrecedeArguments: true`, the first positional argument ends option parsing:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
const spec: CLI.Spec = {
|
|
773
|
+
name: "demo",
|
|
774
|
+
parserDirectives: {
|
|
775
|
+
optionsMustPrecedeArguments: true,
|
|
776
|
+
},
|
|
777
|
+
args: {
|
|
778
|
+
name: "command",
|
|
779
|
+
isVariadic: true,
|
|
780
|
+
},
|
|
781
|
+
options: [
|
|
782
|
+
{
|
|
783
|
+
name: "--verbose",
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
action({ args, options }) {
|
|
787
|
+
console.log(`verbose: ${options.has("--verbose")}`);
|
|
788
|
+
console.log(JSON.stringify(args));
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
```console
|
|
794
|
+
$ demo --verbose node --trace-warnings
|
|
795
|
+
verbose: true
|
|
796
|
+
["node","--trace-warnings"]
|
|
797
|
+
$ demo node --verbose
|
|
798
|
+
verbose: false
|
|
799
|
+
["node","--verbose"]
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
With the directive, `demo node --verbose` treats `--verbose` as a positional value. With default settings, that command would print `verbose: true` and `["node"]`. To end option parsing for a single invocation, use `--`, for example `demo -- node --verbose`.
|
|
803
|
+
|
|
804
|
+
<br>
|
|
805
|
+
|
|
806
|
+
### Abbreviated subcommands
|
|
807
|
+
|
|
808
|
+
Subcommands normally require an exact name or alias. `subcommandsMatchUniquePrefix: true` also accepts a prefix when it identifies just one subcommand:
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
const spec: CLI.Spec = {
|
|
812
|
+
name: "demo",
|
|
813
|
+
parserDirectives: {
|
|
814
|
+
subcommandsMatchUniquePrefix: true,
|
|
815
|
+
},
|
|
816
|
+
subcommands: [
|
|
817
|
+
{
|
|
818
|
+
name: "install",
|
|
819
|
+
action: () => console.log("Installing."),
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: "info",
|
|
823
|
+
action: () => console.log("Project information."),
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
};
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
```console
|
|
830
|
+
$ demo ins
|
|
831
|
+
Installing.
|
|
832
|
+
$ demo inf
|
|
833
|
+
Project information.
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Without the directive, `ins` and `inf` are unknown commands. `demo in` still fails because both names begin with `in`: `Ambiguous command 'in', could be: install, info`.
|
|
837
|
+
|
|
838
|
+
Hidden and inherited subcommands still require exact names.
|
|
839
|
+
|
|
840
|
+
When a command accepts both positional arguments and abbreviated subcommands, a word could be interpreted as either. Spec validation rejects this combination by default.
|
|
61
841
|
|
|
62
|
-
|
|
842
|
+
<br>
|
|
843
|
+
|
|
844
|
+
### Option value separators
|
|
845
|
+
|
|
846
|
+
By default, `--port=3000` attaches a value with `=`, and `--port 3000` supplies it as the next word. `optionArgSeparators` changes which separators are accepted for attached values.
|
|
847
|
+
|
|
848
|
+
This spec adds `:` while keeping `=`:
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
const spec: CLI.Spec = {
|
|
852
|
+
name: "demo",
|
|
853
|
+
parserDirectives: {
|
|
854
|
+
optionArgSeparators: [":", "="],
|
|
855
|
+
},
|
|
856
|
+
options: [
|
|
857
|
+
{
|
|
858
|
+
name: ["-p", "--port"],
|
|
859
|
+
args: {
|
|
860
|
+
name: "port",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
action({ options }) {
|
|
865
|
+
console.log(options.get("--port"));
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
```
|
|
63
869
|
|
|
64
|
-
|
|
870
|
+
```console
|
|
871
|
+
$ demo --port:3000
|
|
872
|
+
3000
|
|
873
|
+
$ demo --port=4000
|
|
874
|
+
4000
|
|
875
|
+
$ demo --port 5000
|
|
876
|
+
5000
|
|
877
|
+
```
|
|
65
878
|
|
|
66
|
-
|
|
879
|
+
Without the directive, `--port:3000` is an unknown option. `optionArgSeparators: ":"` accepts only `:` as a separator. `[]` disables separators; short attached values such as `-p3000` still work.
|
|
67
880
|
|
|
68
|
-
|
|
881
|
+
<details>
|
|
882
|
+
<summary>Requiring a separator</summary>
|
|
883
|
+
<br>
|
|
69
884
|
|
|
70
|
-
|
|
885
|
+
Setting `requiresSeparator: true` on an option requires its value to be attached using an enabled separator. This spec uses the default separator, `=`. It accepts `--port=3000` and rejects `--port 3000`:
|
|
71
886
|
|
|
72
887
|
```ts
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
888
|
+
const spec: CLI.Spec = {
|
|
889
|
+
name: "demo",
|
|
890
|
+
options: [
|
|
891
|
+
{
|
|
892
|
+
name: "--port",
|
|
893
|
+
args: {
|
|
894
|
+
name: "port",
|
|
895
|
+
},
|
|
896
|
+
requiresSeparator: true,
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
action({ options }) {
|
|
900
|
+
console.log(options.get("--port"));
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
A string requires that particular separator: `requiresSeparator: ":"` accepts `--port:3000` and rejects `--port=3000`. The separator must also be enabled in `parserDirectives.optionArgSeparators`.
|
|
906
|
+
|
|
907
|
+
<hr>
|
|
908
|
+
</details>
|
|
78
909
|
|
|
79
|
-
|
|
80
|
-
// static completions
|
|
81
|
-
name: "--mode",
|
|
82
|
-
args: { name: "mode", suggestions: ["debug", "release"] },
|
|
83
|
-
}
|
|
910
|
+
<br>
|
|
84
911
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
912
|
+
### Literal option names
|
|
913
|
+
|
|
914
|
+
Normally, `-abc` is read as chained short options: `-a -b -c`. With `flagsArePosixNoncompliant: true`, names are read literally, so you can define an option such as `-name`:
|
|
915
|
+
|
|
916
|
+
```ts
|
|
917
|
+
const spec: CLI.Spec = {
|
|
918
|
+
name: "demo",
|
|
919
|
+
parserDirectives: {
|
|
920
|
+
flagsArePosixNoncompliant: true,
|
|
921
|
+
},
|
|
88
922
|
args: {
|
|
89
|
-
name: "
|
|
90
|
-
|
|
923
|
+
name: "paths",
|
|
924
|
+
isOptional: true,
|
|
925
|
+
isVariadic: true,
|
|
926
|
+
},
|
|
927
|
+
options: [
|
|
928
|
+
{
|
|
929
|
+
name: "-name",
|
|
930
|
+
args: {
|
|
931
|
+
name: "pattern",
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
action({ args, options }) {
|
|
936
|
+
console.log(`pattern: ${options.get("-name")}`);
|
|
937
|
+
console.log(JSON.stringify(args));
|
|
91
938
|
},
|
|
92
|
-
}
|
|
939
|
+
};
|
|
93
940
|
```
|
|
94
941
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
942
|
+
```console
|
|
943
|
+
$ demo src -name '*.ts'
|
|
944
|
+
pattern: *.ts
|
|
945
|
+
["src"]
|
|
946
|
+
$ demo -unknown
|
|
947
|
+
pattern: undefined
|
|
948
|
+
["-unknown"]
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
This setting also treats unrecognised option-like words as positional arguments. Without it, `-unknown` produces an option error. The command must still declare enough positional arguments to accept these values.
|
|
952
|
+
|
|
953
|
+
<table><tbody><tr><td>💡</td><td>
|
|
954
|
+
<!-- <tip> -->
|
|
955
|
+
Quote glob patterns when you want the CLI to receive the pattern itself. In <code>demo src -name '*.ts'</code>, the quotes prevent the shell from expanding <code>*.ts</code> into matching filenames before the CLI receives it.
|
|
956
|
+
<!-- </tip> -->
|
|
957
|
+
</td></tr></tbody></table>
|
|
958
|
+
|
|
959
|
+
[← Testing your spec](#testing-your-spec) · [↑ Contents](#contents) · [Performance →](#performance)
|
|
960
|
+
|
|
961
|
+
## Performance
|
|
962
|
+
|
|
963
|
+
The [benchmarks](bench/run.ts) compare command execution and startup overhead with other CLI libraries and Node's built-in `parseArgs`. Run them with `pnpm bench`; [CI also records results](.github/workflows/cli-bench.yml).
|
|
964
|
+
|
|
965
|
+
<details>
|
|
966
|
+
<summary>Performance comparison</summary>
|
|
967
|
+
<br>
|
|
968
|
+
|
|
969
|
+
Each value is relative to `@clo4/cli` in the same run: `2.5x` means the library took 2.5 times as long for the measured work. Lower is better. Each column is a separate run on the listed hardware or CI.
|
|
970
|
+
|
|
971
|
+
| Library | CI | AMD Ryzen 9 9950X3D | Apple M3 Pro |
|
|
972
|
+
| --------------------------------------- | ------------------ | ------------------- | ------------------ |
|
|
973
|
+
| `node:util` parseArgs, hand-written CLI | Roughly equivalent | Roughly equivalent | Roughly equivalent |
|
|
974
|
+
| sade | Roughly equivalent | Roughly equivalent | Roughly equivalent |
|
|
975
|
+
| Cac | 1.1x | Roughly equivalent | 1.2x |
|
|
976
|
+
| citty | 1.4x | 1.4x | 1.4x |
|
|
977
|
+
| brocli | 2.5x | 3.0x | 1.6x |
|
|
978
|
+
| Cliffy | 2.8x | 2.7x | 2.4x |
|
|
979
|
+
| Stricli | 3.0x | 2.8x | 1.9x |
|
|
980
|
+
| gunshi | 3.1x | 2.3x | 2.0x |
|
|
981
|
+
| Commander | 3.7x | 4.5x | 2.6x |
|
|
982
|
+
| clerc | 3.8x | 4.3x | 2.7x |
|
|
983
|
+
| cmd-ts | 4.4x | 5.0x | 3.1x |
|
|
984
|
+
| Clipanion | 4.6x | 5.3x | 3.4x |
|
|
985
|
+
| Optique | 11.9x | 12.2x | 11.1x |
|
|
986
|
+
| oclif | 13.8x | 14.8x | 12.7x |
|
|
987
|
+
| Effect | 15.6x | 16.3x | 14.4x |
|
|
988
|
+
| Yargs | 16.0x | 16.1x | 15.6x |
|
|
989
|
+
|
|
990
|
+
The benchmark runs seven tasks, including help output, nested commands and argument errors. Timing starts before the bundled CLI is loaded and ends when the process exits. Node's own startup time is excluded.
|
|
991
|
+
|
|
992
|
+
For each library, the score is the geometric mean of its seven task medians, minus the cost of loading an empty bundle. The table compares those scores. “Roughly equivalent” means the difference was below 0.25 ms, or the 95% bootstrap interval for the difference included zero.
|
|
993
|
+
|
|
994
|
+
</details>
|
|
995
|
+
|
|
996
|
+
[← Parser settings](#parser-settings) · [↑ Contents](#contents) · [API and compatibility →](#api-and-compatibility)
|
|
997
|
+
|
|
998
|
+
## API and compatibility
|
|
999
|
+
|
|
1000
|
+
The package uses ECMAScript modules (ESM), includes TypeScript declarations and has no runtime dependencies. Its named runtime exports are `run`, `printError`, `help`, `helpCommand` and `completion`. The default export is an object containing those same five values.
|
|
1001
|
+
|
|
1002
|
+
Public types include `Spec`, `Subcommand`, `Option`, `Arg` and `ActionInit`. The [type definitions](src/types.ts) document each field; the [entry point](src/index.ts) lists all exports.
|
|
1003
|
+
|
|
1004
|
+
The runtime API follows semantic versioning. The testing module may introduce stricter validation rules in minor or patch releases to catch incorrect or undefined behavior.
|
|
1005
|
+
|
|
1006
|
+
[← Performance](#performance) · [↑ Contents](#contents) · [License →](#license)
|
|
1007
|
+
|
|
1008
|
+
## License
|
|
1009
|
+
|
|
1010
|
+
[MIT](LICENSE)
|
|
1011
|
+
|
|
1012
|
+
[← API and compatibility](#api-and-compatibility) · [↑ Contents](#contents)
|