@boneskull/bargs 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +306 -298
  2. package/dist/bargs.cjs +465 -135
  3. package/dist/bargs.cjs.map +1 -1
  4. package/dist/bargs.d.cts +36 -17
  5. package/dist/bargs.d.cts.map +1 -1
  6. package/dist/bargs.d.ts +36 -17
  7. package/dist/bargs.d.ts.map +1 -1
  8. package/dist/bargs.js +463 -135
  9. package/dist/bargs.js.map +1 -1
  10. package/dist/help.cjs +1 -2
  11. package/dist/help.cjs.map +1 -1
  12. package/dist/help.d.cts +21 -3
  13. package/dist/help.d.cts.map +1 -1
  14. package/dist/help.d.ts +21 -3
  15. package/dist/help.d.ts.map +1 -1
  16. package/dist/help.js +1 -2
  17. package/dist/help.js.map +1 -1
  18. package/dist/index.cjs +27 -31
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +18 -79
  21. package/dist/index.d.cts.map +1 -1
  22. package/dist/index.d.ts +18 -79
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +20 -30
  25. package/dist/index.js.map +1 -1
  26. package/dist/opt.cjs +148 -122
  27. package/dist/opt.cjs.map +1 -1
  28. package/dist/opt.d.cts +85 -113
  29. package/dist/opt.d.cts.map +1 -1
  30. package/dist/opt.d.ts +85 -113
  31. package/dist/opt.d.ts.map +1 -1
  32. package/dist/opt.js +147 -121
  33. package/dist/opt.js.map +1 -1
  34. package/dist/parser.cjs +3 -230
  35. package/dist/parser.cjs.map +1 -1
  36. package/dist/parser.d.cts +3 -51
  37. package/dist/parser.d.cts.map +1 -1
  38. package/dist/parser.d.ts +3 -51
  39. package/dist/parser.d.ts.map +1 -1
  40. package/dist/parser.js +2 -223
  41. package/dist/parser.js.map +1 -1
  42. package/dist/theme.cjs.map +1 -1
  43. package/dist/theme.d.cts +17 -1
  44. package/dist/theme.d.cts.map +1 -1
  45. package/dist/theme.d.ts +17 -1
  46. package/dist/theme.d.ts.map +1 -1
  47. package/dist/theme.js.map +1 -1
  48. package/dist/types.cjs +1 -3
  49. package/dist/types.cjs.map +1 -1
  50. package/dist/types.d.cts +128 -234
  51. package/dist/types.d.cts.map +1 -1
  52. package/dist/types.d.ts +128 -234
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +1 -3
  55. package/dist/types.js.map +1 -1
  56. package/package.json +6 -3
  57. package/dist/validate.cjs +0 -463
  58. package/dist/validate.cjs.map +0 -1
  59. package/dist/validate.d.cts +0 -28
  60. package/dist/validate.d.cts.map +0 -1
  61. package/dist/validate.d.ts +0 -28
  62. package/dist/validate.d.ts.map +0 -1
  63. package/dist/validate.js +0 -459
  64. package/dist/validate.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="/"><img src="./assets/logo.png" width="512px" align="center" alt="bargs: a barg parser"/></a>
3
3
  <h1 align="center"><span class="bargs">⁓ bargs ⁓<span></h1>
4
4
  <p align="center">
5
- <em>“Ex argumentis, veritas”</em>
5
+ <em>"Ex argumentis, veritas"</em>
6
6
  <br/>
7
7
  <small>by <a href="https://github.com/boneskull" title="@boneskull on GitHub">@boneskull</a></small>
8
8
  </p>
@@ -16,311 +16,255 @@ npm install @boneskull/bargs
16
16
 
17
17
  ## Why bargs?
18
18
 
19
- Most argument parsers make you choose: either a simple API with weak types, or a complex and overengineered DSL. **bargs** uses _function helpers_ that instead provide a well-typed and composable API.
19
+ Most argument parsers make you choose: either a simple API with weak types, or a complex and overengineered DSL. **bargs** provides a combinator-style API for building type-safe CLIs—composable schema definitions with full type inference.
20
20
 
21
- ### Type-Safe by Construction
21
+ ## Quick Start
22
22
 
