clap-ts 0.1.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/LICENSE +21 -0
- package/README.md +752 -0
- package/dist/help.d.ts +31 -0
- package/dist/help.js +414 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +646 -0
- package/dist/runner.d.ts +60 -0
- package/dist/runner.js +370 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +5 -0
- package/dist/validation.d.ts +12 -0
- package/dist/validation.js +352 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
# clap-ts
|
|
2
|
+
|
|
3
|
+
A type-safe CLI argument parser for TypeScript, inspired by Rust's [clap](https://docs.rs/clap/latest/clap/) crate.
|
|
4
|
+
|
|
5
|
+
Full clap-style parsing, validation, help generation, and subcommand support. Zero runtime dependencies -- built on `node:util parseArgs` with a rich layer on top.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Full type inference** -- `defineCommand` infers exact types for parsed args, no casts needed
|
|
10
|
+
- **Clap-compatible argument model** -- boolean, string, number, enum, positional args with short/long flags
|
|
11
|
+
- **Subcommands** -- nested command trees with alias support, prefix inference, and external subcommands
|
|
12
|
+
- **Validation** -- required, exclusive, conflictsWith, requires, requiredUnlessPresent, requiredIfEq, valueParser, numArgs, argument groups
|
|
13
|
+
- **Custom value parsers** -- function-based parsers for custom validation and type conversion
|
|
14
|
+
- **Clap-style help** -- colored, terminal-width-aware help with custom headings, templates, and styles
|
|
15
|
+
- **Clap-style errors** -- "did you mean?" typo suggestions via Levenshtein distance
|
|
16
|
+
- **Environment variable fallback** -- `env` field on args, with CLI > env > default precedence
|
|
17
|
+
- **Lifecycle hooks** -- setup/run/cleanup pattern for resource management
|
|
18
|
+
- **Actions** -- `set` (default), `append` (collect into array), `count` (e.g. `-vvv` = 3)
|
|
19
|
+
- **Value delimiters** -- `--tags=a,b,c` splits into array with `valueDelimiter`
|
|
20
|
+
- **Boolean negation** -- `--no-verbose` automatically supported for boolean flags
|
|
21
|
+
- **Global args** -- `global: true` args inherited by all subcommands
|
|
22
|
+
- **Reusable arg groups** -- `defineArgs()` + spread for sharing args across commands
|
|
23
|
+
- **Trailing var args** -- last positional consumes all remaining args
|
|
24
|
+
- **Negative numbers** -- `--offset -10` with `allowNegativeNumbers`
|
|
25
|
+
- **Hyphen values** -- `--grep -pattern` with `allowHyphenValues`
|
|
26
|
+
- **Zero dependencies** -- only uses `node:util` (parseArgs + styleText)
|
|
27
|
+
- **Bun and Node.js** -- works on both runtimes
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# npm
|
|
33
|
+
npm install clap-ts
|
|
34
|
+
|
|
35
|
+
# bun
|
|
36
|
+
bun add clap-ts
|
|
37
|
+
|
|
38
|
+
# pnpm
|
|
39
|
+
pnpm add clap-ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { defineCommand, runMain } from 'clap-ts';
|
|
46
|
+
|
|
47
|
+
const main = defineCommand({
|
|
48
|
+
meta: {
|
|
49
|
+
name: 'my-tool',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
description: 'A great CLI tool',
|
|
52
|
+
},
|
|
53
|
+
args: {
|
|
54
|
+
name: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
short: 'n',
|
|
57
|
+
description: 'Your name',
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
verbose: {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
short: 'v',
|
|
63
|
+
description: 'Enable verbose output',
|
|
64
|
+
},
|
|
65
|
+
port: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
short: 'p',
|
|
68
|
+
default: 3000,
|
|
69
|
+
description: 'Port to listen on',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
run({ args }) {
|
|
73
|
+
// args.name is string (required, so never undefined)
|
|
74
|
+
// args.verbose is boolean | undefined
|
|
75
|
+
// args.port is number (has default, so never undefined)
|
|
76
|
+
console.log(`Hello ${args.name} on port ${args.port}`);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
runMain(main);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
$ my-tool --name World -p 8080
|
|
85
|
+
Hello World on port 8080
|
|
86
|
+
|
|
87
|
+
$ my-tool --help
|
|
88
|
+
A great CLI tool (my-tool v1.0.0)
|
|
89
|
+
|
|
90
|
+
Usage: my-tool [OPTIONS]
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
-n, --name <STRING> Your name [required]
|
|
94
|
+
-v, --verbose Enable verbose output
|
|
95
|
+
-p, --port <PORT> Port to listen on [default: 3000]
|
|
96
|
+
-h, --help Print help
|
|
97
|
+
-V, --version Print version
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### defineCommand
|
|
103
|
+
|
|
104
|
+
The primary API for creating commands. Returns the same object with full type inference.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineCommand } from 'clap-ts';
|
|
108
|
+
|
|
109
|
+
const cmd = defineCommand({
|
|
110
|
+
meta: {
|
|
111
|
+
name: 'serve',
|
|
112
|
+
version: '1.0.0',
|
|
113
|
+
description: 'Start the server', // one-line, shown in parent's subcommand list
|
|
114
|
+
about: 'Start the development server', // shown at top of this command's help
|
|
115
|
+
longAbout: 'Extended description...', // shown with --help (not -h)
|
|
116
|
+
beforeHelp: 'NOTE: Requires auth.', // text before help output
|
|
117
|
+
afterHelp: 'Examples:\n serve -p 8080', // text after help output
|
|
118
|
+
hidden: false, // hide from parent help
|
|
119
|
+
aliases: ['s', 'start'], // subcommand aliases
|
|
120
|
+
|
|
121
|
+
// Subcommand behavior
|
|
122
|
+
subcommandRequired: true, // error if no subcommand
|
|
123
|
+
inferSubcommands: true, // 'ser' matches 'serve'
|
|
124
|
+
inferLongArgs: true, // '--verb' matches '--verbose'
|
|
125
|
+
allowExternalSubcommands: true, // accept undefined subcommands
|
|
126
|
+
subcommandNegatesReqs: true, // subcommand waives parent required args
|
|
127
|
+
argsConflictsWithSubcommands: true, // args and subcommands mutually exclusive
|
|
128
|
+
argRequiredElseHelp: true, // show help if no args provided
|
|
129
|
+
|
|
130
|
+
// Help customization
|
|
131
|
+
helpTemplate: '{name} v{version}\n{usage}\n{options}',
|
|
132
|
+
},
|
|
133
|
+
args: { /* ... */ },
|
|
134
|
+
subCommands: { /* ... */ },
|
|
135
|
+
groups: [ /* ... */ ],
|
|
136
|
+
setup(ctx) { /* pre-run init */ },
|
|
137
|
+
run(ctx) { /* main handler */ },
|
|
138
|
+
cleanup(ctx) { /* always runs, even on error */ },
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Argument Definition
|
|
143
|
+
|
|
144
|
+
Each argument is defined with an `ArgDef`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const cmd = defineCommand({
|
|
148
|
+
meta: { name: 'tool' },
|
|
149
|
+
args: {
|
|
150
|
+
// String argument
|
|
151
|
+
name: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
short: 'n', // -n
|
|
154
|
+
long: 'name', // --name (defaults to key if omitted)
|
|
155
|
+
description: 'User name',
|
|
156
|
+
required: true,
|
|
157
|
+
valueName: 'NAME', // shown in help: --name <NAME>
|
|
158
|
+
env: 'TOOL_NAME', // fallback to $TOOL_NAME
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Number argument
|
|
162
|
+
port: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
short: 'p',
|
|
165
|
+
default: 3000,
|
|
166
|
+
description: 'Port number',
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Boolean flag
|
|
170
|
+
verbose: {
|
|
171
|
+
type: 'boolean',
|
|
172
|
+
short: 'v',
|
|
173
|
+
description: 'Verbose output',
|
|
174
|
+
negativeDescription: 'Disable verbose output', // help text for --no-verbose
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Enum (restricted values)
|
|
178
|
+
env: {
|
|
179
|
+
type: 'enum',
|
|
180
|
+
short: 'e',
|
|
181
|
+
valueParser: ['dev', 'staging', 'prod'],
|
|
182
|
+
description: 'Environment',
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Positional argument
|
|
186
|
+
file: {
|
|
187
|
+
type: 'positional',
|
|
188
|
+
valueName: 'FILE',
|
|
189
|
+
required: true,
|
|
190
|
+
description: 'Input file path',
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Append action (collect multiple values)
|
|
194
|
+
header: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
short: 'H',
|
|
197
|
+
action: 'append',
|
|
198
|
+
description: 'HTTP headers',
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Count action (-vvv = 3)
|
|
202
|
+
verbosity: {
|
|
203
|
+
type: 'boolean',
|
|
204
|
+
short: 'V',
|
|
205
|
+
action: 'count',
|
|
206
|
+
description: 'Increase verbosity',
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Optional value (--flag or --flag=value)
|
|
210
|
+
level: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
numArgs: { min: 0, max: 1 },
|
|
213
|
+
defaultMissingValue: 'info',
|
|
214
|
+
description: 'Log level (default: info when flag present)',
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Aliases (hidden from help)
|
|
218
|
+
config: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
alias: ['c', 'conf', 'configuration'], // hidden from help
|
|
221
|
+
description: 'Config file path',
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// Visible aliases (shown in help)
|
|
225
|
+
output: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
visibleAlias: ['out', 'o'], // shown in help output
|
|
228
|
+
description: 'Output path',
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Value delimiter (--tags=a,b,c -> ['a', 'b', 'c'])
|
|
232
|
+
tags: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
valueDelimiter: ',',
|
|
235
|
+
description: 'Comma-separated tags',
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// Allow negative numbers (--offset -10)
|
|
239
|
+
offset: {
|
|
240
|
+
type: 'number',
|
|
241
|
+
allowNegativeNumbers: true,
|
|
242
|
+
description: 'Offset (can be negative)',
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Allow hyphen values (--grep -pattern)
|
|
246
|
+
grep: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
allowHyphenValues: true,
|
|
249
|
+
description: 'Search pattern (can start with -)',
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Custom value parser (function)
|
|
253
|
+
port2: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
valueParser: (v) => {
|
|
256
|
+
const n = parseInt(v, 10);
|
|
257
|
+
if (n < 1 || n > 65535) throw new Error('port must be 1-65535');
|
|
258
|
+
return n;
|
|
259
|
+
},
|
|
260
|
+
description: 'Port with range validation',
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// Help heading (group args under custom sections)
|
|
264
|
+
host: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
helpHeading: 'Network',
|
|
267
|
+
description: 'Server host',
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// Hide from specific help modes
|
|
271
|
+
debug: {
|
|
272
|
+
type: 'boolean',
|
|
273
|
+
hideShortHelp: true, // hidden from -h, shown in --help
|
|
274
|
+
description: 'Debug mode',
|
|
275
|
+
},
|
|
276
|
+
internal: {
|
|
277
|
+
type: 'boolean',
|
|
278
|
+
hideLongHelp: true, // hidden from --help, shown in -h
|
|
279
|
+
description: 'Internal flag',
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// Hide possible values from help
|
|
283
|
+
format: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
valueParser: ['json', 'yaml', 'toml'],
|
|
286
|
+
hidePossibleValues: true,
|
|
287
|
+
description: 'Output format',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Argument Constraints
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const cmd = defineCommand({
|
|
297
|
+
meta: { name: 'tool' },
|
|
298
|
+
args: {
|
|
299
|
+
// Mutual exclusion
|
|
300
|
+
json: {
|
|
301
|
+
type: 'boolean',
|
|
302
|
+
conflictsWith: ['yaml', 'table'],
|
|
303
|
+
},
|
|
304
|
+
yaml: { type: 'boolean' },
|
|
305
|
+
table: { type: 'boolean' },
|
|
306
|
+
|
|
307
|
+
// Exclusive: cannot be used with ANY other arg
|
|
308
|
+
init: {
|
|
309
|
+
type: 'boolean',
|
|
310
|
+
exclusive: true,
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// Companion requirement
|
|
314
|
+
'tls-cert': {
|
|
315
|
+
type: 'string',
|
|
316
|
+
requires: ['tls'],
|
|
317
|
+
},
|
|
318
|
+
tls: { type: 'boolean' },
|
|
319
|
+
|
|
320
|
+
// Required unless another arg is present
|
|
321
|
+
file: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
required: true,
|
|
324
|
+
requiredUnlessPresent: 'stdin', // or ['stdin', 'generate']
|
|
325
|
+
},
|
|
326
|
+
stdin: { type: 'boolean' },
|
|
327
|
+
|
|
328
|
+
// Conditionally required
|
|
329
|
+
output: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
requiredIfEq: ['format', 'file'], // required when --format=file
|
|
332
|
+
},
|
|
333
|
+
format: { type: 'string' },
|
|
334
|
+
|
|
335
|
+
// Conditional default
|
|
336
|
+
port: {
|
|
337
|
+
type: 'number',
|
|
338
|
+
defaultValueIf: ['env', 'prod', 443], // default 443 when --env=prod
|
|
339
|
+
},
|
|
340
|
+
env: { type: 'string' },
|
|
341
|
+
|
|
342
|
+
// Value count constraint
|
|
343
|
+
files: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
action: 'append',
|
|
346
|
+
numArgs: { min: 1, max: 10 },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
// Argument groups
|
|
351
|
+
groups: [
|
|
352
|
+
{
|
|
353
|
+
name: 'output-format',
|
|
354
|
+
args: ['json', 'yaml', 'table'],
|
|
355
|
+
required: true, // at least one must be set
|
|
356
|
+
multiple: false, // only one allowed
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Trailing Var Args and Last Positional
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// trailingVarArg: last positional consumes all remaining args
|
|
366
|
+
const exec = defineCommand({
|
|
367
|
+
meta: { name: 'exec' },
|
|
368
|
+
args: {
|
|
369
|
+
cmd: { type: 'positional', valueName: 'CMD' },
|
|
370
|
+
rest: { type: 'positional', valueName: 'ARGS', trailingVarArg: true },
|
|
371
|
+
},
|
|
372
|
+
run({ args }) {
|
|
373
|
+
// exec echo hello world
|
|
374
|
+
// args.cmd = 'echo', args.rest = ['hello', 'world']
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// last: positional only assigned from args after --
|
|
379
|
+
const run = defineCommand({
|
|
380
|
+
meta: { name: 'run' },
|
|
381
|
+
args: {
|
|
382
|
+
verbose: { type: 'boolean' },
|
|
383
|
+
script: { type: 'positional', valueName: 'SCRIPT', last: true },
|
|
384
|
+
},
|
|
385
|
+
run({ args }) {
|
|
386
|
+
// run --verbose -- myscript.sh
|
|
387
|
+
// args.script = 'myscript.sh'
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Subcommands
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const root = defineCommand({
|
|
396
|
+
meta: {
|
|
397
|
+
name: 'app',
|
|
398
|
+
version: '1.0.0',
|
|
399
|
+
inferSubcommands: true, // 'ser' matches 'serve'
|
|
400
|
+
},
|
|
401
|
+
args: {
|
|
402
|
+
verbose: { type: 'boolean', short: 'v', global: true },
|
|
403
|
+
},
|
|
404
|
+
subCommands: {
|
|
405
|
+
serve: defineCommand({
|
|
406
|
+
meta: { name: 'serve', description: 'Start server', aliases: ['s'] },
|
|
407
|
+
args: {
|
|
408
|
+
port: { type: 'number', short: 'p', default: 3000 },
|
|
409
|
+
},
|
|
410
|
+
run({ args }) {
|
|
411
|
+
console.log(`Serving on :${args.port}`);
|
|
412
|
+
},
|
|
413
|
+
}),
|
|
414
|
+
build: defineCommand({
|
|
415
|
+
meta: { name: 'build', description: 'Build project' },
|
|
416
|
+
args: {
|
|
417
|
+
outDir: { type: 'string', default: 'dist' },
|
|
418
|
+
},
|
|
419
|
+
run({ args }) {
|
|
420
|
+
console.log(`Building to ${args.outDir}`);
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
runMain(root);
|
|
427
|
+
// $ app serve -p 8080
|
|
428
|
+
// $ app s -p 8080 (alias)
|
|
429
|
+
// $ app ser -p 8080 (inferred)
|
|
430
|
+
// $ app build --out-dir ./out
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### External Subcommands
|
|
434
|
+
|
|
435
|
+
Accept undefined subcommands and handle them in the parent:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
const git = defineCommand({
|
|
439
|
+
meta: { name: 'git', allowExternalSubcommands: true },
|
|
440
|
+
run({ subCommand, rawArgs }) {
|
|
441
|
+
// $ git my-plugin arg1 arg2
|
|
442
|
+
// subCommand = 'my-plugin', rawArgs = ['arg1', 'arg2']
|
|
443
|
+
console.log(`Running plugin: ${subCommand}`);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Reusable Argument Groups
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { defineArgs, defineCommand } from 'clap-ts';
|
|
452
|
+
|
|
453
|
+
const authArgs = defineArgs({
|
|
454
|
+
user: { type: 'string', short: 'u', env: 'APP_USER' },
|
|
455
|
+
token: { type: 'string', short: 't', env: 'APP_TOKEN', hidden: true },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const loggingArgs = defineArgs({
|
|
459
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
460
|
+
quiet: { type: 'boolean', short: 'q', conflictsWith: ['verbose'] },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const cmd = defineCommand({
|
|
464
|
+
meta: { name: 'deploy' },
|
|
465
|
+
args: {
|
|
466
|
+
...authArgs,
|
|
467
|
+
...loggingArgs,
|
|
468
|
+
target: { type: 'string', required: true },
|
|
469
|
+
},
|
|
470
|
+
run({ args }) {
|
|
471
|
+
// args.user, args.token, args.verbose, args.quiet, args.target
|
|
472
|
+
// all fully typed
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Lifecycle Hooks
|
|
478
|
+
|
|
479
|
+
Commands support setup/run/cleanup lifecycle hooks. `cleanup` always runs, even if `run` throws.
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
const cmd = defineCommand({
|
|
483
|
+
meta: { name: 'server' },
|
|
484
|
+
args: { port: { type: 'number', default: 3000 } },
|
|
485
|
+
|
|
486
|
+
async setup(ctx) {
|
|
487
|
+
ctx.data.db = await connectToDatabase();
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
async run(ctx) {
|
|
491
|
+
const db = ctx.data.db as Database;
|
|
492
|
+
await startServer(ctx.args.port, db);
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
async cleanup(ctx) {
|
|
496
|
+
const db = ctx.data.db as Database;
|
|
497
|
+
await db?.close();
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Custom Styles
|
|
503
|
+
|
|
504
|
+
Override the default terminal colors for help and error output:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { runMain } from 'clap-ts';
|
|
508
|
+
|
|
509
|
+
runMain(rootCommand, {
|
|
510
|
+
styles: {
|
|
511
|
+
heading: (s) => `\x1b[35m${s}\x1b[0m`, // magenta headings
|
|
512
|
+
flag: (s) => `\x1b[36m${s}\x1b[0m`, // cyan flags
|
|
513
|
+
command: (s) => `\x1b[1m${s}\x1b[0m`, // bold commands
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### runMain
|
|
519
|
+
|
|
520
|
+
Entry point for CLI applications. Handles argv parsing, subcommand resolution, validation, help/version, and error display.
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
import { runMain } from 'clap-ts';
|
|
524
|
+
|
|
525
|
+
runMain(rootCommand);
|
|
526
|
+
|
|
527
|
+
runMain(rootCommand, {
|
|
528
|
+
argv: ['serve', '--port', '8080'], // override argv (for testing)
|
|
529
|
+
exit: false, // don't call process.exit (for testing)
|
|
530
|
+
showHelpOnEmpty: true, // show help when no args (default: true)
|
|
531
|
+
styles: { /* custom styles */ }, // override terminal colors
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Low-Level API
|
|
536
|
+
|
|
537
|
+
For advanced use cases, you can use the parser and validator directly:
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
import { parseArgs, validate, renderHelp, CliParseError } from 'clap-ts';
|
|
541
|
+
|
|
542
|
+
const command = defineCommand({
|
|
543
|
+
meta: { name: 'tool' },
|
|
544
|
+
args: { port: { type: 'number', default: 3000 } },
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Parse without validation
|
|
548
|
+
const result = parseArgs(['--port', '8080'], command);
|
|
549
|
+
// result.args, result.positionals, result.rest, result.unknown
|
|
550
|
+
// result.explicitlySet -- Set of arg keys that were explicitly provided
|
|
551
|
+
|
|
552
|
+
// Validate separately
|
|
553
|
+
validate(result, command); // throws CliParseError on failure
|
|
554
|
+
|
|
555
|
+
// Render help text
|
|
556
|
+
const helpText = renderHelp(command);
|
|
557
|
+
|
|
558
|
+
// Render short help (-h style, hides hideShortHelp args)
|
|
559
|
+
const shortHelp = renderHelp(command, undefined, true);
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## Value Precedence
|
|
563
|
+
|
|
564
|
+
Arguments are resolved in this order (highest wins):
|
|
565
|
+
|
|
566
|
+
1. **CLI flags** -- `--port 8080`
|
|
567
|
+
2. **Environment variables** -- `PORT=8080` (when `env: 'PORT'` is set)
|
|
568
|
+
3. **Conditional defaults** -- `defaultValueIf: ['env', 'prod', 443]`
|
|
569
|
+
4. **Static defaults** -- `default: 3000`
|
|
570
|
+
|
|
571
|
+
## Error Messages
|
|
572
|
+
|
|
573
|
+
Errors match clap's format with typo suggestions:
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
error: unexpected argument '--verbos' found
|
|
577
|
+
|
|
578
|
+
tip: a similar argument exists: '--verbose'
|
|
579
|
+
|
|
580
|
+
Usage: my-tool [OPTIONS]
|
|
581
|
+
|
|
582
|
+
For more information, try '--help'.
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
error: the argument '--json' cannot be used with '--yaml'
|
|
587
|
+
|
|
588
|
+
Usage: my-tool [OPTIONS]
|
|
589
|
+
|
|
590
|
+
For more information, try '--help'.
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
error: the following required arguments were not provided:
|
|
595
|
+
--name
|
|
596
|
+
--port
|
|
597
|
+
|
|
598
|
+
Usage: my-tool [OPTIONS]
|
|
599
|
+
|
|
600
|
+
For more information, try '--help'.
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Help Output
|
|
604
|
+
|
|
605
|
+
Help is automatically generated in clap's format, respecting `NO_COLOR`, `TERM=dumb`, and terminal width:
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
A great CLI tool (my-tool v1.0.0)
|
|
609
|
+
|
|
610
|
+
Usage: my-tool [OPTIONS] <FILE> [COMMAND]
|
|
611
|
+
|
|
612
|
+
Arguments:
|
|
613
|
+
<FILE> Input file path [required]
|
|
614
|
+
|
|
615
|
+
Commands:
|
|
616
|
+
serve (s) Start the development server
|
|
617
|
+
build Build the project
|
|
618
|
+
|
|
619
|
+
Options:
|
|
620
|
+
-n, --name <NAME> User name [required]
|
|
621
|
+
-e, --env <ENV> Environment [possible values: dev, staging, prod]
|
|
622
|
+
-p, --port <PORT> Port number [default: 3000] [env: PORT]
|
|
623
|
+
--verbose Enable verbose output
|
|
624
|
+
-h, --help Print help
|
|
625
|
+
-V, --version Print version
|
|
626
|
+
|
|
627
|
+
Network:
|
|
628
|
+
--host <STRING> Server host
|
|
629
|
+
--proxy <STRING> Proxy URL
|
|
630
|
+
|
|
631
|
+
Examples:
|
|
632
|
+
my-tool -n World serve -p 8080
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Help Template
|
|
636
|
+
|
|
637
|
+
Use `helpTemplate` for full control over help layout:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
const cmd = defineCommand({
|
|
641
|
+
meta: {
|
|
642
|
+
name: 'tool',
|
|
643
|
+
version: '1.0.0',
|
|
644
|
+
helpTemplate: `{before-help}{name} v{version}
|
|
645
|
+
|
|
646
|
+
{usage}
|
|
647
|
+
|
|
648
|
+
{all-args}
|
|
649
|
+
{commands}
|
|
650
|
+
{after-help}`,
|
|
651
|
+
},
|
|
652
|
+
// ...
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Placeholders: `{name}`, `{version}`, `{about}`, `{usage}`, `{all-args}`, `{arguments}`, `{options}`, `{commands}`, `{before-help}`, `{after-help}`
|
|
657
|
+
|
|
658
|
+
## Exit Codes
|
|
659
|
+
|
|
660
|
+
| Code | Meaning |
|
|
661
|
+
|------|---------|
|
|
662
|
+
| 0 | Success |
|
|
663
|
+
| 1 | Runtime error (unhandled exception in run/setup/cleanup) |
|
|
664
|
+
| 2 | Usage error (parse failure, validation failure, unknown flags) |
|
|
665
|
+
|
|
666
|
+
## Performance
|
|
667
|
+
|
|
668
|
+
clap-ts is built for performance. Core parsing delegates to the native `node:util parseArgs` and adds a thin layer for type coercion, env fallback, and validation. Flag lookup uses precomputed `Map`s for O(1) resolution. All new feature fields short-circuit on `undefined` -- zero overhead when unused.
|
|
669
|
+
|
|
670
|
+
Benchmarks on Apple M3 Pro (Bun 1.3):
|
|
671
|
+
|
|
672
|
+
| Scenario | Time |
|
|
673
|
+
|----------|------|
|
|
674
|
+
| Minimal (no args) | ~1.7us |
|
|
675
|
+
| Simple (5 flags) | ~11us |
|
|
676
|
+
| Complex (22 flags) | ~16us |
|
|
677
|
+
| Subcommand detection | ~12us |
|
|
678
|
+
| Full pipeline (parse + validate) | ~26us |
|
|
679
|
+
|
|
680
|
+
To run benchmarks yourself:
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
bun run bench/parse.bench.ts
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## Comparison with Rust clap
|
|
687
|
+
|
|
688
|
+
| Feature | clap (Rust) | clap-ts |
|
|
689
|
+
|---------|------------|---------|
|
|
690
|
+
| Boolean/string/number args | Yes | Yes |
|
|
691
|
+
| Short and long flags | Yes | Yes |
|
|
692
|
+
| Flag aliases (hidden + visible) | Yes | Yes |
|
|
693
|
+
| Subcommands with aliases | Yes | Yes |
|
|
694
|
+
| Nested subcommands | Yes | Yes |
|
|
695
|
+
| Global args | Yes | Yes |
|
|
696
|
+
| Required args | Yes | Yes |
|
|
697
|
+
| Default values | Yes | Yes |
|
|
698
|
+
| Conditional defaults (defaultValueIf) | Yes | Yes |
|
|
699
|
+
| Env var fallback | Yes | Yes |
|
|
700
|
+
| conflictsWith | Yes | Yes |
|
|
701
|
+
| requires | Yes | Yes |
|
|
702
|
+
| exclusive | Yes | Yes |
|
|
703
|
+
| requiredUnlessPresent | Yes | Yes |
|
|
704
|
+
| requiredIfEq | Yes | Yes |
|
|
705
|
+
| Argument groups | Yes | Yes |
|
|
706
|
+
| Enum values (valueParser) | Yes | Yes |
|
|
707
|
+
| Custom value parsers (function) | Yes | Yes |
|
|
708
|
+
| numArgs (min/max) | Yes | Yes |
|
|
709
|
+
| valueDelimiter | Yes | Yes |
|
|
710
|
+
| append action | Yes | Yes |
|
|
711
|
+
| count action | Yes | Yes |
|
|
712
|
+
| Boolean negation (--no-X) | Yes | Yes |
|
|
713
|
+
| Positional args | Yes | Yes |
|
|
714
|
+
| trailingVarArg | Yes | Yes |
|
|
715
|
+
| last (positional after --) | Yes | Yes |
|
|
716
|
+
| -- rest separator | Yes | Yes |
|
|
717
|
+
| allowHyphenValues | Yes | Yes |
|
|
718
|
+
| allowNegativeNumbers | Yes | Yes |
|
|
719
|
+
| Typo suggestions | Yes | Yes |
|
|
720
|
+
| inferSubcommands | Yes | Yes |
|
|
721
|
+
| inferLongArgs | Yes | Yes |
|
|
722
|
+
| subcommandRequired | Yes | Yes |
|
|
723
|
+
| subcommandNegatesReqs | Yes | Yes |
|
|
724
|
+
| allowExternalSubcommands | Yes | Yes |
|
|
725
|
+
| argsConflictsWithSubcommands | Yes | Yes |
|
|
726
|
+
| argRequiredElseHelp | Yes | Yes |
|
|
727
|
+
| Colored help output | Yes | Yes |
|
|
728
|
+
| Custom styles | Yes | Yes |
|
|
729
|
+
| beforeHelp / afterHelp | Yes | Yes |
|
|
730
|
+
| helpHeading (option grouping) | Yes | Yes |
|
|
731
|
+
| helpTemplate | Yes | Yes |
|
|
732
|
+
| hideShortHelp / hideLongHelp | Yes | Yes |
|
|
733
|
+
| hidePossibleValues | Yes | Yes |
|
|
734
|
+
| Hidden args/commands | Yes | Yes |
|
|
735
|
+
| Type-safe parsed args | derive macro | generics |
|
|
736
|
+
| Shell completions | Yes | Not yet |
|
|
737
|
+
| Man page generation | Yes | Not yet |
|
|
738
|
+
|
|
739
|
+
## Roadmap
|
|
740
|
+
|
|
741
|
+
- Shell completion generation (bash/zsh/fish)
|
|
742
|
+
- Man page generation
|
|
743
|
+
- Markdown help output
|
|
744
|
+
|
|
745
|
+
## Requirements
|
|
746
|
+
|
|
747
|
+
- Node.js >= 20.0.0 or Bun >= 1.0
|
|
748
|
+
- TypeScript >= 5.0 (for `const` type parameter inference)
|
|
749
|
+
|
|
750
|
+
## License
|
|
751
|
+
|
|
752
|
+
MIT
|