Skip to content

Commit ad323c0

Browse files
boneskullclaude
andcommitted
feat!: remove direct CliBuilder pattern for nested commands
Remove the inferior `.command(name, CliBuilder)` overload that lacked type inference for parent globals. The factory pattern `.command(name, factory)` is now the only way to create nested command groups. BREAKING CHANGE: The `.command(name, cliBuilder, options?)` overload has been removed. Users must migrate to the factory pattern: Before: const sub = bargs('sub').command('foo', ...); bargs('main').command('nested', sub, 'desc'); After: bargs('main').command('nested', (nested) => nested.command('foo', ...), 'desc'); The factory pattern provides full type inference for parent globals in nested command handlers, which the direct CliBuilder pattern could not. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a07dc9f commit ad323c0

4 files changed

Lines changed: 14 additions & 213 deletions

File tree

README.md

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,7 @@ Adding origin: https://github.com/...
220220
$ git remote remove origin
221221
```
222222

223-
The factory function receives a `CliBuilder` that already has parent globals typed, so all nested command handlers get full type inference for merged `global + command` options.
224-
225-
You can also pass a pre-built `CliBuilder` directly (see [.command(name, cliBuilder)](#commandname-clibuilder-description)), but handlers won't have parent globals typed at compile time. See `examples/nested-commands.ts` for a full example.
223+
The factory function receives a `CliBuilder` that already has parent globals typed, so all nested command handlers get full type inference for merged `global + command` options. See `examples/nested-commands.ts` for a full example.
226224

227225
## API
228226

@@ -262,24 +260,9 @@ Register a command. The handler receives merged global + command types.
262260
)
263261
```
264262

265-
### .command(name, cliBuilder, description?)
266-
267-
Register a nested command group. The `cliBuilder` is another `CliBuilder` whose commands become subcommands. Parent globals are passed down to nested handlers at runtime, but **handlers won't have parent globals typed** at compile time.
268-
269-
```typescript
270-
const subCommands = bargs('sub').command('foo', ...).command('bar', ...);
271-
272-
bargs('main')
273-
.command('nested', subCommands, 'Nested commands') // nested group
274-
.parseAsync();
275-
276-
// $ main nested foo
277-
// $ main nested bar
278-
```
279-
280263
### .command(name, factory, description?)
281264

282-
Register a nested command group using a factory function. **This is the recommended form** because the factory receives a builder that already has parent globals typed, giving full type inference in nested handlers.
265+
Register a nested command group using a factory function. The factory receives a builder that already has parent globals typed, giving full type inference in nested handlers.
283266

