@boneskull/bargs 1.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.
Files changed (58) hide show
  1. package/README.md +305 -299
  2. package/dist/bargs.cjs +464 -142
  3. package/dist/bargs.cjs.map +1 -1
  4. package/dist/bargs.d.cts +35 -17
  5. package/dist/bargs.d.cts.map +1 -1
  6. package/dist/bargs.d.ts +35 -17
  7. package/dist/bargs.d.ts.map +1 -1
  8. package/dist/bargs.js +462 -142
  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 +20 -3
  13. package/dist/help.d.cts.map +1 -1
  14. package/dist/help.d.ts +20 -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 +15 -78
  21. package/dist/index.d.cts.map +1 -1
  22. package/dist/index.d.ts +15 -78
  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 +147 -80
  27. package/dist/opt.cjs.map +1 -1
  28. package/dist/opt.d.cts +88 -77
  29. package/dist/opt.d.cts.map +1 -1
  30. package/dist/opt.d.ts +88 -77
  31. package/dist/opt.d.ts.map +1 -1
  32. package/dist/opt.js +146 -79
  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/types.cjs +1 -3
  43. package/dist/types.cjs.map +1 -1
  44. package/dist/types.d.cts +110 -122
  45. package/dist/types.d.cts.map +1 -1
  46. package/dist/types.d.ts +110 -122
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js +1 -3
  49. package/dist/types.js.map +1 -1
  50. package/package.json +2 -2
  51. package/dist/validate.cjs +0 -463
  52. package/dist/validate.cjs.map +0 -1
  53. package/dist/validate.d.cts +0 -28
  54. package/dist/validate.d.cts.map +0 -1
  55. package/dist/validate.d.ts +0 -28
  56. package/dist/validate.d.ts.map +0 -1
  57. package/dist/validate.js +0 -459
  58. 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,253 @@ 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)` or `.defaultCommand(parser, handler)`
269
221
 
270
- ### Config Properties
271
-
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)`) |
222
+ Set the command that runs when no command is specified.
284
223
 
285
224
  ```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
- });
225
+ // Reference an existing command by name
226
+ .defaultCommand('list')
227
+
228
+ // Or define an inline default
229
+ .defaultCommand(
230
+ pos.positionals(pos.string({ name: 'file' })),
231
+ ({ positionals }) => console.log(positionals[0]),
232
+ )
295
233
  ```
296
234
 
297
- ### Runtime Options
235
+ ### `.parse(args?)` / `.parseAsync(args?)`
298
236
 
299
- The second argument to `bargs()` or `bargsAsync()` accepts runtime options:
237
+ Parse arguments and execute handlers.
300
238
 