23
- Each helper returns a fully-typed option definition:
23
+ A CLI with an optional command and a couple options:
24
24
 
25
25
  ```typescript
26
- const verbose = bargs.boolean({ aliases: ['v'] });
27
- // Type: BooleanOption & { aliases: ['v'] }
26
+ import { bargs, opt, pos } from '@boneskull/bargs';
27
+
28
+ await bargs
29
+ .create('greet', { version: '1.0.0' })
30
+ .globals(
31
+ opt.options({
32
+ name: opt.string({ default: 'world' }),
33
+ loud: opt.boolean({ aliases: ['l'] }),
34
+ }),
35
+ )
36
+ .command(
37
+ 'say',
38
+ pos.positionals(pos.string({ name: 'message', required: true })),
39
+ ({ positionals, values }) => {
40
+ const [message] = positionals;
41
+ const greeting = `${message}, ${values.name}!`;
42
+ console.log(values.loud ? greeting.toUpperCase() : greeting);
43
+ },
44
+ 'Say a greeting',
45
+ )
46
+ .defaultCommand('say')
47
+ .parseAsync();
48
+ ```
28
49
 
29
- const level = bargs.enum(['low', 'medium', 'high'], { default: 'medium' });
30
- // Type: EnumOption<'low' | 'medium' | 'high'> & { default: 'medium' }
50
+ ```shell
51
+ $ greet Hello --name Alice --loud
52
+ HELLO, ALICE!
31
53
  ```
32
54
 
33
- When you pass these to `bargs()`, the result is always well-typed; options with defaults or `required: true` are non-nullable.
55
+ ## Usage
34
56
 
35
- ### Composable
57
+ ### Type-Safe by Construction
36
58
 
37
- Since helpers are just functions returning objects, composition is trivial:
59
+ Each helper returns a fully-typed definition:
38
60
 
39
61
  ```typescript
40
- // Shared options across commands
41
- const verboseOpt = { verbose: bargs.boolean({ aliases: ['v'] }) };
42
- const outputOpt = {
43
- output: bargs.string({ aliases: ['o'], default: 'stdout' }),
44
- };
45
-
46
- // Merge with spread
47
- const result = bargs({
48
- name: 'tool',
49
- options: {
50
- ...verboseOpt,
51
- ...outputOpt,
52
- format: bargs.enum(['json', 'text']),
53
- },
54
- });
55
- ```
62
+ import { opt, pos } from '@boneskull/bargs';
56
63
 
57
- Or use `bargs.options()` and `bargs.positionals()`:
64
+ const verbose = opt.boolean({ aliases: ['v'] });
65
+ // Type: BooleanOption & { aliases: ['v'] }
58
66
 
59
- ```typescript
60
- // Throws if aliases collide
61
- const sharedOpts = bargs.options(verboseOpt, outputOpt);
67
+ const level = opt.enum(['low', 'medium', 'high'], { default: 'medium' });
68
+ // Type: EnumOption<'low' | 'medium' | 'high'> & { default: 'medium' }
62
69
 
63
- // Combine positionals with type inference
64
- const sharedPos = bargs.positionals(
65
- bargs.stringPos({ name: 'input', required: true }),
66
- bargs.stringPos({ name: 'output' }),
67
- );
70
+ const file = pos.string({ name: 'file', required: true });
71
+ // Type: StringPositional & { name: 'file', required: true }
68
72
  ```
69
73
 
70
- > [!TIP]
71
- > Helper functions are provided for convenience and composability, but you can also just use raw object literals. See the ["tasks" example](./examples/tasks.ts) to see how.
72
-
73
- ### Zero (0) Dependencies
74
+ When you build a CLI with these, the result types flow through automatically—options with defaults or `required: true` are non-nullable.
74
75
 
75
- Only Node.js v22+.
76
+ ### Composable
76
77
 
77
- ## Quick Start
78
+ Options and positionals can be merged using callable parsers:
78
79
 
