Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 46 additions & 41 deletions demo.citty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,63 +60,68 @@ main.subCommands = {
lint: lintCommand,
} as Record<string, CommandDef<any>>;

const completion = await tab(main);

for (const command of completion.commands.values()) {
if (command.name === 'lint') {
command.handler = () => {
return [
{ value: 'main.ts', description: 'Main file' },
{ value: 'index.ts', description: 'Index file' },
];
};
}

for (const [o, config] of command.options.entries()) {
if (o === '--port') {
config.handler = () => {
return [
{ value: '3000', description: 'Development server port' },
{ value: '8080', description: 'Alternative port' },
];
};
}
if (o === '--host') {
config.handler = () => {
const completion = await tab(main, {
subCommands: {
lint: {
handler() {
return [
{ value: 'localhost', description: 'Localhost' },
{ value: '0.0.0.0', description: 'All interfaces' },
{ value: 'main.ts', description: 'Main file' },
{ value: 'index.ts', description: 'Index file' },
];
};
}
if (o === '--config') {
config.handler = () => {
},
},
dev: {
options: {
port: {
handler() {
return [
{ value: '3000', description: 'Development server port' },
{ value: '8080', description: 'Alternative port' },
];
},
},
host: {
handler() {
return [
{ value: 'localhost', description: 'Localhost' },
{ value: '0.0.0.0', description: 'All interfaces' },
];
},
},
},
},
},
options: {
config: {
handler() {
return [
{ value: 'vite.config.ts', description: 'Vite config file' },
{ value: 'vite.config.js', description: 'Vite config file' },
];
};
}
if (o === '--mode') {
config.handler = () => {
},
},
mode: {
handler() {
return [
{ value: 'development', description: 'Development mode' },
{ value: 'production', description: 'Production mode' },
];
};
}
if (o === '--logLevel') {
config.handler = () => {
},
},
logLevel: {
handler() {
return [
{ value: 'info', description: 'Info level' },
{ value: 'warn', description: 'Warn level' },
{ value: 'error', description: 'Error level' },
{ value: 'silent', description: 'Silent level' },
];
};
}
}
}
},
},
},
});

completion;

const cli = createMain(main);

Expand Down
60 changes: 41 additions & 19 deletions src/citty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as zsh from './zsh';
import * as bash from './bash';
import * as fish from './fish';
import * as powershell from './powershell';
import { Completion } from '.';
import { Completion, type Handler } from '.';
import type {
ArgsDef,
CommandDef,
Expand All @@ -29,15 +29,34 @@ function isConfigPositional<T extends ArgsDef>(config: CommandDef<T>) {
);
}

async function handleSubCommands<T extends ArgsDef = ArgsDef>(
// TODO (43081j): use type inference some day, so we can type-check
// that the sub commands exist, the options exist, etc.
interface CompletionConfig {
handler?: Handler;
subCommands?: Record<string, CompletionConfig>;
options?: Record<
string,
{
handler: Handler;
}
>;
}

const noopHandler: Handler = () => {
return [];
};

async function handleSubCommands(
completion: Completion,
subCommands: SubCommandsDef,
parentCmd?: string
parentCmd?: string,
completionConfig?: Record<string, CompletionConfig>
) {
for (const [cmd, resolvableConfig] of Object.entries(subCommands)) {
const config = await resolve(resolvableConfig);
const meta = await resolve(config.meta);
const subCommands = await resolve(config.subCommands);
const subCompletionConfig = completionConfig?.[cmd];

if (!meta || typeof meta?.description !== 'string') {
throw new Error('Invalid meta or missing description.');
Expand All @@ -47,15 +66,18 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
cmd,
meta.description,
isPositional ? [false] : [],
async (previousArgs, toComplete, endsWithSpace) => {
return [];
},
subCompletionConfig?.handler ?? noopHandler,
parentCmd
);

// Handle nested subcommands recursively
if (subCommands) {
await handleSubCommands(completion, subCommands, name);
await handleSubCommands(
completion,
subCommands,
name,
subCompletionConfig?.subCommands
);
}

// Handle arguments
Expand All @@ -77,18 +99,17 @@ async function handleSubCommands<T extends ArgsDef = ArgsDef>(
name,
`--${argName}`,
conf.description ?? '',
async (previousArgs, toComplete, endsWithSpace) => {
return [];
},
subCompletionConfig?.options?.[argName]?.handler ?? noopHandler,
shortFlag
);
}
}
}
}

export default async function tab<T extends ArgsDef = ArgsDef>(
instance: CommandDef<T>
export default async function tab<TArgs extends ArgsDef>(
instance: CommandDef<TArgs>,
completionConfig?: CompletionConfig
) {
const completion = new Completion();

Expand All @@ -113,12 +134,15 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
root,
meta?.description ?? '',
isPositional ? [false] : [],
async (previousArgs, toComplete, endsWithSpace) => {
return [];
}
completionConfig?.handler ?? noopHandler
);

await handleSubCommands(completion, subCommands);
await handleSubCommands(
completion,
subCommands,
undefined,
completionConfig?.subCommands
);

if (instance.args) {
for (const [argName, argConfig] of Object.entries(instance.args)) {
Expand All @@ -127,9 +151,7 @@ export default async function tab<T extends ArgsDef = ArgsDef>(
root,
`--${argName}`,
conf.description ?? '',
async (previousArgs, toComplete, endsWithSpace) => {
return [];
}
completionConfig?.options?.[argName]?.handler ?? noopHandler
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type Item = {
value: string;
};

type Handler = (
export type Handler = (
previousArgs: string[],
toComplete: string,
endsWithSpace: boolean
Expand Down