Skip to content

Commit dbbb0d2

Browse files
boneskullclaude
andcommitted
feat: add enumPos helper, sync/async API, and positional validations
- Add enumPos() helper for enum positional arguments with choice validation - Split API into sync bargs() and async bargsAsync() functions - Add thenable detection to throw if sync handler returns a Promise - Add runtime validation: variadic positional must be last - Add runtime validation: required positionals cannot follow optionals - Add type inference tests for options and positionals with defaults - Update README with positional helpers and enumPos documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cfdbf4e commit dbbb0d2

12 files changed

Lines changed: 1252 additions & 240 deletions

File tree

examples/greeter.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,53 +13,57 @@
1313
* --shout npx tsx examples/greeter.ts World -g "Hey there" npx tsx
1414
* examples/greeter.ts --help
1515
*/
16-
import { bargs, opt } from '../src/index.js';
16+
import { bargs } from '../src/index.js';
1717

18-
const options = opt.options({
19-
greeting: opt.string({
18+
const optionsDef = bargs.options({
19+
greeting: bargs.string({
2020
description: 'The greeting to use',
2121
default: 'Hello',
2222
aliases: ['g'],
2323
}),
24-
shout: opt.boolean({
24+
shout: bargs.boolean({
2525
description: 'SHOUT THE GREETING',
2626
default: false,
2727
aliases: ['s'],
2828
}),
29-
verbose: opt.boolean({
29+
verbose: bargs.boolean({
3030
description: 'Show extra output',
3131
default: false,
3232
aliases: ['v'],
3333
}),
3434
});
3535

36-
const positionalsDef = opt.positionals(
37-
opt.stringPos({ description: 'Name to greet', required: true }),
36+
const positionalsDef = bargs.positionals(
37+
bargs.stringPos({ description: 'Name to greet', required: true }),
3838
);
3939

40-
const result = await bargs({
41-
name: 'greeter',
42-
version: '1.0.0',
43-
description: 'A friendly greeter CLI',
44-
options,
45-
positionals: positionalsDef,
46-
});
40+
const main = async () => {
41+
const result = await bargs({
42+
name: 'greeter',
43+
version: '1.0.0',
44+
description: 'A friendly greeter CLI',
45+
options: optionsDef,
46+
positionals: positionalsDef,
47+
});
48+
49+
// Destructure the result
50+
const { positionals, values } = result;
51+
const [name] = positionals;
52+
const { greeting, shout, verbose } = values;
4753

48-
// Destructure the result
49-
const { positionals, values } = result;
50-
const [name] = positionals;
51-
const { greeting, shout, verbose } = values;
54+
// Build the message
55+
let message = `${greeting}, ${name}!`;
5256

53-
// Build the message
54-
let message = `${greeting}, ${name}!`;
57+
if (shout) {
58+
message = message.toUpperCase();
59+
}
5560

56-
if (shout) {
57-
message = message.toUpperCase();
58-
}
61+
// Output
62+
if (verbose) {
63+
console.log('Configuration:', { greeting, name, shout });
64+
}
5965

60-
// Output
61-
if (verbose) {
62-
console.log('Configuration:', { greeting, name, shout });
63-
}
66+
console.log(message);
67+
};
6468

65-
console.log(message);
69+
void main();

examples/tasks.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
* - Global options (--verbose, --file)
99
* - Command-specific options (--priority for add)
1010
* - Command positionals (task text)
11-
* - Opt.command() for type inference
11+
* - Bargs.command() for type inference
1212
* - DefaultHandler for no-command case
1313
*
1414
* Usage: npx tsx examples/tasks.ts add "Buy groceries" --priority high npx tsx
1515
* examples/tasks.ts list npx tsx examples/tasks.ts done 1 npx tsx
1616
* examples/tasks.ts --help npx tsx examples/tasks.ts add --help
1717
*/
18-
import { bargs, opt } from '../src/index.js';
18+
import bargs from '../src/index.js';
1919

2020
// In-memory task storage (in a real app, this would be a file or database)
2121
interface Task {
@@ -39,36 +39,36 @@ await bargs({
3939

4040
// Global options available to all commands
4141
options: {
42-
file: opt.string({
42+
file: bargs.string({
4343
description: 'Task storage file',
4444
default: 'tasks.json',
4545
aliases: ['f'],
4646
}),
47-
verbose: opt.boolean({
47+
verbose: bargs.boolean({
4848
description: 'Show detailed output',
4949
default: false,
5050
aliases: ['v'],
5151
}),
5252
},
5353

5454
commands: {
55-
add: opt.command({
55+
add: bargs.command({
5656
description: 'Add a new task',
5757
options: {
58-
priority: opt.enum(['low', 'medium', 'high'] as const, {
58+
priority: bargs.enum(['low', 'medium', 'high'], {
5959
description: 'Task priority',
6060
default: 'medium',
6161
aliases: ['p'],
6262
}),
6363
},
6464
positionals: [
65-
opt.stringPos({ description: 'Task description', required: true }),
65+
bargs.stringPos({ description: 'Task description', required: true }),
6666
],
6767
handler: async ({ positionals, values }) => {
6868
const [text] = positionals;
69-
const priority = (values as { priority: 'low' | 'medium' | 'high' })
70-
.priority;
71-
const verbose = (values as { verbose: boolean }).verbose;
69+
// priority is typed from command options
70+
// verbose is accessible via Record<string, unknown>
71+
const { priority, verbose } = values;
7272

7373
const task: Task = {
7474
done: false,
@@ -86,18 +86,18 @@ await bargs({
8686
},
8787
}),
8888

89-
list: opt.command({
89+
list: bargs.command({
9090
description: 'List all tasks',
9191
options: {
92-
all: opt.boolean({
92+
all: bargs.boolean({
9393
description: 'Show completed tasks too',
9494
default: false,
9595
aliases: ['a'],
9696
}),
9797
},
9898
handler: async ({ values }) => {
99-
const all = (values as { all: boolean }).all;
100-
const verbose = (values as { verbose: boolean }).verbose;
99+
// all is typed from command options, verbose from global
100+
const { all, verbose } = values;
101101

102102
const filtered = all ? tasks : tasks.filter((t) => !t.done);
103103

@@ -123,13 +123,16 @@ await bargs({
123123
},
124124
}),
125125

126-
done: opt.command({
126+
done: bargs.command({
127127
description: 'Mark a task as complete',
128-
positionals: [opt.stringPos({ description: 'Task ID', required: true })],
128+
positionals: [
129+
bargs.stringPos({ description: 'Task ID', required: true }),
130+
],
129131
handler: async ({ positionals, values }) => {
130132
const [idStr] = positionals;
131133
const id = parseInt(idStr as string, 10);
132-
const verbose = (values as { verbose: boolean }).verbose;
134+
// verbose is accessible from global options
135+
const { verbose } = values;
133136

134137
const task = tasks.find((t) => t.id === id);
135138
if (!task) {
@@ -151,7 +154,8 @@ await bargs({
151154
// Default handler when no command is given - show task count
152155
defaultHandler: async ({ values }) => {
153156
const pending = tasks.filter((t) => !t.done).length;
154-
if ((values as { verbose: boolean }).verbose) {
157+
// Global options are fully typed in defaultHandler
158+
if (values.verbose) {
155159
console.log(`Tasks: ${tasks.length} total, ${pending} pending`);
156160
} else {
157161
console.log(`${pending} pending task(s)`);

0 commit comments

Comments
 (0)