Skip to content

Commit 4a81c32

Browse files
committed
feat: allow specifying handlers as config
Adds the ability to pass a config object to the citty entry point in order to specify handlers for options/commands. Defining them inline like this means you no longer have to iterate the parsed commands/options. It also means you can safely have options named the same for different sub-commands, and have separate handlers.
1 parent 978fb7b commit 4a81c32

File tree

3 files changed

+83
-58
lines changed

3 files changed

+83
-58
lines changed

demo.citty.ts

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,63 +60,64 @@ main.subCommands = {
6060
lint: lintCommand,
6161
} as Record<string, CommandDef<any>>;
6262

63-
const completion = await tab(main);
64-
65-
for (const command of completion.commands.values()) {
66-
if (command.name === 'lint') {
67-
command.handler = () => {
68-
return [
69-
{ value: 'main.ts', description: 'Main file' },
70-
{ value: 'index.ts', description: 'Index file' },
71-
];
72-
};
73-
}
74-
75-
for (const [o, config] of command.options.entries()) {
76-
if (o === '--port') {
77-
config.handler = () => {
78-
return [
79-
{ value: '3000', description: 'Development server port' },
80-
{ value: '8080', description: 'Alternative port' },
81-
];
82-
};
83-
}
84-
if (o === '--host') {
85-
config.handler = () => {
63+
const completion = await tab(main, {
64+
subCommands: {
65+
lint: {
66+
handler() {
8667
return [
87-
{ value: 'localhost', description: 'Localhost' },
88-
{ value: '0.0.0.0', description: 'All interfaces' },
68+
{ value: 'main.ts', description: 'Main file' },
69+
{ value: 'index.ts', description: 'Index file' },
8970
];
90-
};
91-
}
92-
if (o === '--config') {
93-
config.handler = () => {
71+
},
72+
options: {
73+
port: {
74+
handler() {
75+
return [
76+
{ value: '3000', description: 'Development server port' },
77+
{ value: '8080', description: 'Alternative port' },
78+
];
79+
},
80+
},
81+
host: {
82+
handler() {
83+
return [
84+
{ value: 'localhost', description: 'Localhost' },
85+
{ value: '0.0.0.0', description: 'All interfaces' },
86+
];
87+
},
88+
},
89+
},
90+
},
91+
},
92+
options: {
93+
config: {
94+
handler() {
9495
return [
9596
{ value: 'vite.config.ts', description: 'Vite config file' },
9697
{ value: 'vite.config.js', description: 'Vite config file' },
9798
];
98-
};
99-
}
100-
if (o === '--mode') {
101-
config.handler = () => {
99+
},
100+
},
101+
mode: {
102+
handler() {
102103
return [
103104
{ value: 'development', description: 'Development mode' },
104105
{ value: 'production', description: 'Production mode' },
105106
];
106-
};
107-
}
108-
if (o === '--logLevel') {
109-
config.handler = () => {
107+
},
108+
},
109+
logLevel: {
110+
handler() {
110111
return [
111112
{ value: 'info', description: 'Info level' },
112113
{ value: 'warn', description: 'Warn level' },
113114
{ value: 'error', description: 'Error level' },
114115
{ value: 'silent', description: 'Silent level' },
115116
];
116-
};
117-
}
118-
}
119-
}
117+
},
118+
},
119+
},
120+
});
120121

121122
const cli = createMain(main);
122123

src/citty.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as zsh from './zsh';
33
import * as bash from './bash';
44
import * as fish from './fish';
55
import * as powershell from './powershell';
6-
import { Completion } from '.';
6+
import { Completion, type Handler } from '.';
77
import type {
88
ArgsDef,
99
CommandDef,
@@ -29,15 +29,34 @@ function isConfigPositional<T extends ArgsDef>(config: CommandDef<T>) {
2929
);
3030
}
3131

32-
async function handleSubCommands<T extends ArgsDef = ArgsDef>(
32+
// TODO (43081j): use type inference some day, so we can type-check
33+
// that the sub commands exist, the options exist, etc.
34+
interface CompletionConfig {
35+
handler?: Handler;
36+
subCommands?: Record<string, CompletionConfig>;
37+
options?: Record<
38+
string,
39+
{
40+
handler: Handler;
41+
}
42+
>;
43+
}
44+
45+
const noopHandler: Handler = () => {
46+
return [];
47+
};
48+
49+
async function handleSubCommands(
3350
completion: Completion,
3451
subCommands: SubCommandsDef,
35-
parentCmd?: string
52+
parentCmd?: string,
53+
completionConfig?: Record<string, CompletionConfig>
3654
) {
3755
for (const [cmd, resolvableConfig] of Object.entries(subCommands)) {
3856
const config = await resolve(resolvableConfig);
3957
const meta = await resolve(config.meta);
4058
const subCommands = await resolve(config.subCommands);
59+
const subCompletionConfig = completionConfig?.[cmd];
4160

4261
if (!meta || typeof meta?.description !== 'string') {
4362
throw new Error('Invalid meta or missing description.');
@@ -47,15 +66,18 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
4766
cmd,
4867
meta.description,
4968
isPositional ? [false] : [],
50-
async (previousArgs, toComplete, endsWithSpace) => {
51-
return [];
52-
},
69+
subCompletionConfig?.handler ?? noopHandler,
5370
parentCmd
5471
);
5572

5673
// Handle nested subcommands recursively
5774
if (subCommands) {
58-
await handleSubCommands(completion, subCommands, name);
75+
await handleSubCommands(
76+
completion,
77+
subCommands,
78+
name,
79+
subCompletionConfig?.subCommands
80+
);
5981
}
6082

6183
// Handle arguments
@@ -87,8 +109,9 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
87109
}
88110
}
89111

90-
export default async function tab<T extends ArgsDef = ArgsDef>(
91-
instance: CommandDef<T>
112+
export default async function tab<TArgs extends ArgsDef>(
113+
instance: CommandDef<TArgs>,
114+
completionConfig?: CompletionConfig
92115
) {
93116
const completion = new Completion();
94117

@@ -113,12 +136,15 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
113136
root,
114137
meta?.description ?? '',
115138
isPositional ? [false] : [],
116-
async (previousArgs, toComplete, endsWithSpace) => {
117-
return [];
118-
}
139+
completionConfig.handler ?? noopHandler
119140
);
120141

121-
await handleSubCommands(completion, subCommands);
142+
await handleSubCommands(
143+
completion,
144+
subCommands,
145+
undefined,
146+
completionConfig?.subCommands
147+
);
122148

123149
if (instance.args) {
124150
for (const [argName, argConfig] of Object.entries(instance.args)) {
@@ -127,9 +153,7 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
127153
root,
128154
`--${argName}`,
129155
conf.description ?? '',
130-
async (previousArgs, toComplete, endsWithSpace) => {
131-
return [];
132-
}
156+
completionConfig?.options?.[argName]?.handler ?? noopHandler
133157
);
134158
}
135159
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ type Item = {
6666
value: string;
6767
};
6868

69-
type Handler = (
69+
export type Handler = (
7070
previousArgs: string[],
7171
toComplete: string,
7272
endsWithSpace: boolean

0 commit comments

Comments
 (0)