From 1f9a0c9286e60585adf6d35ef6787c50da59902d Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:28:30 +0000 Subject: [PATCH] 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. --- demo.citty.ts | 87 +++++++++++++++++++++++++++------------------------ src/citty.ts | 60 ++++++++++++++++++++++++----------- src/index.ts | 2 +- 3 files changed, 88 insertions(+), 61 deletions(-) diff --git a/demo.citty.ts b/demo.citty.ts index 7ad5625..1585f44 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -60,63 +60,68 @@ main.subCommands = { lint: lintCommand, } as Record>; -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); diff --git a/src/citty.ts b/src/citty.ts index 4ff6325..5f2936e 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -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, @@ -29,15 +29,34 @@ function isConfigPositional(config: CommandDef) { ); } -async function handleSubCommands( +// 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; + options?: Record< + string, + { + handler: Handler; + } + >; +} + +const noopHandler: Handler = () => { + return []; +}; + +async function handleSubCommands( completion: Completion, subCommands: SubCommandsDef, - parentCmd?: string + parentCmd?: string, + completionConfig?: Record ) { 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.'); @@ -47,15 +66,18 @@ async function handleSubCommands( 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 @@ -77,9 +99,7 @@ async function handleSubCommands( name, `--${argName}`, conf.description ?? '', - async (previousArgs, toComplete, endsWithSpace) => { - return []; - }, + subCompletionConfig?.options?.[argName]?.handler ?? noopHandler, shortFlag ); } @@ -87,8 +107,9 @@ async function handleSubCommands( } } -export default async function tab( - instance: CommandDef +export default async function tab( + instance: CommandDef, + completionConfig?: CompletionConfig ) { const completion = new Completion(); @@ -113,12 +134,15 @@ export default async function tab( 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)) { @@ -127,9 +151,7 @@ export default async function tab( root, `--${argName}`, conf.description ?? '', - async (previousArgs, toComplete, endsWithSpace) => { - return []; - } + completionConfig?.options?.[argName]?.handler ?? noopHandler ); } } diff --git a/src/index.ts b/src/index.ts index 82c1a6b..0cda7e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,7 +66,7 @@ type Item = { value: string; }; -type Handler = ( +export type Handler = ( previousArgs: string[], toComplete: string, endsWithSpace: boolean