@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 CHANGED
@@ -1,97 +1,1012 @@
1
- # @clo4/cli
1
+ # clo4's CLI SDK
2
2
 
3
- Fully declarative CLI framework, lean footprint, fast, good API for completions.
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: "Compile a project.",
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: { name: "input" },
34
+ args: {
35
+ name: "input",
36
+ description: "Source directory.",
37
+ isOptional: true,
38
+ default: ".",
39
+ template: "folders",
40
+ },
17
41
  options: [
18
- { name: "--watch", description: "Watch for changes." },
42
+ {
43
+ name: ["-w", "--watch"],
44
+ description: "Watch for changes.",
45
+ },
19
46
  {
20
47
  name: ["-o", "--output"],
21
- args: { name: "dir" },
22
- isRequired: true,
23
- description: "Write output to this directory.",
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
- Specs define `args`, `options`, `subcommands`, and `action` functions. Parsing and validating values stay in your code, it's not the job of the library.
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
- Option actions override command actions. That lets helpers like `--help` run even when the selected command is missing required positional arguments.
140
+ For a command line like `demo build src --output dist --watch`:
39
141
 
40
- Use `@clo4/cli/testing` in package tests to validate structural constraints that TypeScript cannot express, such as duplicate names, bad required/optional argument order, invalid repeatable options, and invalid option references.
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
- ## Shell completion
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
- Add `CLI.completion` to a spec's `subcommands` to give the CLI a `completion <shell>` command that prints a completion script:
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
- options: [CLI.help],
50
- subcommands: [CLI.completion /* ... */],
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 > /etc/bash_completion.d/demo
56
- demo completion zsh > "${fpath[1]}/_demo"
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
- Completion describes the whole CLI from the root spec regardless of where the subcommand is placed. Hidden options and subcommands are excluded; persistent options are offered on the descendants that inherit them.
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
- ### How completion works
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
- `completion <shell>` prints a short script. On every Tab, the script runs `<cli> completion complete -- <commandline>`, and the CLI answers using its own parser. Completions therefore follow every parser option and relationship rule (`exclusiveOn`, `isRepeatable`, `dependsOn`, and so on). Each Tab starts the CLI once, so keep its startup fast.
870
+ ```console
871
+ $ demo --port:3000
872
+ 3000
873
+ $ demo --port=4000
874
+ 4000
875
+ $ demo --port 5000
876
+ 5000
877
+ ```
65
878
 
66
- The bash and fish scripts pass `--gate-options`, so options are only offered once the word starts with `-`, which is the bash and fish convention. The zsh script leaves it out and offers options as usual.
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
- ### Argument suggestions
881
+ <details>
882
+ <summary>Requiring a separator</summary>
883
+ <br>
69
884
 
70
- Arguments can declare completion metadata. These are only used for completions, not parsing.
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
- // dynamic completions: file/folder paths
75
- name: "--config",
76
- args: { name: "file", template: "filepaths" },
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
- // fully dynamic completions using your own code
87
- name: "--target",
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: "target",
90
- generators: { custom: (tokens) => listTargets() },
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
- - `template` (`"filepaths"` / `"folders"`) completes paths.
96
- - `suggestions` is a static list (strings or `{ name, description }`).
97
- - `generators.custom(tokens)` returns dynamic suggestions. It runs inside the CLI when the shell asks for completions.
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)