[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

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.

authored by

James Garbutt and committed by
GitHub
(Feb 26, 2025, 1:38 PM +0330) fa066b96 ebb2dcac

+88 -61
+46 -41
demo.citty.ts
··· 60 60 lint: lintCommand, 61 61 } as Record<string, CommandDef<any>>; 62 62 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 = () => { 63 + const completion = await tab(main, { 64 + subCommands: { 65 + lint: { 66 + handler() { 78 67 return [ 79 - { value: '3000', description: 'Development server port' }, 80 - { value: '8080', description: 'Alternative port' }, 68 + { value: 'main.ts', description: 'Main file' }, 69 + { value: 'index.ts', description: 'Index file' }, 81 70 ]; 82 - }; 83 - } 84 - if (o === '--host') { 85 - config.handler = () => { 86 - return [ 87 - { value: 'localhost', description: 'Localhost' }, 88 - { value: '0.0.0.0', description: 'All interfaces' }, 89 - ]; 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() { 94 97 return [ 95 98 { value: 'vite.config.ts', description: 'Vite config file' }, 96 99 { value: 'vite.config.js', description: 'Vite config file' }, 97 100 ]; 98 - }; 99 - } 100 - if (o === '--mode') { 101 - config.handler = () => { 101 + }, 102 + }, 103 + mode: { 104 + handler() { 102 105 return [ 103 106 { value: 'development', description: 'Development mode' }, 104 107 { value: 'production', description: 'Production mode' }, 105 108 ]; 106 - }; 107 - } 108 - if (o === '--logLevel') { 109 - config.handler = () => { 109 + }, 110 + }, 111 + logLevel: { 112 + handler() { 110 113 return [ 111 114 { value: 'info', description: 'Info level' }, 112 115 { value: 'warn', description: 'Warn level' }, 113 116 { value: 'error', description: 'Error level' }, 114 117 { value: 'silent', description: 'Silent level' }, 115 118 ]; 116 - }; 117 - } 118 - } 119 - } 119 + }, 120 + }, 121 + }, 122 + }); 123 + 124 + completion; 120 125 121 126 const cli = createMain(main); 122 127
+41 -19
src/citty.ts
··· 3 3 import * as bash from './bash'; 4 4 import * as fish from './fish'; 5 5 import * as powershell from './powershell'; 6 - import { Completion } from '.'; 6 + import { Completion, type Handler } from '.'; 7 7 import type { 8 8 ArgsDef, 9 9 CommandDef, ··· 30 30 ); 31 31 } 32 32 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( 34 51 completion: Completion, 35 52 subCommands: SubCommandsDef, 36 - parentCmd?: string 53 + parentCmd?: string, 54 + completionConfig?: Record<string, CompletionConfig> 37 55 ) { 38 56 for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { 39 57 const config = await resolve(resolvableConfig); 40 58 const meta = await resolve(config.meta); 41 59 const subCommands = await resolve(config.subCommands); 60 + const subCompletionConfig = completionConfig?.[cmd]; 42 61 43 62 if (!meta || typeof meta?.description !== 'string') { 44 63 throw new Error('Invalid meta or missing description.'); ··· 48 67 cmd, 49 68 meta.description, 50 69 isPositional ? [false] : [], 51 - async (previousArgs, toComplete, endsWithSpace) => { 52 - return []; 53 - }, 70 + subCompletionConfig?.handler ?? noopHandler, 54 71 parentCmd 55 72 ); 56 73 57 74 // Handle nested subcommands recursively 58 75 if (subCommands) { 59 - await handleSubCommands(completion, subCommands, name); 76 + await handleSubCommands( 77 + completion, 78 + subCommands, 79 + name, 80 + subCompletionConfig?.subCommands 81 + ); 60 82 } 61 83 62 84 // Handle arguments ··· 78 100 name, 79 101 `--${argName}`, 80 102 conf.description ?? '', 81 - async (previousArgs, toComplete, endsWithSpace) => { 82 - return []; 83 - }, 103 + subCompletionConfig?.options?.[argName]?.handler ?? noopHandler, 84 104 shortFlag 85 105 ); 86 106 } ··· 88 108 } 89 109 } 90 110 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 93 114 ) { 94 115 const completion = new Completion(); 95 116 ··· 114 135 root, 115 136 meta?.description ?? '', 116 137 isPositional ? [false] : [], 117 - async (previousArgs, toComplete, endsWithSpace) => { 118 - return []; 119 - } 138 + completionConfig?.handler ?? noopHandler 120 139 ); 121 140 122 - await handleSubCommands(completion, subCommands); 141 + await handleSubCommands( 142 + completion, 143 + subCommands, 144 + undefined, 145 + completionConfig?.subCommands 146 + ); 123 147 124 148 if (instance.args) { 125 149 for (const [argName, argConfig] of Object.entries(instance.args)) { ··· 128 152 root, 129 153 `--${argName}`, 130 154 conf.description ?? '', 131 - async (previousArgs, toComplete, endsWithSpace) => { 132 - return []; 133 - } 155 + completionConfig?.options?.[argName]?.handler ?? noopHandler 134 156 ); 135 157 } 136 158 }
+1 -1
src/index.ts
··· 66 66 value: string; 67 67 }; 68 68 69 - type Handler = ( 69 + export type Handler = ( 70 70 previousArgs: string[], 71 71 toComplete: string, 72 72 endsWithSpace: boolean