79
80
  ```typescript
80
- import { bargs } from '@boneskull/bargs';
81
+ import { opt, pos } from '@boneskull/bargs';
81
82
 
82
- const result = bargs({
83
- name: 'greet',
84
- options: {
85
- name: bargs.string({ default: 'world' }),
86
- loud: bargs.boolean({ aliases: ['l'] }),
87
- },
83
+ // Create separate parsers
84
+ const options = opt.options({
85
+ verbose: opt.boolean({ aliases: ['v'] }),
86
+ output: opt.string({ aliases: ['o'], default: 'stdout' }),
88
87
  });
89
88
 
90
- const greeting = `Hello, ${result.values.name}!`;
91
- console.log(result.values.loud ? greeting.toUpperCase() : greeting);
92
- ```
89
+ const positionals = pos.positionals(
90
+ pos.string({ name: 'input', required: true }),
91
+ );
93
92
 
94
- ```shell
95
- $ greet --name Alice --loud
96
- HELLO, ALICE!
93
+ // Merge them: positionals(options) combines both
94
+ const parser = positionals(options);
95
+ // Type: Parser<{ verbose: boolean | undefined, output: string }, [string]>
97
96
  ```
98
97
 
99
- ## Sync vs Async
98
+ ### Simple CLI
100
99
 
101
- **`bargs()`** runs synchronously. If a handler returns a `Promise`, it will break and you will be sorry.
100
+ For a CLI without subcommands, use `.globals()` with merged options and positionals, then handle the result yourself:
102
101
 
103
102
  ```typescript
104
- // Sync - no await needed
105
- const result = bargs({
106
- name: 'my-cli',
107
- options: { verbose: bargs.boolean() },
108
- handler: ({ values }) => {
109
- console.log('Verbose:', values.verbose);
110
- },
111
- });
112
- ```
113
-
114
- Instead, use **`bargsAsync()`**:
115
-
116
- ```typescript
117
- import { bargsAsync } from '@boneskull/bargs';
103
+ import { bargs, opt, pos } from '@boneskull/bargs';
104
+
105
+ // Merge options and positionals into one parser
106
+ // when a positional is variadic, it becomes an array within the result
107
+ const parser = pos.positionals(pos.variadic('string', { name: 'text' }))(
108
+ opt.options({
109
+ uppercase: opt.boolean({ aliases: ['u'], default: false }),
110
+ }),
111
+ );
118
112
 
119
- // Async - handlers can return Promises
120
- const result = await bargsAsync({
121
- name: 'my-cli',
122
- options: { url: bargs.string({ required: true }) },
123
- handler: async ({ values }) => {
124
- const response = await fetch(values.url);
125
- console.log(await response.text());
126
- },
127
- });
113
+ const { values, positionals } = await bargs
114
+ .create('echo', {
115
+ description: 'Echo text to stdout',
116
+ version: '1.0.0',
117
+ })
118
+ .globals(parser)
119
+ .parseAsync();
120
+
121
+ const [words] = positionals;
122
+ const text = words.join(' ');
123
+ console.log(values.uppercase ? text.toUpperCase() : text);
128
124
  ```
129
125
 
130
- ## Commands
126
+ ### Command-Based CLI
131
127
 
132
- Define subcommands with `bargs.command()`:
128
+ For a CLI with multiple subcommands:
133
129
 
134
130
  ```typescript
135
- bargs({
136
- name: 'db',
137
- commands: {
138
- migrate: bargs.command({
139
- description: 'Run database migrations',
140
- options: { dry: bargs.boolean({ aliases: ['n'] }) },
141
- handler: ({ values }) => {
142
- console.log(values.dry ? 'Dry run...' : 'Migrating...');
143
- },
131
+ import { bargs, merge, opt, pos } from '@boneskull/bargs';
132
+
133
+ await bargs
134
+ .create('tasks', {
135
+ description: 'A task manager',
136
+ version: '1.0.0',
137
+ })
138
+ .globals(
139
+ opt.options({
140
+ verbose: opt.boolean({ aliases: ['v'], default: false }),
144
141
  }),
145
- seed: bargs.command({
146
- description: 'Seed the database',
147
- positionals: [bargs.stringPos({ required: true })],
148
- handler: ({ positionals }) => {
149
- const [file] = positionals;
150
- console.log(`Seeding from ${file}...`);
151
- },
142
+ )
143
+ .command(
144
+ 'add',
145
+ // Use merge() to combine positionals with command-specific options
146
+ merge(
147
+ opt.options({
148
+ priority: opt.enum(['low', 'medium', 'high'], { default: 'medium' }),
149
+ }),
150
+ pos.positionals(pos.string({ name: 'text', required: true })),
151
+ ),
152
+ ({ positionals, values }) => {
153
+ const [text] = positionals;
154
+ console.log(`Adding ${values.priority} priority task: ${text}`);
155
+ if (values.verbose) console.log('Verbose mode enabled');
156
+ },
157
+ 'Add a task',
158
+ )
159
+ .command(
160
+ 'list',
161
+ opt.options({
162
+ all: opt.boolean({ default: false }),
152
163
  }),
153
- },
154
- });
164
+ ({ values }) => {
165
+ console.log(values.all ? 'All tasks' : 'Pending tasks');
166
+ },
167
+ 'List tasks',
168
+ )
169
+ .defaultCommand('list')
170
+ .parseAsync();
155
171
  ```
