Skip to content

Commit fa066b9

Browse files
authored
feat: allow specifying handlers as config (#11)
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 ebb2dca commit fa066b9

File tree

3 files changed

+88
-61
lines changed

3 files changed

+88
-61
lines changed

demo.citty.ts

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,63 +60,68 @@ 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+
},
73+
dev: {
74+
options: {
75+
port: {
76+
handler() {
77+
return [
78+
{ value: '3000', description: 'Development server port' },
79+
{ value: '8080', description: 'Alternative port' },
80+
];
81+
},
82+
},
83+
host: {
84+
handler() {
85+
return [
86+
{ value: 'localhost', description: 'Localhost' },
87+
{ value: '0.0.0.0', description: 'All interfaces' },
88+
];
89+
},
90+
},
91+
},
92+
},
93+
},
94+
options: {
95+
config: {
96+
handler() {
9497
return [
9598
{ value: 'vite.config.ts', description: 'Vite config file' },
9699
{ value: 'vite.config.js', description: 'Vite config file' },
97100
];
98-
};
99-
}
100-
if (o === '--mode') {
101-
config.handler = () => {
101+
},
102+
},
103+
mode: {
104+
handler() {
102105
return [
103106
{ value: 'development', description: 'Development mode' },
104107
{ value: 'production', description: 'Production mode' },
105108
];
106-
};
107-
}
108-
if (o === '--logLevel') {
109-
config.handler = () => {
109+
},
110+
},
111+
logLevel: {
112+
handler() {
110113
return [
111114
{ value: 'info', description: 'Info level' },
112115
{ value: 'warn', description: 'Warn level' },
113116
{ value: 'error', description: 'Error level' },
114117
{ value: 'silent', description: 'Silent level' },
115118
];
116-
};
117-
}
118-
}
119-
}
119+
},
120+
},
121+
},
122+
});
123+
124+
completion;
120125

121126
const cli = createMain(main);
122127

src/citty.ts

Lines changed: 41 additions & 19 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,
@@ -30,15 +30,34 @@ function isConfigPositional<T extends ArgsDef>(config: CommandDef<T>) {
3030
);
3131
}
3232

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

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

5774
// Handle nested subcommands recursively
5875
if (subCommands) {
59-
await handleSubCommands(completion, subCommands, name);
76+
await handleSubCommands(
77+
completion,
78+
subCommands,
79+
name,
80+
subCompletionConfig?.subCommands
81+
);
6082
}
6183

6284
// Handle arguments
@@ -78,18 +100,17 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
78100
name,
79101
`--${argName}`,
80102
conf.description ?? '',
81-
async (previousArgs, toComplete, endsWithSpace) => {
82-
return [];
83-
},
103+
subCompletionConfig?.options?.[argName]?.handler ?? noopHandler,
84104
shortFlag
85105
);
86106
}
87107
}
88108
}
89109
}
90110

91-
export default async function tab<T extends ArgsDef = ArgsDef>(
92-
instance: CommandDef<T>
111+
export default async function tab<TArgs extends ArgsDef>(
112+
instance: CommandDef<TArgs>,
113+
completionConfig?: CompletionConfig
93114
) {
94115
const completion = new Completion();
95116

@@ -114,12 +135,15 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
114135
root,
115136
meta?.description ?? '',
116137
isPositional ? [false] : [],
117-
async (previousArgs, toComplete, endsWithSpace) => {
118-
return [];
119-
}
138+
completionConfig?.handler ?? noopHandler
120139
);
121140

122-
await handleSubCommands(completion, subCommands);
141+
await handleSubCommands(
142+
completion,
143+
subCommands,
144+
undefined,
145+
completionConfig?.subCommands
146+
);
123147

124148
if (instance.args) {
125149
for (const [argName, argConfig] of Object.entries(instance.args)) {
@@ -128,9 +152,7 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
128152
root,
129153
`--${argName}`,
130154
conf.description ?? '',
131-
async (previousArgs, toComplete, endsWithSpace) => {
132-
return [];
133-
}
155+
completionConfig?.options?.[argName]?.handler ?? noopHandler
134156
);
135157
}
136158
}

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)