284267
```typescript
285268
bargs('main')

src/bargs.ts

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -550,27 +550,25 @@ const createCliBuilder = <V, P extends readonly unknown[]>(
550550
| Promise<ParseResult<V, P> & { command?: string }>;
551551
},
552552

553-
// Overloaded command(): accepts (name, factory, options?), (name, CliBuilder, options?),
553+
// Overloaded command(): accepts (name, factory, options?),
554554
// (name, Command, options?), or (name, Parser, handler, options?)
555555
command<CV, CP extends readonly unknown[]>(
556556
name: string,
557-
cmdOrParserOrBuilderOrFactory:
557+
cmdOrParserOrFactory:
558558
| ((builder: CliBuilder<V, P>) => CliBuilder<CV, CP>)
559-
| CliBuilder<CV, CP>
560559
| Command<CV, CP>
561560
| Parser<CV, CP>,
562561
handlerOrDescOrOpts?: CommandOptions | HandlerFn<CV & V, CP> | string,
563562
maybeDescOrOpts?: CommandOptions | string,
564563
): CliBuilder<V, P> {
565-
// Form 4: command(name, factory, options?) - factory for nested commands with parent globals
566-
// Check this FIRST before isCliBuilder/isParser since those check for __brand which a plain function won't have
564+
// Form 3: command(name, factory, options?) - factory for nested commands with parent globals
565+
// Check this FIRST before isParser since that checks for __brand which a plain function won't have
567566
if (
568-
typeof cmdOrParserOrBuilderOrFactory === 'function' &&
569-
!isParser(cmdOrParserOrBuilderOrFactory) &&
570-
!isCommand(cmdOrParserOrBuilderOrFactory) &&
571-
!isCliBuilder(cmdOrParserOrBuilderOrFactory)
567+
typeof cmdOrParserOrFactory === 'function' &&
568+
!isParser(cmdOrParserOrFactory) &&
569+
!isCommand(cmdOrParserOrFactory)
572570
) {
573-
const factory = cmdOrParserOrBuilderOrFactory as (
571+
const factory = cmdOrParserOrFactory as (
574572
b: CliBuilder<V, P>,
575573
) => CliBuilder<CV, CP>;
576574
const { aliases, description } = parseCommandOptions(
@@ -602,37 +600,21 @@ const createCliBuilder = <V, P extends readonly unknown[]>(
602600
return this;
603601
}
604602

605-
// Form 3: command(name, CliBuilder, options?) - nested commands
606-
if (isCliBuilder(cmdOrParserOrBuilderOrFactory)) {
607-
const builder = cmdOrParserOrBuilderOrFactory;
608-
const { aliases, description } = parseCommandOptions(
609-
handlerOrDescOrOpts as CommandOptions | string | undefined,
610-
);
611-
state.commands.set(name, {
612-
aliases,
613-
builder: builder as CliBuilder<unknown, readonly unknown[]>,
614-
description,
615-
type: 'nested',
616-
});
617-
registerAliases(state.aliasMap, state.commands, name, aliases);
618-
return this;
619-
}
620-
621603
let cmd: Command<unknown, readonly unknown[]>;
622604
let aliases: string[] | undefined;
623605
let description: string | undefined;
624606

625-
if (isCommand(cmdOrParserOrBuilderOrFactory)) {
607+
if (isCommand(cmdOrParserOrFactory)) {
626608
// Form 1: command(name, Command, options?)
627-
cmd = cmdOrParserOrBuilderOrFactory;
609+
cmd = cmdOrParserOrFactory;
628610
const opts = parseCommandOptions(
629611
handlerOrDescOrOpts as CommandOptions | string | undefined,
630612
);
631613
aliases = opts.aliases;
632614
description = opts.description;
633-
} else if (isParser(cmdOrParserOrBuilderOrFactory)) {
615+
} else if (isParser(cmdOrParserOrFactory)) {
634616
// Form 2: command(name, Parser, handler, options?)
635-
const parser = cmdOrParserOrBuilderOrFactory;
617+
const parser = cmdOrParserOrFactory;
636618
const handler = handlerOrDescOrOpts as HandlerFn<CV & V, CP>;
637619
const opts = parseCommandOptions(maybeDescOrOpts);
638620
aliases = opts.aliases;
@@ -980,27 +962,6 @@ const isParser = (x: unknown): x is Parser<unknown, readonly unknown[]> => {
980962
return '__brand' in obj && obj.__brand === 'Parser';
981963
};
982964

983-
/**
984-
* Check if something is a CliBuilder (has command, globals, parse, parseAsync
985-
* methods).
986-
*
987-
* @function
988-
*/
989-
const isCliBuilder = (
990-
x: unknown,
991-
): x is CliBuilder<unknown, readonly unknown[]> => {
992-
if (x === null || x === undefined || typeof x !== 'object') {
993-
return false;
994-
}
995-
const obj = x as Record<string, unknown>;
996-
return (
997-
typeof obj.command === 'function' &&
998-
typeof obj.globals === 'function' &&
999-
typeof obj.parse === 'function' &&
1000-
typeof obj.parseAsync === 'function'
1001-
);
1002-
};
1003-
1004965
/**
1005966
* Run a simple CLI (no commands).
1006967
*

src/types.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,6 @@ export interface CliBuilder<
8484
options?: CommandOptions | string,
8585
): CliBuilder<TGlobalValues, TGlobalPositionals>;
8686

87-
/**
88-
* Register a nested command group (subcommands).
89-
*
90-
* The nested CliBuilder's commands become subcommands of this command. Parent
91-
* globals are passed down to nested command handlers.
92-
*/
93-
command<CV, CP extends readonly unknown[]>(
94-
name: string,
95-
nestedBuilder: CliBuilder<CV, CP>,
96-
options?: CommandOptions | string,
97-
): CliBuilder<TGlobalValues, TGlobalPositionals>;
98-
9987
/**
10088
* Register a nested command group using a factory function.
10189
*

test/bargs.test.ts

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -406,137 +406,6 @@ describe('positionals', () => {
406406
});
407407
});
408408

409-
describe('nested commands (subcommands)', () => {
410-
it('supports nested commands via CliBuilder', async () => {
411-
let result: unknown;
412-
413-
const remoteCommands = bargs('remote').command(
414-
'add',
415-
pos.positionals(
416-
pos.string({ name: 'name', required: true }),
417-
pos.string({ name: 'url', required: true }),
418-
),
419-
({ positionals }) => {
420-
result = { command: 'remote add', positionals };
421-
},
422-
'Add a remote',
423-
);
424-
425-
const cli = bargs('git').command(
426-
'remote',
427-
remoteCommands,
428-
'Manage remotes',
429-
);
430-
431-
await cli.parseAsync(['remote', 'add', 'origin', 'https://github.com/...']);
432-
433-
expect(result, 'to satisfy', {
434-
command: 'remote add',
435-
positionals: ['origin', 'https://github.com/...'],
436-
});
437-
});
438-
439-
it('supports deeply nested commands', async () => {
440-
let result: unknown;
441-
442-
const setCommands = bargs('set').command(
443-
'url',
444-
pos.positionals(pos.string({ name: 'url', required: true })),
445-
({ positionals }) => {
446-
result = { command: 'remote origin set url', positionals };
447-
},
448-
);
449-
450-
const originCommands = bargs('origin').command(
451-
'set',
452-
setCommands,
453-
'Set properties',
454-
);
455-
456-
const remoteCommands = bargs('remote').command(
457-
'origin',
458-
originCommands,
459-
'Manage origin',
460-
);
461-
462-
const cli = bargs('git').command('remote', remoteCommands);
463-
464-
await cli.parseAsync(['remote', 'origin', 'set', 'url', 'https://new.url']);
465-
466-
expect(result, 'to satisfy', {
467-
command: 'remote origin set url',
468-
positionals: ['https://new.url'],
469-
});
470-
});
471-
472-
it('passes parent globals to nested command handlers', async () => {
473-
let result: unknown;
474-
475-
const remoteCommands = bargs('remote').command(
476-
'add',
477-
pos.positionals(pos.string({ name: 'name', required: true })),
478-
({ positionals, values }) => {
479-
result = { positionals, values };
480-
},
481-
);
482-
483-
const cli = bargs('git')
484-
.globals(opt.options({ verbose: opt.boolean({ aliases: ['v'] }) }))
485-
.command('remote', remoteCommands);
486-
487-
await cli.parseAsync(['--verbose', 'remote', 'add', 'origin']);
488-
489-
expect(result, 'to satisfy', {
490-
positionals: ['origin'],
491-
values: { verbose: true },
492-
});
493-
});
494-
495-
it('runs default subcommand when no subcommand specified', async () => {
496-
let result: unknown;
497-
498-
const remoteCommands = bargs('remote')
499-
.command(
500-
'list',
501-
opt.options({}),
502-
() => {
503-
result = 'list called';
504-
},
505-
'List remotes',
506-
)
507-
.command('add', opt.options({}), () => {
508-
result = 'add called';
509-
})
510-
.defaultCommand('list');
511-
512-
const cli = bargs('git').command('remote', remoteCommands);
513-
514-
await cli.parseAsync(['remote']);
515-
516-
expect(result, 'to be', 'list called');
517-
});
518-
519-
it('generates help listing nested command', () => {
520-
const remoteCommands = bargs('remote')
521-
.command('add', opt.options({}), () => {}, 'Add a remote')
522-
.command('remove', opt.options({}), () => {}, 'Remove a remote');
523-
524-
const cli = bargs('git').command(
525-
'remote',
526-
remoteCommands,
527-
'Manage remotes',
528-
);
529-
530-
// The parent CLI should list 'remote' as a command with its description
531-
// Note: The actual help generation for nested commands is handled by the
532-
// nested builder when --help is passed to it. Here we just verify
533-
// the parent CLI shows the nested command group.
534-
// This is a bit tricky to test since --help calls process.exit(0).
535-
// For now, we verify the nested command is properly registered.
536-
expect(cli, 'to be defined');
537-
});
538-
});
539-
540409
describe('nested commands via factory pattern', () => {
541410
it('supports nested commands via factory function', async () => {
542411
let result: unknown;

0 commit comments

Comments
 (0)