156
172
 
157
173
  ```shell
158
- $ db migrate --dry
159
- Dry run...
174
+ $ tasks add "Buy groceries" --priority high --verbose
175
+ Adding high priority task: Buy groceries
176
+ Verbose mode enabled
160
177
 
161
- $ db seed data.sql
162
- Seeding from data.sql...
178
+ $ tasks list --all
179
+ All tasks
163
180
  ```
164
181
 
165
- ### Default Handler
182
+ ## API
166
183
 
167
- For command-based CLIs, use `defaultHandler` to handle the case when no command is provided:
168
-
169
- ```typescript
170
- bargs({
171
- name: 'git',
172
- commands: {
173
- /* ... */
174
- },
175
- // Run 'status' when no command given
176
- defaultHandler: 'status',
177
- });
184
+ ### bargs.create(name, options?)
178
185
 
179
- // Or provide a custom handler
180
- bargs({
181
- name: 'git',
182
- commands: {
183
- /* ... */
184
- },
185
- defaultHandler: ({ values }) => {
186
- console.log('Run "git --help" for usage');
187
- },
188
- });
189
- ```
186
+ Create a CLI builder.
190
187
 
191
- ## Transforms
188
+ | Option | Type | Description |
189
+ | ------------- | ------------------- | ------------------------------------------- |
190
+ | `description` | `string` | Description shown in help |
191
+ | `version` | `string` | Enables `--version` flag |
192
+ | `epilog` | `string` or `false` | Footer text in help (see [Epilog](#epilog)) |
193
+ | `theme` | `Theme` | Help color theme (see [Theming](#theming)) |
192
194
 
193
- Transforms let you modify parsed values and positionals before they reach your handler. This is useful for:
195
+ ### .globals(parser)
194
196
 
195
- - Loading and merging configuration files
196
- - Adding computed/derived values
197
- - Validating or normalizing inputs
198
- - Async operations like file system checks
197
+ Set global options and transforms that apply to all commands.
199
198
 
200
199
  ```typescript
201
- const result = await bargsAsync({
202
- name: 'my-cli',
203
- options: {
204
- config: bargsAsync.string({ aliases: ['c'] }),
205
- verbose: bargsAsync.boolean({ default: false }),
206
- },
207
- positionals: [bargsAsync.variadic('string', { name: 'files' })],
208
- transforms: {
209
- // Transform option values
210
- values: (values) => {
211
- // Load config file if specified
212
- const fileConfig = values.config
213
- ? JSON.parse(readFileSync(values.config, 'utf8'))
214
- : {};
215
-
216
- return {
217
- ...fileConfig,
218
- ...values,
219
- // Add computed values
220
- timestamp: new Date().toISOString(),
221
- };
222
- },
223
- // Transform positionals
224
- positionals: (positionals) => {
225
- const [files] = positionals;
226
- // Filter to only existing files
227
- return [files.filter((f) => existsSync(f))];
228
- },
229
- },
230
- handler: ({ values, positionals }) => {
231
- // values.timestamp is available here
232
- // positionals contains only valid files
233
- },
234
- });
200
+ bargs.create('my-cli').globals(opt.options({ verbose: opt.boolean() }));
201
+ // ...
235
202
  ```
236
203
 
237
- ### Transform Properties
238
-
239
- Transforms are akin to yargs' "middleware". They execute after parsing but before the handler (if present). Transforms should also be used in place of yargs' "coerce" option.
240
-
241
- | Property | Type | Description |
242
- | ------------- | ----------------------------------------- | --------------------------------- |
243
- | `values` | `(values) => TransformedValues` | Transform parsed option values |
244
- | `positionals` | `(positionals) => TransformedPositionals` | Transform parsed positional tuple |
204
+ ### .command(name, parser, handler, description?)
245
205
 
246
- Both transforms can be sync or async. The return type of each transform becomes the type available in your handler.
247
-
248
- ### Type Inference
249
-
250
- Transforms are fully type-safe. TypeScript infers the transformed types:
206
+ Register a command. The handler receives merged global + command types.
251
207
 
252
208
  ```typescript
253
- bargs({
254
- name: 'example',
255
- options: { count: bargs.number() },
256
- transforms: {
257
- values: (values) => ({
258
- ...values,
259
- doubled: (values.count ?? 0) * 2, // Add computed property
260
- }),
209
+ .command(
210
+ 'build',
211
+ opt.options({ watch: opt.boolean() }),
212
+ ({ values }) => {
213
+ // values has both global options AND { watch: boolean }
214
+ console.log(values.verbose, values.watch);
261
215
  },
262
- handler: ({ values }) => {
263
- console.log(values.doubled); // number - fully typed!
264
- },
265
- });
216
+ 'Build the project',
217
+ )
266
218
  ```
267
219
 
268
- ## Configuration
220
+ ### .defaultCommand(name)
269
221
 
270
- ### Config Properties
222
+ > Or `.defaultCommand(parser, handler)`
271
223
 
272
- | Property | Type | Description |
273
- | ------------- | --------------------- | ------------------------------------------------------------ |
274
- | `name` | `string` | CLI name (required) |
275
- | `description` | `string` | Description shown in help |
276
- | `version` | `string` | Enables `--version` flag |
277
- | `options` | `OptionsSchema` | Named options (`--flag`) |
278
- | `positionals` | `PositionalsSchema` | Positional arguments |
279
- | `commands` | `Record<string, ...>` | Subcommands |
280
- | `transforms` | `TransformsConfig` | Transform values/positionals (see [Transforms](#transforms)) |
281
- | `handler` | `Handler` | Handler function for simple CLIs |
282
- | `epilog` | `string \| false` | Footer text in help (see [Epilog](#epilog)) |
283
- | `args` | `string[]` | Custom args (defaults to `process.argv.slice(2)`) |
224
+ Set the command that runs when no command is specified.
284
225
 
285
226
  ```typescript
286
- bargs({
287
- name: 'my-cli',
288
- description: 'Does amazing things',
289
- version: '1.2.3', // enables --version
290
- args: ['--verbose', 'file.txt'], // useful for testing
291
- options: {
292
- /* ... */
293
- },
294
- });
227
+ // Reference an existing command by name
228
+ .defaultCommand('list')
229
+
230
+ // Or define an inline default
231
+ .defaultCommand(
232
+ pos.positionals(pos.string({ name: 'file' })),
233
+ ({ positionals }) => console.log(positionals[0]),
234
+ )
295
235
  ```
296
236
 
297
- ### Runtime Options
237
+ ### .parse(args?) / .parseAsync(args?)
298
238
 
299
- The second argument to `bargs()` or `bargsAsync()` accepts runtime options:
239
+ Parse arguments and execute handlers.
300
240
 
301
- | Property | Type | Description |
302
- | -------- | ------------ | ---------------------------------------------- |
303
- | `theme` | `ThemeInput` | `--help` Color theme (see [Theming](#theming)) |
241
+ - **`.parse()`** - Synchronous. Throws if any transform or handler returns a Promise.
242
+ - **`.parseAsync()`** - Asynchronous. Supports async transforms and handlers.
304
243
 
305
244
  ```typescript
306
- bargs(config, { theme: 'ocean' });
245
+ // Async (supports async transforms/handlers)
246
+ const result = await bargs.create('my-cli').globals(...).parseAsync();
247
+ console.log(result.values, result.positionals, result.command);
248
+
249
+ // Sync (no async transforms/handlers)
250
+ const result = bargs.create('my-cli').globals(...).parse();
307
251
  ```
308
252
 
309
253
  ## Option Helpers
310
254
 
311
255
  ```typescript
312
- bargs.string({ default: 'value' }); // --name value
313
- bargs.number({ default: 42 }); // --count 42
314
- bargs.boolean({ aliases: ['v'] }); // --verbose, -v
315
- bargs.enum(['a', 'b', 'c']); // --level a
316
- bargs.array('string'); // --file x --file y
317
- bargs.count(); // -vvv → 3
256
+ import { opt } from '@boneskull/bargs';
257
+
258
+ opt.string({ default: 'value' }); // --name value
259
+ opt.number({ default: 42 }); // --count 42
260
+ opt.boolean({ aliases: ['v'] }); // --verbose, -v
261
+ opt.enum(['a', 'b', 'c']); // --level a
262
+ opt.array('string'); // --file x --file y
263
+ opt.count(); // -vvv → 3
318
264
  ```
319
265
 
320
266
  ### Option Properties
321
267
 
322
- All option helpers accept these properties:
323
-
324
268
  | Property | Type | Description |
325
269
  | ------------- | ---------- | ------------------------------------------------ |
326
270
  | `aliases` | `string[]` | Short flags (e.g., `['v']` for `-v`) |
@@ -330,27 +274,27 @@ All option helpers accept these properties:
330
274
  | `hidden` | `boolean` | Hide from `--help` output |
331
275
  | `required` | `boolean` | Mark as required (makes the option non-nullable) |
332
276
 
333
- Example:
277
+ ### `opt.options(schema)`
278
+
279
+ Create a parser from an options schema:
334
280
 
335
281
  ```typescript
336
- bargs.string({
337
- aliases: ['o'],
338
- default: 'output.txt',
339
- description: 'Output file path',
340
- group: 'Output Options',
282
+ const parser = opt.options({
283
+ verbose: opt.boolean({ aliases: ['v'] }),
284
+ output: opt.string({ default: 'out.txt' }),
341
285
  });
342
-
343
- // Hidden options won't appear in help
344
- bargs.boolean({ hidden: true });
286
+ // Type: Parser<{ verbose: boolean | undefined, output: string }, []>
345
287
  ```
346
288
 
347
289
  ## Positional Helpers
348
290
 
349
291
  ```typescript
350
- bargs.stringPos({ required: true }); // <file>
351
- bargs.numberPos({ default: 8080 }); // [port]
352
- bargs.enumPos(['dev', 'prod']); // [env]
353
- bargs.variadic('string'); // [files...]
292
+ import { pos } from '@boneskull/bargs';
293
+
294
+ pos.string({ required: true }); // <file>
295
+ pos.number({ default: 8080 }); // [port]
296
+ pos.enum(['dev', 'prod']); // [env]
297
+ pos.variadic('string'); // [files...]
354
298
  ```
355
299
 
356
300
  ### Positional Properties
@@ -362,41 +306,116 @@ bargs.variadic('string'); // [files...]
362
306
  | `name` | `string` | Display name in help (defaults to `arg0`, `arg1`, ...) |
363
307
  | `required` | `boolean` | Mark as required (shown as `<name>` vs `[name]`) |
364
308
 
365
- Example:
309
+ ### `pos.positionals(...defs)`
310
+
311
+ Create a parser from positional definitions:
366
312
 
367
313
  ```typescript
368
- bargs.stringPos({
369
- name: 'file',
370
- description: 'Input file to process',
371
- required: true,
372
- });
314
+ const parser = pos.positionals(
315
+ pos.string({ name: 'source', required: true }),
316
+ pos.string({ name: 'dest', required: true }),
317
+ );
318
+ // Type: Parser<{}, [string, string]>
373
319
  ```
374
320
 
375
- Positionals are defined as an array and accessed by index:
321
+ Use `variadic` for rest arguments (must be last):
376
322
 
377
323
  ```typescript
378
- const result = bargs({
379
- name: 'cp',
380
- positionals: [
381
- bargs.stringPos({ required: true }), // source
382
- bargs.stringPos({ required: true }), // destination
383
- ],
384
- });
324
+ const parser = pos.positionals(pos.variadic('string', { name: 'files' }));
325
+ // Type: Parser<{}, [string[]]>
326
+ ```
327
+
328
+ ## Merging Parsers
329
+
330
+ Use `merge()` to combine multiple parsers into one:
331
+
332
+ ```typescript
333
+ import { merge, opt, pos } from '@boneskull/bargs';
385
334
 
386
- const [source, dest] = result.positionals;
387
- console.log(`Copying ${source} to ${dest}`);
335
+ const combined = merge(
336
+ opt.options({
337
+ priority: opt.enum(['low', 'medium', 'high'], { default: 'medium' }),
338
+ }),
339
+ pos.positionals(pos.string({ name: 'task', required: true })),
340
+ );
341
+ // Type: Parser<{ priority: 'low' | 'medium' | 'high' }, [string]>
388
342
  ```
389
343
 
390
- Use `variadic` for rest arguments (must be last):
344
+ You can merge as many parsers as needed—options are merged (later overrides earlier), and positionals are concatenated.
345
+
346
+ Alternatively, parsers can be merged by calling one with the other:
391
347
 
392
348
  ```typescript
393
- const result = bargs({
394
- name: 'cat',
395
- positionals: [bargs.variadic('string')],
396
- });
349
+ const options = opt.options({ priority: opt.enum(['low', 'medium', 'high']) });
350
+ const positionals = pos.positionals(
351
+ pos.string({ name: 'task', required: true }),
352
+ );
353
+
354
+ // These are equivalent:
355
+ const combined1 = positionals(options);
356
+ const combined2 = options(positionals);
357
+ ```
397
358
 
398
- const [files] = result.positionals; // string[]
399
- files.forEach((file) => console.log(readFileSync(file, 'utf8')));
359
+ Use whichever style you find more readable.
360
+
361
+ ## Transforms
362
+
363
+ Use `map()` to transform parsed values before they reach your handler:
364
+
365
+ ```typescript
366
+ import { bargs, map, opt } from '@boneskull/bargs';
367
+
368
+ const globals = map(
369
+ opt.options({
370
+ config: opt.string(),
371
+ verbose: opt.boolean({ default: false }),
372
+ }),
373
+ ({ values, positionals }) => ({
374
+ positionals,
375
+ values: {
376
+ ...values,
377
+ // Add computed properties
378
+ timestamp: new Date().toISOString(),
379
+ configLoaded: !!values.config,
380
+ },
381
+ }),
382
+ );
383
+
384
+ await bargs
385
+ .create('my-cli')
386
+ .globals(globals)
387
+ .command(
388
+ 'info',
389
+ opt.options({}),
390
+ ({ values }) => {
391
+ // values.timestamp and values.configLoaded are available
392
+ console.log(values.timestamp);
393
+ },
394
+ 'Show info',
395
+ )
396
+ .parseAsync();
397
+ ```
398
+
399
+ Transforms are fully type-safe—the return type becomes the type available in handlers.
400
+
401
+ ### Async Transforms
402
+
403
+ Transforms can be async:
404
+
405
+ ```typescript
406
+ const globals = map(
407
+ opt.options({ url: opt.string({ required: true }) }),
408
+ async ({ values, positionals }) => {
409
+ const response = await fetch(values.url);
410
+ return {
411
+ positionals,
412
+ values: {
413
+ ...values,
414
+ data: await response.json(),
415
+ },
416
+ };
417
+ },
418
+ );
400
419
  ```
401
420
 
402
421
  ## Epilog
@@ -405,16 +424,12 @@ By default, **bargs** displays your package's homepage and repository URLs (from
405
424
 
406
425
  ```typescript
407
426
  // Custom epilog
408
- bargs({
409
- name: 'my-cli',
427
+ bargs.create('my-cli', {
410
428
  epilog: 'For more info, visit https://example.com',
411
429
  });
412
430
 
413
431
  // Disable epilog entirely
414
- bargs({
415
- name: 'my-cli',
416
- epilog: false,
417
- });
432
+ bargs.create('my-cli', { epilog: false });
418
433
  ```
419
434
 
420
435
  ## Theming
@@ -423,38 +438,23 @@ Customize help output colors with built-in themes or your own:
423
438
 
424
439
  ```typescript
425
440
  // Use a built-in theme: 'default', 'mono', 'ocean', 'warm'
426
- bargs(
427
- {
428
- name: 'my-cli',
429
- options: { verbose: bargs.boolean() },
430
- },
431
- { theme: 'ocean' },
432
- );
441
+ bargs.create('my-cli', { theme: 'ocean' });
433
442
 
434
443
  // Disable colors entirely
435
- bargs(config, { theme: 'mono' });
444
+ bargs.create('my-cli', { theme: 'mono' });
436
445
  ```
437
446
 
438
- The `ansi` export provides common ANSI escape codes for styled terminal output: text styles (`bold`, `dim`, `italic`, `underline`, etc.), foreground colors, background colors, and their `bright*` variants. Use this to create your own themes (instead of hardcoding ANSI escape codes).
447
+ The `ansi` export provides common ANSI escape codes for styled terminal output:
439
448
 
440
449
  ```typescript
441
450
  import { ansi } from '@boneskull/bargs';
442
451
 
443
- bargs(someConfig, {
452
+ bargs.create('my-cli', {
444
453
  theme: {
445
454
  command: ansi.bold,
446
- defaultText: ansi.dim,
447
- defaultValue: ansi.white,
448
- description: ansi.white,
449
- epilog: ansi.dim,
450
- example: ansi.white + ansi.dim,
451
455
  flag: ansi.brightCyan,
452
456
  positional: ansi.magenta,
453
- scriptName: ansi.bold,
454
- sectionHeader: ansi.brightMagenta,
455
- type: ansi.magenta,
456
- url: ansi.cyan,
457
- usage: ansi.cyan,
457
+ // ...
458
458
  },
459
459
  });
460
460
  ```
@@ -495,14 +495,16 @@ import {
495
495
  } from '@boneskull/bargs';
496
496
 
497
497
  try {
498
- bargs(config);
498
+ await bargs.create('my-cli').parseAsync();
499
499
  } catch (error) {
500
500
  if (error instanceof ValidationError) {
501
501
  // Config validation failed (e.g., invalid schema)
502
+ // i.e., "you screwed up"
502
503
  console.error(`Config error at "${error.path}": ${error.message}`);
503
504
  } else if (error instanceof HelpError) {
504
- // User needs guidance (e.g., unknown option)
505
- console.error(error.message);
505
+ // Likely invalid options, command or positionals;
506
+ // re-throw to trigger help display
507
+ throw error;
506
508
  } else if (error instanceof BargsError) {
507
509
  // General bargs error
508
510
  console.error(error.message);
@@ -512,11 +514,12 @@ try {
512
514
 
513
515
  ### Programmatic Help
514
516
 
515
- Generate help text without calling `bargs()`:
517
+ Generate help text programmatically:
516
518
 
517
519
  ```typescript
518
520
  import { generateHelp, generateCommandHelp } from '@boneskull/bargs';
519
521
 
522
+ // These require the internal config structure—see source for details
520
523
  const helpText = generateHelp(config);
521
524
  const commandHelp = generateCommandHelp(config, 'migrate');
522
525
  ```
@@ -558,11 +561,16 @@ console.log(styler.flag('--verbose'));
558
561
 
559
562
  // Strip ANSI codes for plain text output
560
563
  const plain = stripAnsi('\x1b[32m--verbose\x1b[0m'); // '--verbose'
561
-
562
- // Override some colors in a built-in theme
563
- const customTheme = { ...themes.ocean, colors: { flag: ansi.green } };
564
564
  ```
565
565
 
566
+ ### Low-Level Utilities
567
+
568
+ The `handle(parser, fn)` function is exported for advanced use cases where you need to create a `Command` object outside the fluent builder. It's mostly superseded by `.command(name, parser, handler)`.
569
+
570
+ ## Dependencies
571
+
572
+ **bargs** has zero (0) dependencies. Only Node.js v22+.
573
+
566
574
  ## Motivation
567
575
 
568
576
  I've always reached for [yargs](https://github.com/yargs/yargs) in my CLI projects. However, I find myself repeatedly doing the same things; I have a sort of boilerplate in my head, ready to go (`requiresArg: true` and `nargs: 1`, amirite?). I don't want boilerplate in my head. I wanted to distill my chosen subset of yargs' behavior into a composable API. And so **bargs** was begat.