301
- | Property | Type | Description |
302
- | -------- | ------------ | ---------------------------------------------- |
303
- | `theme` | `ThemeInput` | `--help` Color theme (see [Theming](#theming)) |
239
+ - **`.parse()`** - Synchronous. Throws if any transform or handler returns a Promise.
240
+ - **`.parseAsync()`** - Asynchronous. Supports async transforms and handlers.
304
241
 
305
242
  ```typescript
306
- bargs(config, { theme: 'ocean' });
243
+ // Async (supports async transforms/handlers)
244
+ const result = await bargs.create('my-cli').globals(...).parseAsync();
245
+ console.log(result.values, result.positionals, result.command);
246
+
247
+ // Sync (no async transforms/handlers)
248
+ const result = bargs.create('my-cli').globals(...).parse();
307
249
  ```
308
250
 
309
251
  ## Option Helpers
310
252
 
311
253
  ```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
254
+ import { opt } from '@boneskull/bargs';
255
+
256
+ opt.string({ default: 'value' }); // --name value
257
+ opt.number({ default: 42 }); // --count 42
258
+ opt.boolean({ aliases: ['v'] }); // --verbose, -v
259
+ opt.enum(['a', 'b', 'c']); // --level a
260
+ opt.array('string'); // --file x --file y
261
+ opt.count(); // -vvv → 3
318
262
  ```
319
263
 
320
264
  ### Option Properties
321
265
 
322
- All option helpers accept these properties:
323
-
324
266
  | Property | Type | Description |
325
267
  | ------------- | ---------- | ------------------------------------------------ |
326
268
  | `aliases` | `string[]` | Short flags (e.g., `['v']` for `-v`) |
@@ -330,27 +272,27 @@ All option helpers accept these properties:
330
272
  | `hidden` | `boolean` | Hide from `--help` output |
331
273
  | `required` | `boolean` | Mark as required (makes the option non-nullable) |
332
274
 
333
- Example:
275
+ ### `opt.options(schema)`
276
+
277
+ Create a parser from an options schema:
334
278
 
335
279
  ```typescript
336
- bargs.string({
337
- aliases: ['o'],
338
- default: 'output.txt',
339
- description: 'Output file path',
340
- group: 'Output Options',
280
+ const parser = opt.options({
281
+ verbose: opt.boolean({ aliases: ['v'] }),
282
+ output: opt.string({ default: 'out.txt' }),
341
283
  });
342
-
343
- // Hidden options won't appear in help
344
- bargs.boolean({ hidden: true });
284
+ // Type: Parser<{ verbose: boolean | undefined, output: string }, []>
345
285
  ```
346
286
 
347
287
  ## Positional Helpers
348
288
 
349
289
  ```typescript
350
- bargs.stringPos({ required: true }); // <file>
351
- bargs.numberPos({ default: 8080 }); // [port]
352
- bargs.enumPos(['dev', 'prod']); // [env]
353
- bargs.variadic('string'); // [files...]
290
+ import { pos } from '@boneskull/bargs';
291
+
292
+ pos.string({ required: true }); // <file>
293
+ pos.number({ default: 8080 }); // [port]
294
+ pos.enum(['dev', 'prod']); // [env]
295
+ pos.variadic('string'); // [files...]
354
296
  ```
355
297
 
356
298
  ### Positional Properties
@@ -362,41 +304,116 @@ bargs.variadic('string'); // [files...]
362
304
  | `name` | `string` | Display name in help (defaults to `arg0`, `arg1`, ...) |
363
305
  | `required` | `boolean` | Mark as required (shown as `<name>` vs `[name]`) |
364
306
 
365
- Example:
307
+ ### `pos.positionals(...defs)`
308
+
309
+ Create a parser from positional definitions:
366
310
 
367
311
  ```typescript
368
- bargs.stringPos({
369
- name: 'file',
370
- description: 'Input file to process',
371
- required: true,
372
- });
312
+ const parser = pos.positionals(
313
+ pos.string({ name: 'source', required: true }),
314
+ pos.string({ name: 'dest', required: true }),
315
+ );
316
+ // Type: Parser<{}, [string, string]>
373
317
  ```
374
318
 
375
- Positionals are defined as an array and accessed by index:
319
+ Use `variadic` for rest arguments (must be last):
376
320
 
377
321
  ```typescript
378
- const result = bargs({
379
- name: 'cp',
380
- positionals: [
381
- bargs.stringPos({ required: true }), // source
382
- bargs.stringPos({ required: true }), // destination
383
- ],
384
- });
322
+ const parser = pos.positionals(pos.variadic('string', { name: 'files' }));
323
+ // Type: Parser<{}, [string[]]>
324
+ ```
325
+
326
+ ## Merging Parsers
327
+
328
+ Use `merge()` to combine multiple parsers into one:
329
+
330
+ ```typescript
331
+ import { merge, opt, pos } from '@boneskull/bargs';
385
332
 
386
- const [source, dest] = result.positionals;
387
- console.log(`Copying ${source} to ${dest}`);
333
+ const combined = merge(
334
+ opt.options({
335
+ priority: opt.enum(['low', 'medium', 'high'], { default: 'medium' }),
336
+ }),
337
+ pos.positionals(pos.string({ name: 'task', required: true })),
338
+ );
339
+ // Type: Parser<{ priority: 'low' | 'medium' | 'high' }, [string]>
388
340
  ```
389
341
 
390
- Use `variadic` for rest arguments (must be last):
342
+ You can merge as many parsers as needed—options are merged (later overrides earlier), and positionals are concatenated.
343
+
344
+ Alternatively, parsers can be merged by calling one with the other:
391
345
 
392
346
  ```typescript
393
- const result = bargs({
394
- name: 'cat',
395
- positionals: [bargs.variadic('string')],
396
- });
347
+ const options = opt.options({ priority: opt.enum(['low', 'medium', 'high']) });
348
+ const positionals = pos.positionals(
349
+ pos.string({ name: 'task', required: true }),
350
+ );
397
351
 
398
- const [files] = result.positionals; // string[]
399
- files.forEach((file) => console.log(readFileSync(file, 'utf8')));
352
+ // These are equivalent:
353
+ const combined1 = positionals(options);
354
+ const combined2 = options(positionals);
355
+ ```
356
+
357
+ Use whichever style you find more readable.
358
+
359
+ ## Transforms
360
+
361
+ Use `map()` to transform parsed values before they reach your handler:
362
+
363
+ ```typescript
364
+ import { bargs, map, opt } from '@boneskull/bargs';
365
+
366
+ const globals = map(
367
+ opt.options({
368
+ config: opt.string(),
369
+ verbose: opt.boolean({ default: false }),
370
+ }),
371
+ ({ values, positionals }) => ({
372
+ positionals,
373
+ values: {
374
+ ...values,
375
+ // Add computed properties
376
+ timestamp: new Date().toISOString(),
377
+ configLoaded: !!values.config,
378
+ },
379
+ }),
380
+ );
381
+
382
+ await bargs
383
+ .create('my-cli')
384
+ .globals(globals)
385
+ .command(
386
+ 'info',
387
+ opt.options({}),
388
+ ({ values }) => {
389
+ // values.timestamp and values.configLoaded are available
390
+ console.log(values.timestamp);
391
+ },
392
+ 'Show info',
393
+ )
394
+ .parseAsync();
395
+ ```
396
+
397
+ Transforms are fully type-safe—the return type becomes the type available in handlers.
398
+
399
+ ### Async Transforms
400
+
401
+ Transforms can be async:
402
+
403
+ ```typescript
404
+ const globals = map(
405
+ opt.options({ url: opt.string({ required: true }) }),
406
+ async ({ values, positionals }) => {
407
+ const response = await fetch(values.url);
408
+ return {
409
+ positionals,
410
+ values: {
411
+ ...values,
412
+ data: await response.json(),
413
+ },
414
+ };
415
+ },
416
+ );
400
417
  ```
401
418
 
402
419
  ## Epilog
@@ -405,16 +422,12 @@ By default, **bargs** displays your package's homepage and repository URLs (from
405
422
 
406
423
  ```typescript
407
424
  // Custom epilog
408
- bargs({
409
- name: 'my-cli',
425
+ bargs.create('my-cli', {
410
426
  epilog: 'For more info, visit https://example.com',
411
427
  });
412
428
 
413
429
  // Disable epilog entirely
414
- bargs({
415
- name: 'my-cli',
416
- epilog: false,
417
- });
430
+ bargs.create('my-cli', { epilog: false });
418
431
  ```
419
432
 
420
433
  ## Theming
@@ -423,38 +436,23 @@ Customize help output colors with built-in themes or your own:
423
436
 
424
437
  ```typescript
425
438
  // 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
- );
439
+ bargs.create('my-cli', { theme: 'ocean' });
433
440
 
434
441
  // Disable colors entirely
435
- bargs(config, { theme: 'mono' });
442
+ bargs.create('my-cli', { theme: 'mono' });
436
443
  ```
437
444
 
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).
445
+ The `ansi` export provides common ANSI escape codes for styled terminal output:
439
446
 
440
447
  ```typescript
441
448
  import { ansi } from '@boneskull/bargs';
442
449
 
443
- bargs(someConfig, {
450
+ bargs.create('my-cli', {
444
451
  theme: {
445
452
  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
453
  flag: ansi.brightCyan,
452
454
  positional: ansi.magenta,
453
- scriptName: ansi.bold,
454
- sectionHeader: ansi.brightMagenta,
455
- type: ansi.magenta,
456
- url: ansi.cyan,
457
- usage: ansi.cyan,
455
+ // ...
458
456
  },
459
457
  });
460
458
  ```
@@ -495,14 +493,16 @@ import {
495
493
  } from '@boneskull/bargs';
496
494
 
497
495
  try {
498
- bargs(config);
496
+ await bargs.create('my-cli').parseAsync();
499
497
  } catch (error) {
500
498
  if (error instanceof ValidationError) {
501
499
  // Config validation failed (e.g., invalid schema)
500
+ // i.e., "you screwed up"
502
501
  console.error(`Config error at "${error.path}": ${error.message}`);
503
502
  } else if (error instanceof HelpError) {
504
- // User needs guidance (e.g., unknown option)
505
- console.error(error.message);
503
+ // Likely invalid options, command or positionals;
504
+ // re-throw to trigger help display
505
+ throw error;
506
506
  } else if (error instanceof BargsError) {
507
507
  // General bargs error
508
508
  console.error(error.message);
@@ -512,11 +512,12 @@ try {
512
512
 
513
513
  ### Programmatic Help
514
514
 
515
- Generate help text without calling `bargs()`:
515
+ Generate help text programmatically:
516
516
 
517
517
  ```typescript
518
518
  import { generateHelp, generateCommandHelp } from '@boneskull/bargs';
519
519
 
520
+ // These require the internal config structure—see source for details
520
521
  const helpText = generateHelp(config);
521
522
  const commandHelp = generateCommandHelp(config, 'migrate');
522
523
  ```
@@ -558,11 +559,16 @@ console.log(styler.flag('--verbose'));
558
559
 
559
560
  // Strip ANSI codes for plain text output
560
561
  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
562
  ```
565
563
 
564
+ ### Low-Level Utilities
565
+
566
+ 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)`.
567
+
568
+ ## Dependencies
569
+
570
+ **bargs** has zero (0) dependencies. Only Node.js v22+.
571
+
566
572
  ## Motivation
567
573
 
568
574
  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.