[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: add pnpm completion script (#23)

* init

* update

* new implementation

* prettier

* feat: autocomplete clis executing through a package manager (#26)

* init

* prettier

* update

* cli completions

* pnpm install

* fix: handle complete command manually

* fix: completion-handler __complete => complete

* fix: remove examples form package.json

* fix: generateCompletionScript function

* move the package manager completion logic directly into the parse method

* prettier

* tab -> t wip

* before some ai changes

* huge wip

* grammar issues

* prettier

---------

Co-authored-by: Mohammad Bagher Abiyat <37929992+Aslemammad@users.noreply.github.com>

authored by

AmirHossein Sakhravi
Mohammad Bagher Abiyat
and committed by
GitHub
(Aug 4, 2025, 7:16 PM +0330) 571f12ee 6c42c55f

+2617 -220
+108
README.2.md
··· 1 + > A video showcasing how pnpm autocompletions work on a test CLI command like `my-cli` 2 + 3 + # tab 4 + 5 + > Instant feedback for your CLI tool when hitting [TAB] in your terminal 6 + 7 + As CLI tooling authors, if we can spare our users a second or two by not checking the documentation or writing the `-h` option, we're doing them a huge favor. The unconscious loves hitting the [TAB] key. It always expects feedback. So it feels disappointing when hitting that key in the terminal but then nothing happens. That frustration is apparent across the whole JavaScript CLI tooling ecosystem. 8 + 9 + Autocompletions are the solution to not break the user's flow. The issue is they're not simple to add. `zsh` expects them in one way, and `bash` in another way. Then where do we provide them so the shell environment parses them? Too many headaches to ease the user's experience. Whether it's worth it or not is out of the question. Because tab is the solution to this complexity. 10 + 11 + `my-cli.ts`: 12 + 13 + ```typescript 14 + import t from '@bombsh/tab'; 15 + 16 + t.name('my-cli'); 17 + 18 + t.command('start', 'start the development server'); 19 + 20 + if (process.argv[2] === 'complete') { 21 + const [shell, ...args] = process.argv.slice(3); 22 + if (shell === '--') { 23 + t.parse(args); 24 + } else { 25 + t.setup(shell, x); 26 + } 27 + } 28 + ``` 29 + 30 + This `my-cli.ts` would be equipped with all the tools required to provide autocompletions. 31 + 32 + ```bash 33 + node my-cli.ts complete -- "st" 34 + ``` 35 + 36 + ``` 37 + start start the development server 38 + :0 39 + ``` 40 + 41 + This output was generated by the `t.parse` method to autocomplete "st" to "start". 42 + 43 + Obviously, the user won't be running that command directly in their terminal. They'd be running something like this. 44 + 45 + ```bash 46 + source <(node my-cli.ts complete zsh) 47 + ``` 48 + 49 + Now whenever the shell sees `my-cli`, it would bring the autocompletions we wrote for this CLI tool. The `node my-cli.ts complete zsh` part would output the zsh script that loads the autocompletions provided by `t.parse` which then would be executed using `source`. 50 + 51 + The autocompletions only live through the current shell session. To set them up across all terminal sessions, the autocompletion script should be injected in the `.zshrc` file. 52 + 53 + ```bash 54 + my-cli complete zsh > ~/completion-for-my-cli.zsh && echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc 55 + ``` 56 + 57 + Or 58 + 59 + ```bash 60 + echo 'source <(npx --offline my-cli complete zsh)' >> ~/.zshrc 61 + ``` 62 + 63 + This is an example of autocompletions on a global CLI command that is usually installed using the `-g` flag (e.g. `npm add -g my-cli`) which is available across the computer. 64 + 65 + --- 66 + 67 + While working on tab, we came to the realization that most JavaScript CLIs are not global CLI commands but rather, per-project dependencies. 68 + 69 + For instance, Vite won't be installed globally and instead it'd be always installed on a project. Here's an example usage: 70 + 71 + ```bash 72 + pnpm vite dev 73 + ``` 74 + 75 + Rather than installing it globally. This example is pretty rare: 76 + 77 + ```bash 78 + vite dev 79 + ``` 80 + 81 + So in this case, a computer might have hundreds of Vite instances each installed separately and potentially from different versions on different projects. 82 + 83 + We were looking for a fluid strategy that would be able to load the autocompletions from each of these dependencies on a per-project basis. 84 + 85 + And that made us develop our own autocompletion abstraction over npm, pnpm and yarn. This would help tab identify which binaries are available in a project and which of these binaries provide autocompletions. So the user would not have to `source` anything or inject any script in their `.zshrc`. 86 + 87 + They'd only have to run this command once and inject it in their shell config. 88 + 89 + ```bash 90 + npx @bombsh/tab pnpm zsh 91 + ``` 92 + 93 + These autocompletions on top of the normal autocompletions that these package managers provide are going to be way more powerful. 94 + 95 + These new autocompletions on top of package managers would help us with autocompletions on commands like `pnpm vite` and other global or per-project binaries. The only requirement would be that the npm binary itself would be a tab-compatible binary. 96 + 97 + What is a tab-compatible binary? It's a tool that provides the `complete` subcommand that was showcased above. Basically any CLI tool that uses tab for its autocompletions is a tab-compatible binary. 98 + 99 + ```bash 100 + pnpm my-cli complete -- 101 + ``` 102 + 103 + ``` 104 + start start the development server 105 + :0 106 + ``` 107 + 108 + We are planning to maintain these package manager autocompletions on our own and turn them into full-fledged autocompletions that touch on every part of our package managers.
-2
README.md
··· 1 - [![tweet-1827921103093932490](https://github.com/user-attachments/assets/21521787-7936-44be-8d3c-8214cd2fcee9)](https://x.com/karpathy/status/1827921103093932490) 2 - 3 1 # tab 4 2 5 3 Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool.
+88
bin/cli.ts
··· 1 + #!/usr/bin/env node 2 + 3 + import cac from 'cac'; 4 + import { script, Completion } from '../src/index.js'; 5 + import tab from '../src/cac.js'; 6 + 7 + import { setupCompletionForPackageManager } from './completion-handlers'; 8 + 9 + const packageManagers = ['npm', 'pnpm', 'yarn', 'bun']; 10 + const shells = ['zsh', 'bash', 'fish', 'powershell']; 11 + 12 + async function main() { 13 + const cli = cac('tab'); 14 + 15 + // TODO: aren't these conditions are already handled by cac? 16 + const args = process.argv.slice(2); 17 + if (args.length >= 2 && args[1] === 'complete') { 18 + const packageManager = args[0]; 19 + 20 + if (!packageManagers.includes(packageManager)) { 21 + console.error(`Error: Unsupported package manager "${packageManager}"`); 22 + console.error( 23 + `Supported package managers: ${packageManagers.join(', ')}` 24 + ); 25 + process.exit(1); 26 + } 27 + 28 + const dashIndex = process.argv.indexOf('--'); 29 + if (dashIndex !== -1) { 30 + // TOOD: there's no Completion anymore 31 + const completion = new Completion(); 32 + setupCompletionForPackageManager(packageManager, completion); 33 + const toComplete = process.argv.slice(dashIndex + 1); 34 + await completion.parse(toComplete); 35 + process.exit(0); 36 + } else { 37 + console.error(`Error: Expected '--' followed by command to complete`); 38 + console.error( 39 + `Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete` 40 + ); 41 + process.exit(1); 42 + } 43 + } 44 + 45 + cli 46 + .command( 47 + '<packageManager> <shell>', 48 + 'Generate shell completion script for a package manager' 49 + ) 50 + .action(async (packageManager, shell) => { 51 + if (!packageManagers.includes(packageManager)) { 52 + console.error(`Error: Unsupported package manager "${packageManager}"`); 53 + console.error( 54 + `Supported package managers: ${packageManagers.join(', ')}` 55 + ); 56 + process.exit(1); 57 + } 58 + 59 + if (!shells.includes(shell)) { 60 + console.error(`Error: Unsupported shell "${shell}"`); 61 + console.error(`Supported shells: ${shells.join(', ')}`); 62 + process.exit(1); 63 + } 64 + 65 + generateCompletionScript(packageManager, shell); 66 + }); 67 + 68 + tab(cli); 69 + 70 + cli.parse(); 71 + } 72 + 73 + // function generateCompletionScript(packageManager: string, shell: string) { 74 + // const name = packageManager; 75 + // const executable = process.env.npm_execpath 76 + // ? `${packageManager} exec @bombsh/tab ${packageManager}` 77 + // : `node ${process.argv[1]} ${packageManager}`; 78 + // script(shell as any, name, executable); 79 + // } 80 + 81 + function generateCompletionScript(packageManager: string, shell: string) { 82 + const name = packageManager; 83 + // this always points at the actual file on disk (TESTING) 84 + const executable = `node ${process.argv[1]} ${packageManager}`; 85 + script(shell as any, name, executable); 86 + } 87 + 88 + main().catch(console.error);
+126
bin/completion-handlers.ts
··· 1 + // TODO: i do not see any completion functionality in this file. nothing is being provided for the defined commands of these package managers. this is a blocker for release. every each of them should be handled. 2 + import { Completion } from '../src/index.js'; 3 + import { execSync } from 'child_process'; 4 + 5 + const DEBUG = false; // for debugging purposes 6 + 7 + function debugLog(...args: any[]) { 8 + if (DEBUG) { 9 + console.error('[DEBUG]', ...args); 10 + } 11 + } 12 + 13 + async function checkCliHasCompletions( 14 + cliName: string, 15 + packageManager: string 16 + ): Promise<boolean> { 17 + try { 18 + debugLog(`Checking if ${cliName} has completions via ${packageManager}`); 19 + const command = `${packageManager} ${cliName} complete --`; 20 + const result = execSync(command, { 21 + encoding: 'utf8', 22 + stdio: ['pipe', 'pipe', 'ignore'], 23 + timeout: 1000, // AMIR: we still havin issues with this, it still hangs if a cli doesn't have completions. longer timeout needed for shell completion system (shell → node → package manager → cli) 24 + }); 25 + const hasCompletions = !!result.trim(); 26 + debugLog(`${cliName} supports completions: ${hasCompletions}`); 27 + return hasCompletions; 28 + } catch (error) { 29 + debugLog(`Error checking completions for ${cliName}:`, error); 30 + return false; 31 + } 32 + } 33 + 34 + async function getCliCompletions( 35 + cliName: string, 36 + packageManager: string, 37 + args: string[] 38 + ): Promise<string[]> { 39 + try { 40 + const completeArgs = args.map((arg) => 41 + arg.includes(' ') ? `"${arg}"` : arg 42 + ); 43 + const completeCommand = `${packageManager} ${cliName} complete -- ${completeArgs.join(' ')}`; 44 + debugLog(`Getting completions with command: ${completeCommand}`); 45 + 46 + const result = execSync(completeCommand, { 47 + encoding: 'utf8', 48 + stdio: ['pipe', 'pipe', 'ignore'], 49 + timeout: 1000, // same: longer timeout needed for shell completion system (shell → node → package manager → cli) 50 + }); 51 + 52 + const completions = result.trim().split('\n').filter(Boolean); 53 + debugLog(`Got ${completions.length} completions from ${cliName}`); 54 + return completions; 55 + } catch (error) { 56 + debugLog(`Error getting completions from ${cliName}:`, error); 57 + return []; 58 + } 59 + } 60 + 61 + export function setupCompletionForPackageManager( 62 + packageManager: string, 63 + completion: Completion 64 + ) { 65 + if (packageManager === 'pnpm') { 66 + setupPnpmCompletions(completion); 67 + } else if (packageManager === 'npm') { 68 + setupNpmCompletions(completion); 69 + } else if (packageManager === 'yarn') { 70 + setupYarnCompletions(completion); 71 + } else if (packageManager === 'bun') { 72 + setupBunCompletions(completion); 73 + } 74 + 75 + // TODO: the core functionality of tab should have nothing related to package managers. even though completion is not there anymore, but this is something to consider. 76 + completion.setPackageManager(packageManager); 77 + } 78 + 79 + export function setupPnpmCompletions(completion: Completion) { 80 + completion.addCommand('add', 'Install a package', [], async () => []); 81 + completion.addCommand('remove', 'Remove a package', [], async () => []); 82 + completion.addCommand( 83 + 'install', 84 + 'Install all dependencies', 85 + [], 86 + async () => [] 87 + ); 88 + // TODO: empty functions should be replaced with noop functions rather than creating that many empty functions 89 + completion.addCommand('update', 'Update packages', [], async () => []); 90 + completion.addCommand('exec', 'Execute a command', [], async () => []); 91 + completion.addCommand('run', 'Run a script', [], async () => []); 92 + completion.addCommand('publish', 'Publish package', [], async () => []); 93 + completion.addCommand('test', 'Run tests', [], async () => []); 94 + completion.addCommand('build', 'Build project', [], async () => []); 95 + } 96 + 97 + export function setupNpmCompletions(completion: Completion) { 98 + completion.addCommand('install', 'Install a package', [], async () => []); 99 + completion.addCommand('uninstall', 'Uninstall a package', [], async () => []); 100 + completion.addCommand('run', 'Run a script', [], async () => []); 101 + completion.addCommand('test', 'Run tests', [], async () => []); 102 + completion.addCommand('publish', 'Publish package', [], async () => []); 103 + completion.addCommand('update', 'Update packages', [], async () => []); 104 + completion.addCommand('start', 'Start the application', [], async () => []); 105 + completion.addCommand('build', 'Build project', [], async () => []); 106 + } 107 + 108 + export function setupYarnCompletions(completion: Completion) { 109 + completion.addCommand('add', 'Add a package', [], async () => []); 110 + completion.addCommand('remove', 'Remove a package', [], async () => []); 111 + completion.addCommand('run', 'Run a script', [], async () => []); 112 + completion.addCommand('test', 'Run tests', [], async () => []); 113 + completion.addCommand('publish', 'Publish package', [], async () => []); 114 + completion.addCommand('install', 'Install dependencies', [], async () => []); 115 + completion.addCommand('build', 'Build project', [], async () => []); 116 + } 117 + 118 + export function setupBunCompletions(completion: Completion) { 119 + completion.addCommand('add', 'Add a package', [], async () => []); 120 + completion.addCommand('remove', 'Remove a package', [], async () => []); 121 + completion.addCommand('run', 'Run a script', [], async () => []); 122 + completion.addCommand('test', 'Run tests', [], async () => []); 123 + completion.addCommand('install', 'Install dependencies', [], async () => []); 124 + completion.addCommand('update', 'Update packages', [], async () => []); 125 + completion.addCommand('build', 'Build project', [], async () => []); 126 + }
+70
examples/demo-cli-cac/demo-cli-cac.js
··· 1 + #!/usr/bin/env node 2 + 3 + import cac from 'cac'; 4 + import tab from '../../dist/src/cac.js'; 5 + 6 + const cli = cac('demo-cli-cac'); 7 + 8 + // Define version and help 9 + cli.version('1.0.0'); 10 + cli.help(); 11 + 12 + // Global options 13 + cli.option('-c, --config <file>', 'Specify config file'); 14 + cli.option('-d, --debug', 'Enable debugging'); 15 + 16 + // Start command 17 + cli 18 + .command('start', 'Start the application') 19 + .option('-p, --port <port>', 'Port to use', { default: '3000' }) 20 + .action((options) => { 21 + console.log('Starting application...'); 22 + console.log('Options:', options); 23 + }); 24 + 25 + // Build command 26 + cli 27 + .command('build', 'Build the application') 28 + .option('-m, --mode <mode>', 'Build mode', { default: 'production' }) 29 + .action((options) => { 30 + console.log('Building application...'); 31 + console.log('Options:', options); 32 + }); 33 + 34 + // Set up completion using the cac adapter 35 + const completion = await tab(cli); 36 + 37 + // custom config for options 38 + for (const command of completion.commands.values()) { 39 + for (const [optionName, config] of command.options.entries()) { 40 + if (optionName === '--port') { 41 + config.handler = () => { 42 + return [ 43 + { value: '3000', description: 'Default port' }, 44 + { value: '8080', description: 'Alternative port' }, 45 + ]; 46 + }; 47 + } 48 + 49 + if (optionName === '--mode') { 50 + config.handler = () => { 51 + return [ 52 + { value: 'development', description: 'Development mode' }, 53 + { value: 'production', description: 'Production mode' }, 54 + { value: 'test', description: 'Test mode' }, 55 + ]; 56 + }; 57 + } 58 + 59 + if (optionName === '--config') { 60 + config.handler = () => { 61 + return [ 62 + { value: 'config.json', description: 'JSON config file' }, 63 + { value: 'config.js', description: 'JavaScript config file' }, 64 + ]; 65 + }; 66 + } 67 + } 68 + } 69 + 70 + cli.parse();
+16
examples/demo-cli-cac/package.json
··· 1 + { 2 + "name": "demo-cli-cac", 3 + "version": "1.0.0", 4 + "description": "Demo CLI using CAC for testing tab completions with pnpm", 5 + "main": "demo-cli-cac.js", 6 + "type": "module", 7 + "bin": { 8 + "demo-cli-cac": "./demo-cli-cac.js" 9 + }, 10 + "scripts": { 11 + "start": "node demo-cli-cac.js" 12 + }, 13 + "dependencies": { 14 + "cac": "^6.7.14" 15 + } 16 + }
+23
examples/demo-cli-cac/pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + cac: 12 + specifier: ^6.7.14 13 + version: 6.7.14 14 + 15 + packages: 16 + 17 + cac@6.7.14: 18 + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 19 + engines: {node: '>=8'} 20 + 21 + snapshots: 22 + 23 + cac@6.7.14: {}
+81 -57
examples/demo.cac.ts
··· 1 1 import cac from 'cac'; 2 2 import tab from '../src/cac'; 3 + import type { Command, Option, OptionsMap } from '../src/t'; 3 4 4 5 const cli = cac('vite'); 5 6 ··· 22 23 23 24 cli.command('dev build', 'Build project').action((options) => {}); 24 25 26 + cli 27 + .command('copy <source> <destination>', 'Copy files') 28 + .action((source, destination, options) => {}); 29 + 25 30 cli.command('lint [...files]', 'Lint project').action((files, options) => {}); 26 31 27 - const completion = await tab(cli); 28 - 29 - for (const command of completion.commands.values()) { 30 - if (command.name === 'lint') { 31 - command.handler = () => { 32 - return [ 33 - { value: 'main.ts', description: 'Main file' }, 34 - { value: 'index.ts', description: 'Index file' }, 35 - ]; 36 - }; 37 - } 38 - 39 - for (const [o, config] of command.options.entries()) { 40 - if (o === '--port') { 41 - config.handler = () => { 42 - return [ 43 - { value: '3000', description: 'Development server port' }, 44 - { value: '8080', description: 'Alternative port' }, 45 - ]; 46 - }; 47 - } 48 - if (o === '--host') { 49 - config.handler = () => { 50 - return [ 51 - { value: 'localhost', description: 'Localhost' }, 52 - { value: '0.0.0.0', description: 'All interfaces' }, 53 - ]; 54 - }; 55 - } 56 - if (o === '--config') { 57 - config.handler = () => { 58 - return [ 59 - { value: 'vite.config.ts', description: 'Vite config file' }, 60 - { value: 'vite.config.js', description: 'Vite config file' }, 61 - ]; 62 - }; 63 - } 64 - if (o === '--mode') { 65 - config.handler = () => { 66 - return [ 67 - { value: 'development', description: 'Development mode' }, 68 - { value: 'production', description: 'Production mode' }, 69 - ]; 70 - }; 71 - } 72 - if (o === '--logLevel') { 73 - config.handler = () => { 74 - return [ 75 - { value: 'info', description: 'Info level' }, 76 - { value: 'warn', description: 'Warn level' }, 77 - { value: 'error', description: 'Error level' }, 78 - { value: 'silent', description: 'Silent level' }, 79 - ]; 80 - }; 81 - } 82 - } 83 - } 32 + // Note: With the new t.ts API, handlers are configured through the completionConfig parameter 33 + // rather than by modifying the returned completion object directly 34 + await tab(cli, { 35 + subCommands: { 36 + copy: { 37 + args: { 38 + source: function (complete) { 39 + complete('src/', 'Source directory'); 40 + complete('dist/', 'Distribution directory'); 41 + complete('public/', 'Public assets'); 42 + }, 43 + destination: function (complete) { 44 + complete('build/', 'Build output'); 45 + complete('release/', 'Release directory'); 46 + complete('backup/', 'Backup location'); 47 + }, 48 + }, 49 + }, 50 + lint: { 51 + args: { 52 + files: function (complete) { 53 + complete('main.ts', 'Main file'); 54 + complete('index.ts', 'Index file'); 55 + }, 56 + }, 57 + }, 58 + dev: { 59 + options: { 60 + port: function ( 61 + this: Option, 62 + complete: (value: string, description: string) => void, 63 + options: OptionsMap 64 + ) { 65 + complete('3000', 'Development server port'); 66 + complete('8080', 'Alternative port'); 67 + }, 68 + host: function ( 69 + this: Option, 70 + complete: (value: string, description: string) => void, 71 + options: OptionsMap 72 + ) { 73 + complete('localhost', 'Localhost'); 74 + complete('0.0.0.0', 'All interfaces'); 75 + }, 76 + }, 77 + }, 78 + }, 79 + options: { 80 + config: function ( 81 + this: Option, 82 + complete: (value: string, description: string) => void, 83 + options: OptionsMap 84 + ) { 85 + complete('vite.config.ts', 'Vite config file'); 86 + complete('vite.config.js', 'Vite config file'); 87 + }, 88 + mode: function ( 89 + this: Option, 90 + complete: (value: string, description: string) => void, 91 + options: OptionsMap 92 + ) { 93 + complete('development', 'Development mode'); 94 + complete('production', 'Production mode'); 95 + }, 96 + logLevel: function ( 97 + this: Option, 98 + complete: (value: string, description: string) => void, 99 + options: OptionsMap 100 + ) { 101 + complete('info', 'Info level'); 102 + complete('warn', 'Warn level'); 103 + complete('error', 'Error level'); 104 + complete('silent', 'Silent level'); 105 + }, 106 + }, 107 + }); 84 108 85 109 cli.parse();
+76 -32
examples/demo.citty.ts
··· 1 - import { defineCommand, createMain, CommandDef, ArgsDef } from 'citty'; 1 + import { 2 + defineCommand, 3 + createMain, 4 + type CommandDef, 5 + type ArgsDef, 6 + } from 'citty'; 2 7 import tab from '../src/citty'; 3 8 4 9 const main = defineCommand({ ··· 8 13 description: 'Vite CLI', 9 14 }, 10 15 args: { 16 + project: { 17 + type: 'positional', 18 + description: 'Project name', 19 + required: true, 20 + }, 11 21 config: { 12 22 type: 'string', 13 23 description: 'Use specified config file', ··· 55 65 run: () => {}, 56 66 }); 57 67 68 + const copyCommand = defineCommand({ 69 + meta: { 70 + name: 'copy', 71 + description: 'Copy files', 72 + }, 73 + args: { 74 + source: { 75 + type: 'positional', 76 + description: 'Source file or directory', 77 + required: true, 78 + }, 79 + destination: { 80 + type: 'positional', 81 + description: 'Destination file or directory', 82 + required: true, 83 + }, 84 + }, 85 + run: () => {}, 86 + }); 87 + 58 88 const lintCommand = defineCommand({ 59 89 meta: { 60 90 name: 'lint', ··· 73 103 main.subCommands = { 74 104 dev: devCommand, 75 105 build: buildCommand, 106 + copy: copyCommand, 76 107 lint: lintCommand, 77 108 } as Record<string, CommandDef<ArgsDef>>; 78 109 79 110 const completion = await tab(main, { 111 + args: { 112 + project: function (complete) { 113 + complete('my-app', 'My application'); 114 + complete('my-lib', 'My library'); 115 + complete('my-tool', 'My tool'); 116 + }, 117 + }, 80 118 options: { 81 - config: { 82 - handler: () => [ 83 - { value: 'vite.config.ts', description: 'Vite config file' }, 84 - { value: 'vite.config.js', description: 'Vite config file' }, 85 - ], 119 + config: function (this: any, complete) { 120 + complete('vite.config.ts', 'Vite config file'); 121 + complete('vite.config.js', 'Vite config file'); 86 122 }, 87 - mode: { 88 - handler: () => [ 89 - { value: 'development', description: 'Development mode' }, 90 - { value: 'production', description: 'Production mode' }, 91 - ], 123 + mode: function (this: any, complete) { 124 + complete('development', 'Development mode'); 125 + complete('production', 'Production mode'); 92 126 }, 93 - logLevel: { 94 - handler: () => [ 95 - { value: 'info', description: 'Info level' }, 96 - { value: 'warn', description: 'Warn level' }, 97 - { value: 'error', description: 'Error level' }, 98 - { value: 'silent', description: 'Silent level' }, 99 - ], 127 + logLevel: function (this: any, complete) { 128 + complete('info', 'Info level'); 129 + complete('warn', 'Warn level'); 130 + complete('error', 'Error level'); 131 + complete('silent', 'Silent level'); 100 132 }, 101 133 }, 102 134 103 135 subCommands: { 136 + copy: { 137 + args: { 138 + source: function (complete) { 139 + complete('src/', 'Source directory'); 140 + complete('dist/', 'Distribution directory'); 141 + complete('public/', 'Public assets'); 142 + }, 143 + destination: function (complete) { 144 + complete('build/', 'Build output'); 145 + complete('release/', 'Release directory'); 146 + complete('backup/', 'Backup location'); 147 + }, 148 + }, 149 + }, 104 150 lint: { 105 - handler: () => [ 106 - { value: 'main.ts', description: 'Main file' }, 107 - { value: 'index.ts', description: 'Index file' }, 108 - ], 151 + args: { 152 + files: function (complete) { 153 + complete('main.ts', 'Main file'); 154 + complete('index.ts', 'Index file'); 155 + }, 156 + }, 109 157 }, 110 158 dev: { 111 159 options: { 112 - port: { 113 - handler: () => [ 114 - { value: '3000', description: 'Development server port' }, 115 - { value: '8080', description: 'Alternative port' }, 116 - ], 160 + port: function (this: any, complete) { 161 + complete('3000', 'Development server port'); 162 + complete('8080', 'Alternative port'); 117 163 }, 118 - host: { 119 - handler: () => [ 120 - { value: 'localhost', description: 'Localhost' }, 121 - { value: '0.0.0.0', description: 'All interfaces' }, 122 - ], 164 + host: function (this: any, complete) { 165 + complete('localhost', 'Localhost'); 166 + complete('0.0.0.0', 'All interfaces'); 123 167 }, 124 168 }, 125 169 },
+2 -11
examples/demo.commander.ts
··· 107 107 } 108 108 } 109 109 110 - // Test completion directly if the first argument is "test-completion" 111 - if (process.argv[2] === 'test-completion') { 112 - const args = process.argv.slice(3); 113 - console.log('Testing completion with args:', args); 114 - completion.parse(args).then(() => { 115 - // Done 116 - }); 117 - } else { 118 - // Parse command line arguments 119 - program.parse(); 120 - } 110 + // Parse command line arguments 111 + program.parse();
+132
examples/demo.t.ts
··· 1 + import t from '../src/t'; 2 + 3 + // Global options 4 + t.option( 5 + 'config', 6 + 'Use specified config file', 7 + function (complete) { 8 + complete('vite.config.ts', 'Vite config file'); 9 + complete('vite.config.js', 'Vite config file'); 10 + }, 11 + 'c' 12 + ); 13 + 14 + t.option( 15 + 'mode', 16 + 'Set env mode', 17 + function (complete) { 18 + complete('development', 'Development mode'); 19 + complete('production', 'Production mode'); 20 + }, 21 + 'm' 22 + ); 23 + 24 + t.option( 25 + 'logLevel', 26 + 'info | warn | error | silent', 27 + function (complete) { 28 + complete('info', 'Info level'); 29 + complete('warn', 'Warn level'); 30 + complete('error', 'Error level'); 31 + complete('silent', 'Silent level'); 32 + }, 33 + 'l' 34 + ); 35 + 36 + // Root command argument 37 + t.argument('project', function (complete) { 38 + complete('my-app', 'My application'); 39 + complete('my-lib', 'My library'); 40 + complete('my-tool', 'My tool'); 41 + }); 42 + 43 + // Dev command 44 + const devCmd = t.command('dev', 'Start dev server'); 45 + devCmd.option( 46 + 'host', 47 + 'Specify hostname', 48 + function (complete) { 49 + complete('localhost', 'Localhost'); 50 + complete('0.0.0.0', 'All interfaces'); 51 + }, 52 + 'H' 53 + ); 54 + 55 + devCmd.option( 56 + 'port', 57 + 'Specify port', 58 + function (complete) { 59 + complete('3000', 'Development server port'); 60 + complete('8080', 'Alternative port'); 61 + }, 62 + 'p' 63 + ); 64 + 65 + // Serve command 66 + const serveCmd = t.command('serve', 'Start the server'); 67 + serveCmd.option( 68 + 'host', 69 + 'Specify hostname', 70 + function (complete) { 71 + complete('localhost', 'Localhost'); 72 + complete('0.0.0.0', 'All interfaces'); 73 + }, 74 + 'H' 75 + ); 76 + 77 + serveCmd.option( 78 + 'port', 79 + 'Specify port', 80 + function (complete) { 81 + complete('3000', 'Development server port'); 82 + complete('8080', 'Alternative port'); 83 + }, 84 + 'p' 85 + ); 86 + 87 + // Build command 88 + t.command('dev build', 'Build project'); 89 + 90 + // Copy command with multiple arguments 91 + const copyCmd = t 92 + .command('copy', 'Copy files') 93 + .argument('source', function (complete) { 94 + complete('src/', 'Source directory'); 95 + complete('dist/', 'Distribution directory'); 96 + complete('public/', 'Public assets'); 97 + }) 98 + .argument('destination', function (complete) { 99 + complete('build/', 'Build output'); 100 + complete('release/', 'Release directory'); 101 + complete('backup/', 'Backup location'); 102 + }); 103 + 104 + // Lint command with variadic arguments 105 + const lintCmd = t.command('lint', 'Lint project').argument( 106 + 'files', 107 + function (complete) { 108 + complete('main.ts', 'Main file'); 109 + complete('index.ts', 'Index file'); 110 + complete('src/', 'Source directory'); 111 + complete('tests/', 'Tests directory'); 112 + }, 113 + true 114 + ); // Variadic argument for multiple files 115 + 116 + // Handle completion command 117 + if (process.argv[2] === 'complete') { 118 + const shell = process.argv[3]; 119 + if (shell && ['zsh', 'bash', 'fish', 'powershell'].includes(shell)) { 120 + t.setup('vite', 'pnpm tsx examples/demo.t.ts', shell); 121 + } else { 122 + // Parse completion arguments (everything after --) 123 + const separatorIndex = process.argv.indexOf('--'); 124 + const completionArgs = 125 + separatorIndex !== -1 ? process.argv.slice(separatorIndex + 1) : []; 126 + t.parse(completionArgs); 127 + } 128 + } else { 129 + // Regular CLI usage (just show help for demo) 130 + console.log('Vite CLI Demo'); 131 + console.log('Use "complete" command for shell completion'); 132 + }
+9
examples/tiny-cli/package.json
··· 1 + { 2 + "name": "tiny-cli", 3 + "version": "1.0.0", 4 + "description": "Minimal CLI for testing tab completions with pnpm", 5 + "main": "tiny-cli.js", 6 + "bin": { 7 + "tiny-cli": "./tiny-cli.js" 8 + } 9 + }
+16
examples/tiny-cli/tiny-cli.js
··· 1 + #!/usr/bin/env node 2 + 3 + if (process.argv[2] === '__complete') { 4 + console.log('hello\tSay hello'); 5 + console.log('world\tSay world'); 6 + process.exit(0); 7 + } else { 8 + const command = process.argv[2]; 9 + if (command === 'hello') { 10 + console.log('Hello!'); 11 + } else if (command === 'world') { 12 + console.log('World!'); 13 + } else { 14 + console.log('Usage: tiny-cli [hello|world]'); 15 + } 16 + }
+7 -4
package.json
··· 1 1 { 2 - "name": "tab", 2 + "name": "@bombsh/tab", 3 3 "version": "0.0.0", 4 - "description": "", 5 4 "main": "./dist/index.js", 6 5 "types": "./dist/index.d.ts", 7 6 "type": "module", 7 + "bin": { 8 + "tab": "./dist/bin/cli.js" 9 + }, 8 10 "scripts": { 9 - "test": "vitest", 11 + "test": "vitest run", 10 12 "type-check": "tsc --noEmit", 11 13 "format": "prettier --write .", 12 14 "format:check": "prettier --check .", 13 15 "build": "tsdown", 14 16 "prepare": "pnpm build", 15 - "lint": "eslint src \"./*.ts\"" 17 + "lint": "eslint src \"./*.ts\"", 18 + "test-cli": "tsx bin/cli.ts" 16 19 }, 17 20 "files": [ 18 21 "dist"
+242
pnpm-script-extended/pnpm-shell-completion-extended.plugin.zsh
··· 1 + #compdef pnpm 2 + 3 + # ----------------------------------------------------------------------------- 4 + # pnpm Shell Completion Extension 5 + # Adds support for tab completion of CLIs executed through pnpm 6 + # ----------------------------------------------------------------------------- 7 + 8 + # Set to 1 to enable debug logging, 0 to disable 9 + PNPM_TAB_DEBUG=0 10 + DEBUG_FILE="/tmp/pnpm-completion-debug.log" 11 + 12 + # Debug logging function 13 + _pnpm_tab_debug() { 14 + if [[ $PNPM_TAB_DEBUG -eq 1 ]]; then 15 + echo "$(date): $*" >> $DEBUG_FILE 16 + fi 17 + } 18 + 19 + _pnpm_tab_debug "Loading pnpm completion script with CLI tab completion support" 20 + 21 + # Run a CLI tool through pnpm directly 22 + _pnpm_run_cli() { 23 + local cli_name=$1 24 + shift 25 + local cli_args=("$@") 26 + 27 + _pnpm_tab_debug "Running CLI via pnpm: $cli_name ${cli_args[*]}" 28 + # Execute the command through pnpm directly 29 + pnpm $cli_name "${cli_args[@]}" 2>/dev/null 30 + } 31 + 32 + # Complete commands using pnpm execution 33 + _pnpm_complete_cli() { 34 + local cli_name=$1 35 + shift 36 + local cli_args=("$@") 37 + local output 38 + 39 + _pnpm_tab_debug "Completing $cli_name with args: ${cli_args[*]}" 40 + 41 + # Add __complete as the first argument 42 + cli_args=("__complete" "${cli_args[@]}") 43 + output=$(_pnpm_run_cli $cli_name "${cli_args[@]}" 2>&1) 44 + _pnpm_tab_debug "Completion output from pnpm: $output" 45 + 46 + # Process the output for ZSH completion 47 + if [[ -n "$output" ]]; then 48 + # Convert the output into a format that ZSH can use for completion 49 + local -a completions 50 + 51 + # Process each line of the output 52 + while IFS=$'\n' read -r line; do 53 + _pnpm_tab_debug "Processing line: $line" 54 + # Check if the line has a tab character (description) 55 + if [[ "$line" == *$'\t'* ]]; then 56 + # Split the line at the tab character 57 + local value=${line%%$'\t'*} 58 + local description=${line#*$'\t'} 59 + _pnpm_tab_debug "Adding completion with description: $value -> $description" 60 + completions+=("${value}:${description}") 61 + else 62 + # No description 63 + _pnpm_tab_debug "Adding completion without description: $line" 64 + completions+=("$line") 65 + fi 66 + done <<< "$output" 67 + 68 + # Use _describe to present the completions 69 + if [[ ${#completions[@]} -gt 0 ]]; then 70 + _pnpm_tab_debug "Found ${#completions[@]} completions, calling _describe" 71 + _describe "completions" completions 72 + return 0 73 + fi 74 + fi 75 + 76 + _pnpm_tab_debug "No completions found, falling back to file completion" 77 + # If we couldn't get or process completions, fall back to file completion 78 + _files 79 + return 1 80 + } 81 + 82 + # Check if a CLI supports __complete by running it through pnpm 83 + _pnpm_cli_has_completions() { 84 + local cli_name=$1 85 + _pnpm_tab_debug "Checking if $cli_name has completions via pnpm" 86 + 87 + # Try to execute the __complete command through pnpm 88 + if output=$(_pnpm_run_cli $cli_name "__complete" 2>/dev/null) && [[ -n "$output" ]]; then 89 + _pnpm_tab_debug "$cli_name supports completions via pnpm: $output" 90 + return 0 91 + fi 92 + 93 + _pnpm_tab_debug "$cli_name does not support completions via pnpm" 94 + return 1 95 + } 96 + 97 + # Original pnpm-shell-completion logic 98 + if command -v pnpm-shell-completion &> /dev/null; then 99 + pnpm_comp_bin="$(which pnpm-shell-completion)" 100 + _pnpm_tab_debug "Found pnpm-shell-completion at $pnpm_comp_bin" 101 + else 102 + pnpm_comp_bin="$(dirname $0)/pnpm-shell-completion" 103 + _pnpm_tab_debug "Using relative pnpm-shell-completion at $pnpm_comp_bin" 104 + fi 105 + 106 + _pnpm() { 107 + typeset -A opt_args 108 + _pnpm_tab_debug "Starting pnpm completion, words: ${words[*]}" 109 + 110 + _arguments \ 111 + '(--filter -F)'{--filter,-F}'=:flag:->filter' \ 112 + ':command:->scripts' \ 113 + '*:: :->command_args' 114 + 115 + local target_pkg=${opt_args[--filter]:-$opt_args[-F]} 116 + _pnpm_tab_debug "State: $state, target_pkg: $target_pkg" 117 + 118 + case $state in 119 + filter) 120 + if [[ -f ./pnpm-workspace.yaml ]] && [[ -x "$pnpm_comp_bin" ]]; then 121 + _pnpm_tab_debug "Using pnpm-shell-completion for filter packages" 122 + _values 'filter packages' $(FEATURE=filter $pnpm_comp_bin) 123 + else 124 + _pnpm_tab_debug "No workspace or pnpm-shell-completion for filter" 125 + _message "package filter" 126 + fi 127 + ;; 128 + scripts) 129 + if [[ -x "$pnpm_comp_bin" ]]; then 130 + _pnpm_tab_debug "Using pnpm-shell-completion for scripts" 131 + _values 'scripts' $(FEATURE=scripts TARGET_PKG=$target_pkg ZSH=true $pnpm_comp_bin) \ 132 + add remove install update publish 133 + else 134 + _pnpm_tab_debug "Using basic pnpm commands (no pnpm-shell-completion)" 135 + _values 'scripts' \ 136 + 'add:Add a dependency' \ 137 + 'install:Install dependencies' \ 138 + 'remove:Remove a dependency' \ 139 + 'update:Update dependencies' \ 140 + 'publish:Publish package' \ 141 + 'run:Run script' 142 + fi 143 + ;; 144 + command_args) 145 + local cmd=$words[1] 146 + _pnpm_tab_debug "Completing command args for $cmd" 147 + 148 + # Get the pnpm command if available 149 + local pnpm_cmd="" 150 + if [[ -x "$pnpm_comp_bin" ]]; then 151 + pnpm_cmd=$(FEATURE=pnpm_cmd $pnpm_comp_bin $words 2>/dev/null) 152 + _pnpm_tab_debug "pnpm-shell-completion identified command: $pnpm_cmd" 153 + else 154 + pnpm_cmd=$cmd 155 + _pnpm_tab_debug "Using first word as command: $pnpm_cmd" 156 + fi 157 + 158 + # Check if this is a potential CLI command that might support tab completion 159 + if [[ $cmd != "add" && $cmd != "remove" && $cmd != "install" && 160 + $cmd != "update" && $cmd != "publish" && $cmd != "i" && 161 + $cmd != "rm" && $cmd != "up" && $cmd != "run" ]]; then 162 + 163 + _pnpm_tab_debug "Checking if $cmd has tab completions via pnpm" 164 + # Check if the command has tab completions through pnpm 165 + if _pnpm_cli_has_completions $cmd; then 166 + _pnpm_tab_debug "$cmd has tab completions via pnpm, passing args" 167 + # Pass remaining arguments to the CLI's completion 168 + local cli_args=("${words[@]:2}") 169 + _pnpm_complete_cli $cmd "${cli_args[@]}" 170 + return 171 + fi 172 + fi 173 + 174 + # Fall back to default pnpm completion behavior 175 + _pnpm_tab_debug "Using standard completion for pnpm $pnpm_cmd" 176 + case $pnpm_cmd in 177 + add) 178 + _arguments \ 179 + '(--global -g)'{--global,-g}'[Install as a global package]' \ 180 + '(--save-dev -D)'{--save-dev,-D}'[Save package to your `devDependencies`]' \ 181 + '--save-peer[Save package to your `peerDependencies` and `devDependencies`]' 182 + ;; 183 + install|i) 184 + _arguments \ 185 + '(--dev -D)'{--dev,-D}'[Only `devDependencies` are installed regardless of the `NODE_ENV`]' \ 186 + '--fix-lockfile[Fix broken lockfile entries automatically]' \ 187 + '--force[Force reinstall dependencies]' \ 188 + "--ignore-scripts[Don't run lifecycle scripts]" \ 189 + '--lockfile-only[Dependencies are not downloaded. Only `pnpm-lock.yaml` is updated]' \ 190 + '--no-optional[`optionalDependencies` are not installed]' \ 191 + '--offline[Trigger an error if any required dependencies are not available in local store]' \ 192 + '--prefer-offline[Skip staleness checks for cached data, but request missing data from the server]' \ 193 + '(--prod -P)'{--prod,-P}"[Packages in \`devDependencies\` won't be installed]" 194 + ;; 195 + remove|rm|why) 196 + if [[ -f ./package.json && -x "$pnpm_comp_bin" ]]; then 197 + _values 'deps' $(FEATURE=deps TARGET_PKG=$target_pkg $pnpm_comp_bin) 198 + else 199 + _message "package name" 200 + fi 201 + ;; 202 + update|upgrade|up) 203 + _arguments \ 204 + '(--dev -D)'{--dev,-D}'[Update packages only in "devDependencies"]' \ 205 + '(--global -g)'{--global,-g}'[Update globally installed packages]' \ 206 + '(--interactive -i)'{--interactive,-i}'[Show outdated dependencies and select which ones to update]' \ 207 + '(--latest -L)'{--latest,-L}'[Ignore version ranges in package.json]' \ 208 + "--no-optional[Don't update packages in \`optionalDependencies\`]" \ 209 + '(--prod -P)'{--prod,-P}'[Update packages only in "dependencies" and "optionalDependencies"]' \ 210 + '(--recursive -r)'{--recursive,-r}'[Update in every package found in subdirectories or every workspace package]' 211 + if [[ -f ./package.json && -x "$pnpm_comp_bin" ]]; then 212 + _values 'deps' $(FEATURE=deps TARGET_PKG=$target_pkg $pnpm_comp_bin) 213 + fi 214 + ;; 215 + publish) 216 + _arguments \ 217 + '--access=[Tells the registry whether this package should be published as public or restricted]: :(public restricted)' \ 218 + '--dry-run[Does everything a publish would do except actually publishing to the registry]' \ 219 + '--force[Packages are proceeded to be published even if their current version is already in the registry]' \ 220 + '--ignore-scripts[Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)]' \ 221 + "--no-git-checks[Don't check if current branch is your publish branch, clean, and up to date]" \ 222 + '--otp[Specify a one-time password]' \ 223 + '--publish-branch[Sets branch name to publish]' \ 224 + '(--recursive -r)'{--recursive,-r}'[Publish all packages from the workspace]' \ 225 + '--tag=[Registers the published package with the given tag]' 226 + ;; 227 + run) 228 + if [[ -f ./package.json && -x "$pnpm_comp_bin" ]]; then 229 + _values 'scripts' $(FEATURE=scripts TARGET_PKG=$target_pkg ZSH=true $pnpm_comp_bin) 230 + else 231 + _message "script name" 232 + fi 233 + ;; 234 + *) 235 + _files 236 + esac 237 + esac 238 + } 239 + 240 + compdef _pnpm pnpm 241 + 242 + _pnpm_tab_debug "pnpm extended completion script loaded"
+50 -24
src/cac.ts
··· 3 3 import * as fish from './fish'; 4 4 import * as powershell from './powershell'; 5 5 import type { CAC } from 'cac'; 6 - import { Completion } from './index'; 7 - import { CompletionConfig, noopHandler, assertDoubleDashes } from './shared'; 6 + import { assertDoubleDashes } from './shared'; 7 + import { OptionHandler } from './t'; 8 + import { CompletionConfig } from './shared'; 9 + import t from './t'; 10 + 11 + const noopOptionHandler: OptionHandler = function () {}; 8 12 9 13 const execPath = process.execPath; 10 14 const processArgs = process.argv.slice(1); ··· 21 25 export default async function tab( 22 26 instance: CAC, 23 27 completionConfig?: CompletionConfig 24 - ) { 25 - const completion = new Completion(); 26 - 28 + ): Promise<any> { 27 29 // Add all commands and their options 28 30 for (const cmd of [instance.globalCommand, ...instance.commands]) { 29 31 if (cmd.name === 'complete') continue; // Skip completion command ··· 38 40 ? completionConfig 39 41 : completionConfig?.subCommands?.[cmd.name]; 40 42 41 - // Add command to completion 42 - const commandName = completion.addCommand( 43 - isRootCommand ? '' : cmd.name, 44 - cmd.description || '', 45 - args, 46 - commandCompletionConfig?.handler ?? noopHandler 47 - ); 43 + // Add command to completion using t.ts API 44 + const commandName = isRootCommand ? '' : cmd.name; 45 + const command = isRootCommand 46 + ? t 47 + : t.command(commandName, cmd.description || ''); 48 + 49 + // Set args for the command 50 + if (command) { 51 + // Extract argument names from command usage 52 + const argMatches = 53 + cmd.rawName.match(/<([^>]+)>|\[\.\.\.([^\]]+)\]/g) || []; 54 + const argNames = argMatches.map((match) => { 55 + if (match.startsWith('<') && match.endsWith('>')) { 56 + return match.slice(1, -1); // Remove < > 57 + } else if (match.startsWith('[...') && match.endsWith(']')) { 58 + return match.slice(4, -1); // Remove [... ] 59 + } 60 + return match; 61 + }); 62 + 63 + args.forEach((variadic, index) => { 64 + const argName = argNames[index] || `arg${index}`; 65 + const argHandler = commandCompletionConfig?.args?.[argName]; 66 + if (argHandler) { 67 + command.argument(argName, argHandler, variadic); 68 + } else { 69 + command.argument(argName, undefined, variadic); 70 + } 71 + }); 72 + } 48 73 49 74 // Add command options 50 75 for (const option of [...instance.globalCommand.options, ...cmd.options]) { ··· 52 77 const shortFlag = option.name.match(/^-([a-zA-Z]), --/)?.[1]; 53 78 const argName = option.name.replace(/^-[a-zA-Z], --/, ''); 54 79 55 - completion.addOption( 56 - commandName, 57 - `--${argName}`, // Remove the short flag part if it exists 58 - option.description || '', 59 - commandCompletionConfig?.options?.[argName]?.handler ?? noopHandler, 60 - shortFlag 61 - ); 80 + // Add option using t.ts API 81 + const targetCommand = isRootCommand ? t : command; 82 + if (targetCommand) { 83 + targetCommand.option( 84 + argName, // Store just the option name without -- prefix 85 + option.description || '', 86 + commandCompletionConfig?.options?.[argName] ?? noopOptionHandler, 87 + shortFlag 88 + ); 89 + } 62 90 } 63 91 } 64 92 ··· 96 124 run: false, 97 125 }); 98 126 99 - // const matchedCommand = instance.matchedCommand?.name || ''; 100 - // const potentialCommand = args.join(' ') 101 - // console.log(potentialCommand) 102 - return completion.parse(args); 127 + // Use t.ts parse method instead of completion.parse 128 + return t.parse(args); 103 129 } 104 130 } 105 131 }); 106 132 107 - return completion; 133 + return t; 108 134 }
+109 -40
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 './index'; 7 6 import type { 8 7 ArgsDef, 9 8 CommandDef, ··· 11 10 SubCommandsDef, 12 11 } from 'citty'; 13 12 import { generateFigSpec } from './fig'; 14 - import { CompletionConfig, noopHandler, assertDoubleDashes } from './shared'; 13 + import { CompletionConfig, assertDoubleDashes } from './shared'; 14 + import { OptionHandler, Command, Option, OptionsMap } from './t'; 15 + import t from './t'; 15 16 16 17 function quoteIfNeeded(path: string) { 17 18 return path.includes(' ') ? `'${path}'` : path; ··· 31 32 ); 32 33 } 33 34 35 + // Convert Handler from index.ts to OptionHandler from t.ts 36 + function convertOptionHandler(handler: any): OptionHandler { 37 + return function ( 38 + this: Option, 39 + complete: (value: string, description: string) => void, 40 + options: OptionsMap, 41 + previousArgs?: string[], 42 + toComplete?: string, 43 + endsWithSpace?: boolean 44 + ) { 45 + // For short flags with equals sign and a value, don't complete (citty behavior) 46 + // Check if this is a short flag option and if the toComplete looks like a value 47 + if ( 48 + this.alias && 49 + toComplete && 50 + toComplete !== '' && 51 + !toComplete.startsWith('-') 52 + ) { 53 + // This might be a short flag with equals sign and a value 54 + // Check if the previous args contain a short flag with equals sign 55 + if (previousArgs && previousArgs.length > 0) { 56 + const lastArg = previousArgs[previousArgs.length - 1]; 57 + if (lastArg.includes('=')) { 58 + const [flag, value] = lastArg.split('='); 59 + if (flag.startsWith('-') && !flag.startsWith('--') && value !== '') { 60 + return; // Don't complete short flags with equals sign and value 61 + } 62 + } 63 + } 64 + } 65 + 66 + // Call the old handler with the proper context 67 + const result = handler( 68 + previousArgs || [], 69 + toComplete || '', 70 + endsWithSpace || false 71 + ); 72 + 73 + if (Array.isArray(result)) { 74 + result.forEach((item: any) => 75 + complete(item.value, item.description || '') 76 + ); 77 + } else if (result && typeof result.then === 'function') { 78 + // Handle async handlers 79 + result.then((items: any[]) => { 80 + items.forEach((item: any) => 81 + complete(item.value, item.description || '') 82 + ); 83 + }); 84 + } 85 + }; 86 + } 87 + 88 + const noopOptionHandler: OptionHandler = function () {}; 89 + 34 90 async function handleSubCommands( 35 - completion: Completion, 36 91 subCommands: SubCommandsDef, 37 92 parentCmd?: string, 38 93 completionConfig?: Record<string, CompletionConfig> ··· 47 102 throw new Error('Invalid meta or missing description.'); 48 103 } 49 104 const isPositional = isConfigPositional(config); 50 - const name = completion.addCommand( 51 - cmd, 52 - meta.description, 53 - isPositional ? [false] : [], 54 - subCompletionConfig?.handler ?? noopHandler, 55 - parentCmd 56 - ); 105 + 106 + // Add command using t.ts API 107 + const commandName = parentCmd ? `${parentCmd} ${cmd}` : cmd; 108 + const command = t.command(cmd, meta.description); 109 + 110 + // Set args for the command if it has positional arguments 111 + if (isPositional && config.args) { 112 + // Add arguments with completion handlers from subCompletionConfig args 113 + for (const [argName, argConfig] of Object.entries(config.args)) { 114 + const conf = argConfig as ArgDef; 115 + if (conf.type === 'positional') { 116 + // Check if this is a variadic argument (required: false for variadic in citty) 117 + const isVariadic = conf.required === false; 118 + const argHandler = subCompletionConfig?.args?.[argName]; 119 + if (argHandler) { 120 + command.argument(argName, argHandler, isVariadic); 121 + } else { 122 + command.argument(argName, undefined, isVariadic); 123 + } 124 + } 125 + } 126 + } 57 127 58 128 // Handle nested subcommands recursively 59 129 if (subCommands) { 60 130 await handleSubCommands( 61 - completion, 62 131 subCommands, 63 - name, 132 + commandName, 64 133 subCompletionConfig?.subCommands 65 134 ); 66 135 } ··· 80 149 : conf.alias 81 150 : undefined; 82 151 83 - completion.addOption( 84 - name, 85 - `--${argName}`, 152 + // Add option using t.ts API - store without -- prefix 153 + command.option( 154 + argName, 86 155 conf.description ?? '', 87 - subCompletionConfig?.options?.[argName]?.handler ?? noopHandler, 156 + subCompletionConfig?.options?.[argName] ?? noopOptionHandler, 88 157 shortFlag 89 158 ); 90 159 } ··· 95 164 export default async function tab<TArgs extends ArgsDef>( 96 165 instance: CommandDef<TArgs>, 97 166 completionConfig?: CompletionConfig 98 - ) { 99 - const completion = new Completion(); 100 - 167 + ): Promise<any> { 101 168 const meta = await resolve(instance.meta); 102 169 103 170 if (!meta) { ··· 113 180 throw new Error('Invalid or missing subCommands.'); 114 181 } 115 182 116 - const root = ''; 117 183 const isPositional = isConfigPositional(instance); 118 - completion.addCommand( 119 - root, 120 - meta?.description ?? '', 121 - isPositional ? [false] : [], 122 - completionConfig?.handler ?? noopHandler 123 - ); 184 + 185 + // Set args for the root command if it has positional arguments 186 + if (isPositional && instance.args) { 187 + for (const [argName, argConfig] of Object.entries(instance.args)) { 188 + const conf = argConfig as PositionalArgDef; 189 + if (conf.type === 'positional') { 190 + const isVariadic = conf.required === false; 191 + const argHandler = completionConfig?.args?.[argName]; 192 + if (argHandler) { 193 + t.argument(argName, argHandler, isVariadic); 194 + } else { 195 + t.argument(argName, undefined, isVariadic); 196 + } 197 + } 198 + } 199 + } 124 200 125 201 await handleSubCommands( 126 - completion, 127 202 subCommands, 128 203 undefined, 129 204 completionConfig?.subCommands ··· 132 207 if (instance.args) { 133 208 for (const [argName, argConfig] of Object.entries(instance.args)) { 134 209 const conf = argConfig as PositionalArgDef; 135 - completion.addOption( 136 - root, 137 - `--${argName}`, 210 + // Add option using t.ts API - store without -- prefix 211 + t.option( 212 + argName, 138 213 conf.description ?? '', 139 - completionConfig?.options?.[argName]?.handler ?? noopHandler 214 + completionConfig?.options?.[argName] ?? noopOptionHandler 140 215 ); 141 216 } 142 217 } ··· 190 265 assertDoubleDashes(name); 191 266 192 267 const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf('--') + 1); 193 - // const args = (await resolve(instance.args))!; 194 - // const parsed = parseArgs(extra, args); 195 - // TODO: this is not ideal at all 196 - // const matchedCommand = parsed._.join(' ').trim(); //TODO: this was passed to parse line 170 197 - // TODO: `command lint i` does not work because `lint` and `i` are potential commands 198 - // instead the potential command should only be `lint` 199 - // and `i` is the to be completed part 200 - return completion.parse(extra); 268 + // Use t.ts parse method instead of completion.parse 269 + return t.parse(extra); 201 270 } 202 271 } 203 272 }, ··· 205 274 206 275 subCommands.complete = completeCommand; 207 276 208 - return completion; 277 + return t; 209 278 } 210 279 211 280 type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
+1
src/fig.ts
··· 114 114 return spec; 115 115 } 116 116 117 + // TODO: this should be an extension of t.setup function and not something like this. 117 118 export async function generateFigSpec<T extends ArgsDef>( 118 119 command: CommandDef<T> 119 120 ): Promise<string> {
+113 -5
src/index.ts
··· 2 2 import * as bash from './bash'; 3 3 import * as fish from './fish'; 4 4 import * as powershell from './powershell'; 5 + import { execSync } from 'child_process'; 6 + import { Completion as CompletionItem } from './t'; 7 + 8 + const DEBUG = false; 9 + 10 + function debugLog(...args: any[]) { 11 + if (DEBUG) { 12 + console.error('[DEBUG]', ...args); 13 + } 14 + } 15 + 16 + async function checkCliHasCompletions( 17 + cliName: string, 18 + packageManager: string 19 + ): Promise<boolean> { 20 + try { 21 + debugLog(`Checking if ${cliName} has completions via ${packageManager}`); 22 + const command = `${packageManager} ${cliName} complete --`; 23 + const result = execSync(command, { 24 + encoding: 'utf8', 25 + stdio: ['pipe', 'pipe', 'ignore'], 26 + timeout: 1000, 27 + }); 28 + const hasCompletions = !!result.trim(); 29 + debugLog(`${cliName} supports completions: ${hasCompletions}`); 30 + return hasCompletions; 31 + } catch (error) { 32 + debugLog(`Error checking completions for ${cliName}:`, error); 33 + return false; 34 + } 35 + } 36 + 37 + async function getCliCompletions( 38 + cliName: string, 39 + packageManager: string, 40 + args: string[] 41 + ): Promise<string[]> { 42 + try { 43 + const completeArgs = args.map((arg) => 44 + arg.includes(' ') ? `"${arg}"` : arg 45 + ); 46 + const completeCommand = `${packageManager} ${cliName} complete -- ${completeArgs.join(' ')}`; 47 + debugLog(`Getting completions with command: ${completeCommand}`); 48 + 49 + const result = execSync(completeCommand, { 50 + encoding: 'utf8', 51 + stdio: ['pipe', 'pipe', 'ignore'], 52 + timeout: 1000, 53 + }); 54 + 55 + const completions = result.trim().split('\n').filter(Boolean); 56 + debugLog(`Got ${completions.length} completions from ${cliName}`); 57 + return completions; 58 + } catch (error) { 59 + debugLog(`Error getting completions from ${cliName}:`, error); 60 + return []; 61 + } 62 + } 5 63 6 64 // ShellCompRequestCmd is the name of the hidden command that is used to request 7 65 // completion results from the program. It is used by the shell completion scripts. ··· 61 119 completion: Handler; 62 120 }; 63 121 64 - type Item = { 65 - description: string; 66 - value: string; 122 + type CompletionResult = { 123 + items: CompletionItem[]; 124 + suppressDefault: boolean; 67 125 }; 68 126 69 127 export type Handler = ( 70 128 previousArgs: string[], 71 129 toComplete: string, 72 130 endsWithSpace: boolean 73 - ) => Item[] | Promise<Item[]>; 131 + ) => CompletionItem[] | Promise<CompletionItem[]>; 74 132 75 133 type Option = { 76 134 description: string; ··· 89 147 90 148 export class Completion { 91 149 commands = new Map<string, Command>(); 92 - completions: Item[] = []; 150 + completions: CompletionItem[] = []; 93 151 directive = ShellCompDirective.ShellCompDirectiveDefault; 152 + result: CompletionResult = { items: [], suppressDefault: false }; 153 + private packageManager: string | null = null; 154 + 155 + setPackageManager(packageManager: string) { 156 + this.packageManager = packageManager; 157 + } 94 158 95 159 // vite <entry> <another> [...files] 96 160 // args: [false, false, true], only the last argument can be variadic ··· 171 235 } 172 236 173 237 async parse(args: string[]) { 238 + this.result = { items: [], suppressDefault: false }; 239 + 240 + // TODO: i did not notice this, this should not be handled here at all. package manager completions are something on top of this. just like any other completion system that is going to be built on top of tab. 241 + // Handle package manager completions first 242 + if (this.packageManager && args.length >= 1) { 243 + const potentialCliName = args[0]; 244 + const knownCommands = [...this.commands.keys()]; 245 + 246 + if (!knownCommands.includes(potentialCliName)) { 247 + const hasCompletions = await checkCliHasCompletions( 248 + potentialCliName, 249 + this.packageManager 250 + ); 251 + 252 + if (hasCompletions) { 253 + const cliArgs = args.slice(1); 254 + const suggestions = await getCliCompletions( 255 + potentialCliName, 256 + this.packageManager, 257 + cliArgs 258 + ); 259 + 260 + if (suggestions.length > 0) { 261 + this.result.suppressDefault = true; 262 + 263 + for (const suggestion of suggestions) { 264 + if (suggestion.startsWith(':')) continue; 265 + 266 + if (suggestion.includes('\t')) { 267 + const [value, description] = suggestion.split('\t'); 268 + this.result.items.push({ value, description }); 269 + } else { 270 + this.result.items.push({ value: suggestion }); 271 + } 272 + } 273 + 274 + this.completions = this.result.items; 275 + this.complete(''); 276 + return; 277 + } 278 + } 279 + } 280 + } 281 + 174 282 const endsWithSpace = args[args.length - 1] === ''; 175 283 176 284 if (endsWithSpace) {
+5 -10
src/shared.ts
··· 1 - import { Handler } from './index'; 1 + import { OptionHandler, ArgumentHandler } from './t'; 2 2 3 - export const noopHandler: Handler = () => { 4 - return []; 3 + export const noopHandler: OptionHandler = function () { 4 + // No-op handler for options 5 5 }; 6 6 7 7 // TODO (43081j): use type inference some day, so we can type-check 8 8 // that the sub commands exist, the options exist, etc. 9 9 export interface CompletionConfig { 10 - handler?: Handler; 11 10 subCommands?: Record<string, CompletionConfig>; 12 - options?: Record< 13 - string, 14 - { 15 - handler: Handler; 16 - } 17 - >; 11 + options?: Record<string, OptionHandler>; 12 + args?: Record<string, ArgumentHandler>; 18 13 } 19 14 20 15 export function assertDoubleDashes(programName: string = 'cli'): void {
+447
src/t.ts
··· 1 + // Shell directive constants 2 + const ShellCompDirective = { 3 + ShellCompDirectiveError: 1 << 0, 4 + ShellCompDirectiveNoSpace: 1 << 1, 5 + ShellCompDirectiveNoFileComp: 1 << 2, 6 + ShellCompDirectiveFilterFileExt: 1 << 3, 7 + ShellCompDirectiveFilterDirs: 1 << 4, 8 + ShellCompDirectiveKeepOrder: 1 << 5, 9 + shellCompDirectiveMaxValue: 1 << 6, 10 + ShellCompDirectiveDefault: 0, 11 + }; 12 + 13 + export type OptionsMap = Map<string, Option>; 14 + 15 + type Complete = (value: string, description: string) => void; 16 + 17 + export type OptionHandler = ( 18 + this: Option, 19 + complete: Complete, 20 + options: OptionsMap 21 + ) => void; 22 + 23 + // Completion result types 24 + export type Completion = { 25 + description?: string; 26 + value: string; 27 + }; 28 + 29 + export type ArgumentHandler = ( 30 + this: Argument, 31 + complete: Complete, 32 + options: OptionsMap 33 + ) => void; 34 + 35 + export class Argument { 36 + name: string; 37 + variadic: boolean; 38 + command: Command; 39 + handler?: ArgumentHandler; 40 + 41 + constructor( 42 + command: Command, 43 + name: string, 44 + handler?: ArgumentHandler, 45 + variadic: boolean = false 46 + ) { 47 + this.command = command; 48 + this.name = name; 49 + this.handler = handler; 50 + this.variadic = variadic; 51 + } 52 + } 53 + 54 + export class Option { 55 + value: string; 56 + description: string; 57 + command: Command; 58 + handler?: OptionHandler; 59 + alias?: string; 60 + // TODO: handle boolean options 61 + 62 + constructor( 63 + command: Command, 64 + value: string, 65 + description: string, 66 + handler?: OptionHandler, 67 + alias?: string 68 + ) { 69 + this.command = command; 70 + this.value = value; 71 + this.description = description; 72 + this.handler = handler; 73 + this.alias = alias; 74 + } 75 + } 76 + 77 + export class Command { 78 + value: string; 79 + description: string; 80 + options = new Map<string, Option>(); 81 + arguments = new Map<string, Argument>(); 82 + parent?: Command; 83 + 84 + constructor(value: string, description: string) { 85 + this.value = value; 86 + this.description = description; 87 + } 88 + 89 + option( 90 + value: string, 91 + description: string, 92 + handler?: OptionHandler, 93 + alias?: string 94 + ) { 95 + const option = new Option(this, value, description, handler, alias); 96 + this.options.set(value, option); 97 + return this; 98 + } 99 + 100 + argument(name: string, handler?: ArgumentHandler, variadic: boolean = false) { 101 + const arg = new Argument(this, name, handler, variadic); 102 + this.arguments.set(name, arg); 103 + return this; 104 + } 105 + } 106 + 107 + import * as zsh from './zsh'; 108 + import * as bash from './bash'; 109 + import * as fish from './fish'; 110 + import * as powershell from './powershell'; 111 + import assert from 'node:assert'; 112 + 113 + export class RootCommand extends Command { 114 + commands = new Map<string, Command>(); 115 + completions: Completion[] = []; 116 + directive = ShellCompDirective.ShellCompDirectiveDefault; 117 + 118 + constructor() { 119 + super('', ''); 120 + } 121 + 122 + command(value: string, description: string) { 123 + const c = new Command(value, description); 124 + this.commands.set(value, c); 125 + return c; 126 + } 127 + 128 + // Utility method to strip options from args for command matching 129 + private stripOptions(args: string[]): string[] { 130 + const parts: string[] = []; 131 + let i = 0; 132 + 133 + while (i < args.length) { 134 + const arg = args[i]; 135 + 136 + if (arg.startsWith('-')) { 137 + i++; // Skip the option 138 + if (i < args.length && !args[i].startsWith('-')) { 139 + i++; // Skip the option value 140 + } 141 + } else { 142 + parts.push(arg); 143 + i++; 144 + } 145 + } 146 + 147 + return parts; 148 + } 149 + 150 + // Find the appropriate command based on args 151 + private matchCommand(args: string[]): [Command, string[]] { 152 + args = this.stripOptions(args); 153 + const parts: string[] = []; 154 + let remaining: string[] = []; 155 + let matched: Command = this; 156 + 157 + for (let i = 0; i < args.length; i++) { 158 + const k = args[i]; 159 + parts.push(k); 160 + const potential = this.commands.get(parts.join(' ')); 161 + 162 + if (potential) { 163 + matched = potential; 164 + } else { 165 + remaining = args.slice(i, args.length); 166 + break; 167 + } 168 + } 169 + 170 + return [matched, remaining]; 171 + } 172 + 173 + // Determine if we should complete flags 174 + private shouldCompleteFlags( 175 + lastPrevArg: string | undefined, 176 + toComplete: string, 177 + endsWithSpace: boolean 178 + ): boolean { 179 + return lastPrevArg?.startsWith('-') || toComplete.startsWith('-'); 180 + } 181 + 182 + // Determine if we should complete commands 183 + private shouldCompleteCommands( 184 + toComplete: string, 185 + endsWithSpace: boolean 186 + ): boolean { 187 + return !toComplete.startsWith('-'); 188 + } 189 + 190 + // Handle flag completion (names and values) 191 + private handleFlagCompletion( 192 + command: Command, 193 + previousArgs: string[], 194 + toComplete: string, 195 + endsWithSpace: boolean, 196 + lastPrevArg: string | undefined 197 + ) { 198 + // Handle flag value completion 199 + let optionName: string | undefined; 200 + let valueToComplete = toComplete; 201 + 202 + if (toComplete.includes('=')) { 203 + const [flag, value] = toComplete.split('='); 204 + optionName = flag; 205 + valueToComplete = value || ''; 206 + } else if (lastPrevArg?.startsWith('-')) { 207 + optionName = lastPrevArg; 208 + } 209 + 210 + if (optionName) { 211 + const option = this.findOption(command, optionName); 212 + if (option?.handler) { 213 + const suggestions: Completion[] = []; 214 + option.handler.call( 215 + option, 216 + (value: string, description: string) => 217 + suggestions.push({ value, description }), 218 + command.options 219 + ); 220 + 221 + this.completions = toComplete.includes('=') 222 + ? suggestions.map((s) => ({ 223 + value: `${optionName}=${s.value}`, 224 + description: s.description, 225 + })) 226 + : suggestions; 227 + } 228 + return; 229 + } 230 + 231 + // Handle flag name completion 232 + if (toComplete.startsWith('-')) { 233 + const isShortFlag = 234 + toComplete.startsWith('-') && !toComplete.startsWith('--'); 235 + const cleanToComplete = toComplete.replace(/^-+/, ''); 236 + 237 + for (const [name, option] of command.options) { 238 + if ( 239 + isShortFlag && 240 + option.alias && 241 + `-${option.alias}`.startsWith(toComplete) 242 + ) { 243 + this.completions.push({ 244 + value: `-${option.alias}`, 245 + description: option.description, 246 + }); 247 + } else if (!isShortFlag && name.startsWith(cleanToComplete)) { 248 + this.completions.push({ 249 + value: `--${name}`, 250 + description: option.description, 251 + }); 252 + } 253 + } 254 + } 255 + } 256 + 257 + // Helper method to find an option by name or alias 258 + private findOption(command: Command, optionName: string): Option | undefined { 259 + // Try direct match (with dashes) 260 + let option = command.options.get(optionName); 261 + if (option) return option; 262 + 263 + // Try without dashes (the actual storage format) 264 + option = command.options.get(optionName.replace(/^-+/, '')); 265 + if (option) return option; 266 + 267 + // Try by short alias 268 + for (const [name, opt] of command.options) { 269 + if (opt.alias && `-${opt.alias}` === optionName) { 270 + return opt; 271 + } 272 + } 273 + 274 + return undefined; 275 + } 276 + 277 + // Handle command completion 278 + private handleCommandCompletion(previousArgs: string[], toComplete: string) { 279 + const commandParts = previousArgs.filter(Boolean); 280 + 281 + for (const [k, command] of this.commands) { 282 + if (k === '') continue; 283 + 284 + const parts = k.split(' '); 285 + const match = parts 286 + .slice(0, commandParts.length) 287 + .every((part, i) => part === commandParts[i]); 288 + 289 + if (match && parts[commandParts.length]?.startsWith(toComplete)) { 290 + this.completions.push({ 291 + value: parts[commandParts.length], 292 + description: command.description, 293 + }); 294 + } 295 + } 296 + } 297 + 298 + // Handle positional argument completion 299 + private handlePositionalCompletion( 300 + command: Command, 301 + previousArgs: string[], 302 + toComplete: string, 303 + endsWithSpace: boolean 304 + ) { 305 + // Get the current argument position (subtract command name) 306 + const commandParts = command.value.split(' ').length; 307 + const currentArgIndex = Math.max(0, previousArgs.length - commandParts); 308 + const argumentEntries = Array.from(command.arguments.entries()); 309 + 310 + // If we have arguments defined 311 + if (argumentEntries.length > 0) { 312 + // Find the appropriate argument for the current position 313 + let targetArgument: Argument | undefined; 314 + 315 + if (currentArgIndex < argumentEntries.length) { 316 + // We're within the defined arguments 317 + const [argName, argument] = argumentEntries[currentArgIndex]; 318 + targetArgument = argument; 319 + } else { 320 + // We're beyond the defined arguments, check if the last argument is variadic 321 + const lastArgument = argumentEntries[argumentEntries.length - 1][1]; 322 + if (lastArgument.variadic) { 323 + targetArgument = lastArgument; 324 + } 325 + } 326 + 327 + // If we found a target argument with a handler, use it 328 + if ( 329 + targetArgument && 330 + targetArgument.handler && 331 + typeof targetArgument.handler === 'function' 332 + ) { 333 + const suggestions: Completion[] = []; 334 + targetArgument.handler.call( 335 + targetArgument, 336 + (value: string, description: string) => 337 + suggestions.push({ value, description }), 338 + command.options 339 + ); 340 + this.completions.push(...suggestions); 341 + } 342 + } 343 + } 344 + 345 + // Format and output completion results 346 + private complete(toComplete: string) { 347 + this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; 348 + 349 + const seen = new Set<string>(); 350 + this.completions 351 + .filter((comp) => { 352 + if (seen.has(comp.value)) return false; 353 + seen.add(comp.value); 354 + return true; 355 + }) 356 + .filter((comp) => comp.value.startsWith(toComplete)) 357 + .forEach((comp) => 358 + console.log(`${comp.value}\t${comp.description ?? ''}`) 359 + ); 360 + console.log(`:${this.directive}`); 361 + } 362 + 363 + parse(args: string[]) { 364 + this.completions = []; 365 + 366 + const endsWithSpace = args[args.length - 1] === ''; 367 + 368 + if (endsWithSpace) { 369 + args.pop(); 370 + } 371 + 372 + let toComplete = args[args.length - 1] || ''; 373 + const previousArgs = args.slice(0, -1); 374 + 375 + if (endsWithSpace) { 376 + previousArgs.push(toComplete); 377 + toComplete = ''; 378 + } 379 + 380 + const [matchedCommand] = this.matchCommand(previousArgs); 381 + const lastPrevArg = previousArgs[previousArgs.length - 1]; 382 + 383 + // 1. Handle flag/option completion 384 + if (this.shouldCompleteFlags(lastPrevArg, toComplete, endsWithSpace)) { 385 + this.handleFlagCompletion( 386 + matchedCommand, 387 + previousArgs, 388 + toComplete, 389 + endsWithSpace, 390 + lastPrevArg 391 + ); 392 + } else { 393 + // 2. Handle command/subcommand completion 394 + if (this.shouldCompleteCommands(toComplete, endsWithSpace)) { 395 + this.handleCommandCompletion(previousArgs, toComplete); 396 + } 397 + // 3. Handle positional arguments 398 + if (matchedCommand && matchedCommand.arguments.size > 0) { 399 + this.handlePositionalCompletion( 400 + matchedCommand, 401 + previousArgs, 402 + toComplete, 403 + endsWithSpace 404 + ); 405 + } 406 + } 407 + 408 + this.complete(toComplete); 409 + } 410 + 411 + setup(name: string, executable: string, shell: string) { 412 + assert( 413 + shell === 'zsh' || 414 + shell === 'bash' || 415 + shell === 'fish' || 416 + shell === 'powershell', 417 + 'Unsupported shell' 418 + ); 419 + 420 + switch (shell) { 421 + case 'zsh': { 422 + const script = zsh.generate(name, executable); 423 + console.log(script); 424 + break; 425 + } 426 + case 'bash': { 427 + const script = bash.generate(name, executable); 428 + console.log(script); 429 + break; 430 + } 431 + case 'fish': { 432 + const script = fish.generate(name, executable); 433 + console.log(script); 434 + break; 435 + } 436 + case 'powershell': { 437 + const script = powershell.generate(name, executable); 438 + console.log(script); 439 + break; 440 + } 441 + } 442 + } 443 + } 444 + 445 + const t = new RootCommand(); 446 + 447 + export default t;
+697 -3
tests/__snapshots__/cli.test.ts.snap
··· 1 1 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 + exports[`cli completion tests for cac > --config option tests > should complete --config option values 1`] = ` 4 + "vite.config.ts Vite config file 5 + vite.config.js Vite config file 6 + :4 7 + " 8 + `; 9 + 10 + exports[`cli completion tests for cac > --config option tests > should complete --config option with equals sign 1`] = ` 11 + "--config=vite.config.ts Vite config file 12 + --config=vite.config.js Vite config file 13 + :4 14 + " 15 + `; 16 + 17 + exports[`cli completion tests for cac > --config option tests > should complete --config option with partial input 1`] = ` 18 + "vite.config.ts Vite config file 19 + vite.config.js Vite config file 20 + :4 21 + " 22 + `; 23 + 24 + exports[`cli completion tests for cac > --config option tests > should complete short flag -c option values 1`] = ` 25 + ":4 26 + " 27 + `; 28 + 29 + exports[`cli completion tests for cac > --config option tests > should complete short flag -c option with partial input 1`] = ` 30 + ":4 31 + " 32 + `; 33 + 34 + exports[`cli completion tests for cac > --config option tests > should not suggest --config after it has been used 1`] = ` 35 + "--config Use specified config file 36 + --mode Set env mode 37 + --logLevel info | warn | error | silent 38 + :4 39 + " 40 + `; 41 + 3 42 exports[`cli completion tests for cac > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` 4 43 "--port Specify port 5 44 :4 ··· 44 83 " 45 84 `; 46 85 86 + exports[`cli completion tests for cac > copy command argument handlers > should complete destination argument with build suggestions 1`] = ` 87 + "build/ Build output 88 + release/ Release directory 89 + backup/ Backup location 90 + :4 91 + " 92 + `; 93 + 94 + exports[`cli completion tests for cac > copy command argument handlers > should complete source argument with directory suggestions 1`] = ` 95 + "src/ Source directory 96 + dist/ Distribution directory 97 + public/ Public assets 98 + :4 99 + " 100 + `; 101 + 102 + exports[`cli completion tests for cac > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = ` 103 + "build/ Build output 104 + backup/ Backup location 105 + :4 106 + " 107 + `; 108 + 109 + exports[`cli completion tests for cac > copy command argument handlers > should filter source suggestions when typing partial input 1`] = ` 110 + "src/ Source directory 111 + :4 112 + " 113 + `; 114 + 47 115 exports[`cli completion tests for cac > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` 48 116 "--port Specify port 49 117 :4 ··· 64 132 " 65 133 `; 66 134 135 + exports[`cli completion tests for cac > lint command argument handlers > should complete files argument with file suggestions 1`] = ` 136 + "main.ts Main file 137 + index.ts Index file 138 + :4 139 + " 140 + `; 141 + 142 + exports[`cli completion tests for cac > lint command argument handlers > should continue completing variadic files argument after first file 1`] = ` 143 + "main.ts Main file 144 + index.ts Index file 145 + :4 146 + " 147 + `; 148 + 149 + exports[`cli completion tests for cac > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = ` 150 + "index.ts Index file 151 + :4 152 + " 153 + `; 154 + 155 + exports[`cli completion tests for cac > lint command argument handlers > should filter file suggestions when typing partial input 1`] = ` 156 + "main.ts Main file 157 + :4 158 + " 159 + `; 160 + 67 161 exports[`cli completion tests for cac > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = ` 68 162 "index.ts Index file 69 163 :4 ··· 84 178 " 85 179 `; 86 180 181 + exports[`cli completion tests for cac > root command argument tests > should complete root command project argument 1`] = ` 182 + "dev Start dev server 183 + serve Start the server 184 + copy Copy files 185 + lint Lint project 186 + :4 187 + " 188 + `; 189 + 190 + exports[`cli completion tests for cac > root command argument tests > should complete root command project argument after options 1`] = ` 191 + ":4 192 + " 193 + `; 194 + 195 + exports[`cli completion tests for cac > root command argument tests > should complete root command project argument with options and partial input 1`] = ` 196 + ":4 197 + " 198 + `; 199 + 200 + exports[`cli completion tests for cac > root command argument tests > should complete root command project argument with partial input 1`] = ` 201 + ":4 202 + " 203 + `; 204 + 205 + exports[`cli completion tests for cac > root command option tests > should complete root command --logLevel option values 1`] = ` 206 + "info Info level 207 + warn Warn level 208 + error Error level 209 + silent Silent level 210 + :4 211 + " 212 + `; 213 + 214 + exports[`cli completion tests for cac > root command option tests > should complete root command --logLevel option with partial input 1`] = ` 215 + "info Info level 216 + :4 217 + " 218 + `; 219 + 220 + exports[`cli completion tests for cac > root command option tests > should complete root command --mode option values 1`] = ` 221 + "development Development mode 222 + production Production mode 223 + :4 224 + " 225 + `; 226 + 227 + exports[`cli completion tests for cac > root command option tests > should complete root command --mode option with partial input 1`] = ` 228 + "development Development mode 229 + :4 230 + " 231 + `; 232 + 233 + exports[`cli completion tests for cac > root command option tests > should complete root command options after project argument 1`] = ` 234 + "--config Use specified config file 235 + --mode Set env mode 236 + --logLevel info | warn | error | silent 237 + :4 238 + " 239 + `; 240 + 241 + exports[`cli completion tests for cac > root command option tests > should complete root command options with partial input after project argument 1`] = ` 242 + "--mode Set env mode 243 + :4 244 + " 245 + `; 246 + 247 + exports[`cli completion tests for cac > root command option tests > should complete root command short flag -l option values 1`] = ` 248 + ":4 249 + " 250 + `; 251 + 252 + exports[`cli completion tests for cac > root command option tests > should complete root command short flag -m option values 1`] = ` 253 + ":4 254 + " 255 + `; 256 + 87 257 exports[`cli completion tests for cac > short flag handling > should handle global short flags 1`] = ` 88 258 ":4 89 259 " ··· 110 280 exports[`cli completion tests for cac > should complete cli options 1`] = ` 111 281 "dev Start dev server 112 282 serve Start the server 283 + copy Copy files 113 284 lint Lint project 114 285 :4 115 286 " 116 287 `; 117 288 289 + exports[`cli completion tests for citty > --config option tests > should complete --config option values 1`] = ` 290 + "vite.config.ts Vite config file 291 + vite.config.js Vite config file 292 + :4 293 + " 294 + `; 295 + 296 + exports[`cli completion tests for citty > --config option tests > should complete --config option with equals sign 1`] = ` 297 + "--config=vite.config.ts Vite config file 298 + --config=vite.config.js Vite config file 299 + :4 300 + " 301 + `; 302 + 303 + exports[`cli completion tests for citty > --config option tests > should complete --config option with partial input 1`] = ` 304 + "vite.config.ts Vite config file 305 + vite.config.js Vite config file 306 + :4 307 + " 308 + `; 309 + 310 + exports[`cli completion tests for citty > --config option tests > should complete short flag -c option values 1`] = ` 311 + ":4 312 + " 313 + `; 314 + 315 + exports[`cli completion tests for citty > --config option tests > should complete short flag -c option with partial input 1`] = ` 316 + ":4 317 + " 318 + `; 319 + 320 + exports[`cli completion tests for citty > --config option tests > should not suggest --config after it has been used 1`] = ` 321 + "--project Project name 322 + --config Use specified config file 323 + --mode Set env mode 324 + --logLevel info | warn | error | silent 325 + :4 326 + " 327 + `; 328 + 118 329 exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` 119 330 "--port Specify port 120 331 :4 ··· 141 352 exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; 142 353 143 354 exports[`cli completion tests for citty > cli option value handling > should not show duplicate options 1`] = ` 144 - "--config Use specified config file 355 + "--project Project name 356 + --config Use specified config file 145 357 --mode Set env mode 146 358 --logLevel info | warn | error | silent 147 359 :4 ··· 161 373 " 162 374 `; 163 375 376 + exports[`cli completion tests for citty > copy command argument handlers > should complete destination argument with build suggestions 1`] = ` 377 + "build/ Build output 378 + release/ Release directory 379 + backup/ Backup location 380 + :4 381 + " 382 + `; 383 + 384 + exports[`cli completion tests for citty > copy command argument handlers > should complete source argument with directory suggestions 1`] = ` 385 + "src/ Source directory 386 + dist/ Distribution directory 387 + public/ Public assets 388 + :4 389 + " 390 + `; 391 + 392 + exports[`cli completion tests for citty > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = ` 393 + "build/ Build output 394 + backup/ Backup location 395 + :4 396 + " 397 + `; 398 + 399 + exports[`cli completion tests for citty > copy command argument handlers > should filter source suggestions when typing partial input 1`] = ` 400 + "src/ Source directory 401 + :4 402 + " 403 + `; 404 + 164 405 exports[`cli completion tests for citty > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` 165 406 "--port Specify port 166 407 :4 ··· 181 422 " 182 423 `; 183 424 425 + exports[`cli completion tests for citty > lint command argument handlers > should complete files argument with file suggestions 1`] = ` 426 + "main.ts Main file 427 + index.ts Index file 428 + :4 429 + " 430 + `; 431 + 432 + exports[`cli completion tests for citty > lint command argument handlers > should continue completing variadic files argument after first file 1`] = ` 433 + "main.ts Main file 434 + index.ts Index file 435 + :4 436 + " 437 + `; 438 + 439 + exports[`cli completion tests for citty > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = ` 440 + "index.ts Index file 441 + :4 442 + " 443 + `; 444 + 445 + exports[`cli completion tests for citty > lint command argument handlers > should filter file suggestions when typing partial input 1`] = ` 446 + "main.ts Main file 447 + :4 448 + " 449 + `; 450 + 451 + exports[`cli completion tests for citty > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = ` 452 + "index.ts Index file 453 + :4 454 + " 455 + `; 456 + 457 + exports[`cli completion tests for citty > positional argument completions > should complete multiple positional arguments when ending with space 1`] = ` 458 + "main.ts Main file 459 + index.ts Index file 460 + :4 461 + " 462 + `; 463 + 464 + exports[`cli completion tests for citty > positional argument completions > should complete single positional argument when ending with space 1`] = ` 465 + "main.ts Main file 466 + index.ts Index file 467 + :4 468 + " 469 + `; 470 + 471 + exports[`cli completion tests for citty > root command argument tests > should complete root command project argument 1`] = ` 472 + "dev Start dev server 473 + build Build project 474 + copy Copy files 475 + lint Lint project 476 + my-app My application 477 + my-lib My library 478 + my-tool My tool 479 + :4 480 + " 481 + `; 482 + 483 + exports[`cli completion tests for citty > root command argument tests > should complete root command project argument after options 1`] = ` 484 + ":4 485 + " 486 + `; 487 + 488 + exports[`cli completion tests for citty > root command argument tests > should complete root command project argument with options and partial input 1`] = ` 489 + ":4 490 + " 491 + `; 492 + 493 + exports[`cli completion tests for citty > root command argument tests > should complete root command project argument with partial input 1`] = ` 494 + "my-app My application 495 + my-lib My library 496 + my-tool My tool 497 + :4 498 + " 499 + `; 500 + 501 + exports[`cli completion tests for citty > root command option tests > should complete root command --logLevel option values 1`] = ` 502 + "info Info level 503 + warn Warn level 504 + error Error level 505 + silent Silent level 506 + :4 507 + " 508 + `; 509 + 510 + exports[`cli completion tests for citty > root command option tests > should complete root command --logLevel option with partial input 1`] = ` 511 + "info Info level 512 + :4 513 + " 514 + `; 515 + 516 + exports[`cli completion tests for citty > root command option tests > should complete root command --mode option values 1`] = ` 517 + "development Development mode 518 + production Production mode 519 + :4 520 + " 521 + `; 522 + 523 + exports[`cli completion tests for citty > root command option tests > should complete root command --mode option with partial input 1`] = ` 524 + "development Development mode 525 + :4 526 + " 527 + `; 528 + 529 + exports[`cli completion tests for citty > root command option tests > should complete root command options after project argument 1`] = ` 530 + "--project Project name 531 + --config Use specified config file 532 + --mode Set env mode 533 + --logLevel info | warn | error | silent 534 + :4 535 + " 536 + `; 537 + 538 + exports[`cli completion tests for citty > root command option tests > should complete root command options with partial input after project argument 1`] = ` 539 + "--mode Set env mode 540 + :4 541 + " 542 + `; 543 + 544 + exports[`cli completion tests for citty > root command option tests > should complete root command short flag -l option values 1`] = ` 545 + ":4 546 + " 547 + `; 548 + 549 + exports[`cli completion tests for citty > root command option tests > should complete root command short flag -m option values 1`] = ` 550 + ":4 551 + " 552 + `; 553 + 184 554 exports[`cli completion tests for citty > short flag handling > should handle global short flags 1`] = ` 185 555 ":4 186 556 " ··· 193 563 `; 194 564 195 565 exports[`cli completion tests for citty > short flag handling > should handle short flag with equals sign 1`] = ` 196 - ":4 566 + "-p=3000 Development server port 567 + :4 197 568 " 198 569 `; 199 570 200 571 exports[`cli completion tests for citty > short flag handling > should not show duplicate options when short flag is used 1`] = ` 201 - "--config Use specified config file 572 + "--project Project name 573 + --config Use specified config file 202 574 --mode Set env mode 203 575 --logLevel info | warn | error | silent 204 576 :4 ··· 208 580 exports[`cli completion tests for citty > should complete cli options 1`] = ` 209 581 "dev Start dev server 210 582 build Build project 583 + copy Copy files 211 584 lint Lint project 585 + my-app My application 586 + my-lib My library 587 + my-tool My tool 212 588 :4 213 589 " 214 590 `; ··· 229 605 :4 230 606 " 231 607 `; 608 + 609 + exports[`cli completion tests for t > --config option tests > should complete --config option values 1`] = ` 610 + "vite.config.ts Vite config file 611 + vite.config.js Vite config file 612 + :4 613 + " 614 + `; 615 + 616 + exports[`cli completion tests for t > --config option tests > should complete --config option with equals sign 1`] = ` 617 + "--config=vite.config.ts Vite config file 618 + --config=vite.config.js Vite config file 619 + :4 620 + " 621 + `; 622 + 623 + exports[`cli completion tests for t > --config option tests > should complete --config option with partial input 1`] = ` 624 + "vite.config.ts Vite config file 625 + vite.config.js Vite config file 626 + :4 627 + " 628 + `; 629 + 630 + exports[`cli completion tests for t > --config option tests > should complete short flag -c option values 1`] = ` 631 + "vite.config.ts Vite config file 632 + vite.config.js Vite config file 633 + :4 634 + " 635 + `; 636 + 637 + exports[`cli completion tests for t > --config option tests > should complete short flag -c option with partial input 1`] = ` 638 + "vite.config.ts Vite config file 639 + vite.config.js Vite config file 640 + :4 641 + " 642 + `; 643 + 644 + exports[`cli completion tests for t > --config option tests > should not suggest --config after it has been used 1`] = ` 645 + "--config Use specified config file 646 + --mode Set env mode 647 + --logLevel info | warn | error | silent 648 + :4 649 + " 650 + `; 651 + 652 + exports[`cli completion tests for t > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` 653 + "--port Specify port 654 + :4 655 + " 656 + `; 657 + 658 + exports[`cli completion tests for t > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = ` 659 + "-H Specify hostname 660 + :4 661 + " 662 + `; 663 + 664 + exports[`cli completion tests for t > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = ` 665 + "-p Specify port 666 + :4 667 + " 668 + `; 669 + 670 + exports[`cli completion tests for t > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` 671 + ":4 672 + " 673 + `; 674 + 675 + exports[`cli completion tests for t > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; 676 + 677 + exports[`cli completion tests for t > cli option value handling > should not show duplicate options 1`] = ` 678 + "--config Use specified config file 679 + --mode Set env mode 680 + --logLevel info | warn | error | silent 681 + :4 682 + " 683 + `; 684 + 685 + exports[`cli completion tests for t > cli option value handling > should resolve config option values correctly 1`] = ` 686 + "vite.config.ts Vite config file 687 + vite.config.js Vite config file 688 + :4 689 + " 690 + `; 691 + 692 + exports[`cli completion tests for t > cli option value handling > should resolve port value correctly 1`] = ` 693 + "--port=3000 Development server port 694 + :4 695 + " 696 + `; 697 + 698 + exports[`cli completion tests for t > copy command argument handlers > should complete destination argument with build suggestions 1`] = ` 699 + "build/ Build output 700 + release/ Release directory 701 + backup/ Backup location 702 + :4 703 + " 704 + `; 705 + 706 + exports[`cli completion tests for t > copy command argument handlers > should complete source argument with directory suggestions 1`] = ` 707 + "src/ Source directory 708 + dist/ Distribution directory 709 + public/ Public assets 710 + :4 711 + " 712 + `; 713 + 714 + exports[`cli completion tests for t > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = ` 715 + "build/ Build output 716 + backup/ Backup location 717 + :4 718 + " 719 + `; 720 + 721 + exports[`cli completion tests for t > copy command argument handlers > should filter source suggestions when typing partial input 1`] = ` 722 + "src/ Source directory 723 + :4 724 + " 725 + `; 726 + 727 + exports[`cli completion tests for t > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` 728 + "--port Specify port 729 + :4 730 + " 731 + `; 732 + 733 + exports[`cli completion tests for t > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = ` 734 + "3000 Development server port 735 + 8080 Alternative port 736 + :4 737 + " 738 + `; 739 + 740 + exports[`cli completion tests for t > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` 741 + "--port=3000 Development server port 742 + --port=8080 Alternative port 743 + :4 744 + " 745 + `; 746 + 747 + exports[`cli completion tests for t > lint command argument handlers > should complete files argument with file suggestions 1`] = ` 748 + "main.ts Main file 749 + index.ts Index file 750 + src/ Source directory 751 + tests/ Tests directory 752 + :4 753 + " 754 + `; 755 + 756 + exports[`cli completion tests for t > lint command argument handlers > should continue completing variadic files argument after first file 1`] = ` 757 + "main.ts Main file 758 + index.ts Index file 759 + src/ Source directory 760 + tests/ Tests directory 761 + :4 762 + " 763 + `; 764 + 765 + exports[`cli completion tests for t > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = ` 766 + "index.ts Index file 767 + :4 768 + " 769 + `; 770 + 771 + exports[`cli completion tests for t > lint command argument handlers > should filter file suggestions when typing partial input 1`] = ` 772 + "main.ts Main file 773 + :4 774 + " 775 + `; 776 + 777 + exports[`cli completion tests for t > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = ` 778 + "index.ts Index file 779 + :4 780 + " 781 + `; 782 + 783 + exports[`cli completion tests for t > positional argument completions > should complete multiple positional arguments when ending with space 1`] = ` 784 + "main.ts Main file 785 + index.ts Index file 786 + src/ Source directory 787 + tests/ Tests directory 788 + :4 789 + " 790 + `; 791 + 792 + exports[`cli completion tests for t > positional argument completions > should complete single positional argument when ending with space 1`] = ` 793 + "main.ts Main file 794 + index.ts Index file 795 + src/ Source directory 796 + tests/ Tests directory 797 + :4 798 + " 799 + `; 800 + 801 + exports[`cli completion tests for t > root command argument tests > should complete root command project argument 1`] = ` 802 + "dev Start dev server 803 + serve Start the server 804 + copy Copy files 805 + lint Lint project 806 + my-app My application 807 + my-lib My library 808 + my-tool My tool 809 + :4 810 + " 811 + `; 812 + 813 + exports[`cli completion tests for t > root command argument tests > should complete root command project argument after options 1`] = ` 814 + ":4 815 + " 816 + `; 817 + 818 + exports[`cli completion tests for t > root command argument tests > should complete root command project argument with options and partial input 1`] = ` 819 + ":4 820 + " 821 + `; 822 + 823 + exports[`cli completion tests for t > root command argument tests > should complete root command project argument with partial input 1`] = ` 824 + "my-app My application 825 + my-lib My library 826 + my-tool My tool 827 + :4 828 + " 829 + `; 830 + 831 + exports[`cli completion tests for t > root command option tests > should complete root command --logLevel option values 1`] = ` 832 + "info Info level 833 + warn Warn level 834 + error Error level 835 + silent Silent level 836 + :4 837 + " 838 + `; 839 + 840 + exports[`cli completion tests for t > root command option tests > should complete root command --logLevel option with partial input 1`] = ` 841 + "info Info level 842 + :4 843 + " 844 + `; 845 + 846 + exports[`cli completion tests for t > root command option tests > should complete root command --mode option values 1`] = ` 847 + "development Development mode 848 + production Production mode 849 + :4 850 + " 851 + `; 852 + 853 + exports[`cli completion tests for t > root command option tests > should complete root command --mode option with partial input 1`] = ` 854 + "development Development mode 855 + :4 856 + " 857 + `; 858 + 859 + exports[`cli completion tests for t > root command option tests > should complete root command options after project argument 1`] = ` 860 + "--config Use specified config file 861 + --mode Set env mode 862 + --logLevel info | warn | error | silent 863 + :4 864 + " 865 + `; 866 + 867 + exports[`cli completion tests for t > root command option tests > should complete root command options with partial input after project argument 1`] = ` 868 + "--mode Set env mode 869 + :4 870 + " 871 + `; 872 + 873 + exports[`cli completion tests for t > root command option tests > should complete root command short flag -l option values 1`] = ` 874 + "info Info level 875 + warn Warn level 876 + error Error level 877 + silent Silent level 878 + :4 879 + " 880 + `; 881 + 882 + exports[`cli completion tests for t > root command option tests > should complete root command short flag -m option values 1`] = ` 883 + "development Development mode 884 + production Production mode 885 + :4 886 + " 887 + `; 888 + 889 + exports[`cli completion tests for t > short flag handling > should handle global short flags 1`] = ` 890 + "-c Use specified config file 891 + :4 892 + " 893 + `; 894 + 895 + exports[`cli completion tests for t > short flag handling > should handle short flag value completion 1`] = ` 896 + "-p Specify port 897 + :4 898 + " 899 + `; 900 + 901 + exports[`cli completion tests for t > short flag handling > should handle short flag with equals sign 1`] = ` 902 + "-p=3000 Development server port 903 + :4 904 + " 905 + `; 906 + 907 + exports[`cli completion tests for t > short flag handling > should not show duplicate options when short flag is used 1`] = ` 908 + "--config Use specified config file 909 + --mode Set env mode 910 + --logLevel info | warn | error | silent 911 + :4 912 + " 913 + `; 914 + 915 + exports[`cli completion tests for t > should complete cli options 1`] = ` 916 + "dev Start dev server 917 + serve Start the server 918 + copy Copy files 919 + lint Lint project 920 + my-app My application 921 + my-lib My library 922 + my-tool My tool 923 + :4 924 + " 925 + `;
+192 -31
tests/cli.test.ts
··· 13 13 }); 14 14 } 15 15 16 - const cliTools = ['citty', 'cac', 'commander']; 16 + const cliTools = ['t', 'citty', 'cac', 'commander']; 17 17 18 18 describe.each(cliTools)('cli completion tests for %s', (cliTool) => { 19 19 // For Commander, we need to skip most of the tests since it handles completion differently 20 20 const shouldSkipTest = cliTool === 'commander'; 21 21 22 22 // Commander uses a different command structure for completion 23 + // TODO: why commander does that? our convention is the -- part which should be always there. 23 24 const commandPrefix = 24 25 cliTool === 'commander' 25 26 ? `pnpm tsx examples/demo.${cliTool}.ts complete` 26 27 : `pnpm tsx examples/demo.${cliTool}.ts complete --`; 27 - 28 - // Use 'dev' for citty and 'serve' for other tools 29 - const commandName = cliTool === 'citty' ? 'dev' : 'serve'; 30 28 31 29 it.runIf(!shouldSkipTest)('should complete cli options', async () => { 32 30 const output = await runCommand(`${commandPrefix}`); ··· 43 41 test.each(optionTests)( 44 42 "should complete option for partial input '%s'", 45 43 async ({ partial }) => { 46 - const command = `${commandPrefix} ${commandName} ${partial}`; 44 + const command = `${commandPrefix} dev ${partial}`; 47 45 const output = await runCommand(command); 48 46 expect(output).toMatchSnapshot(); 49 47 } ··· 67 65 68 66 describe.runIf(!shouldSkipTest)('cli option value handling', () => { 69 67 it('should resolve port value correctly', async () => { 70 - const command = `${commandPrefix} ${commandName} --port=3`; 68 + const command = `${commandPrefix} dev --port=3`; 71 69 const output = await runCommand(command); 72 70 expect(output).toMatchSnapshot(); 73 71 }); ··· 91 89 }); 92 90 }); 93 91 92 + describe.runIf(!shouldSkipTest)('--config option tests', () => { 93 + it('should complete --config option values', async () => { 94 + const command = `${commandPrefix} --config ""`; 95 + const output = await runCommand(command); 96 + expect(output).toMatchSnapshot(); 97 + }); 98 + 99 + it('should complete --config option with partial input', async () => { 100 + const command = `${commandPrefix} --config vite.config`; 101 + const output = await runCommand(command); 102 + expect(output).toMatchSnapshot(); 103 + }); 104 + 105 + it('should complete --config option with equals sign', async () => { 106 + const command = `${commandPrefix} --config=vite.config`; 107 + const output = await runCommand(command); 108 + expect(output).toMatchSnapshot(); 109 + }); 110 + 111 + it('should complete short flag -c option values', async () => { 112 + const command = `${commandPrefix} -c ""`; 113 + const output = await runCommand(command); 114 + expect(output).toMatchSnapshot(); 115 + }); 116 + 117 + it('should complete short flag -c option with partial input', async () => { 118 + const command = `${commandPrefix} -c vite.config`; 119 + const output = await runCommand(command); 120 + expect(output).toMatchSnapshot(); 121 + }); 122 + 123 + it('should not suggest --config after it has been used', async () => { 124 + const command = `${commandPrefix} --config vite.config.ts --`; 125 + const output = await runCommand(command); 126 + expect(output).toMatchSnapshot(); 127 + }); 128 + }); 129 + 130 + describe.runIf(!shouldSkipTest)('root command argument tests', () => { 131 + it('should complete root command project argument', async () => { 132 + const command = `${commandPrefix} ""`; 133 + const output = await runCommand(command); 134 + expect(output).toMatchSnapshot(); 135 + }); 136 + 137 + it('should complete root command project argument with partial input', async () => { 138 + const command = `${commandPrefix} my-`; 139 + const output = await runCommand(command); 140 + expect(output).toMatchSnapshot(); 141 + }); 142 + 143 + it('should complete root command project argument after options', async () => { 144 + const command = `${commandPrefix} --config vite.config.ts ""`; 145 + const output = await runCommand(command); 146 + expect(output).toMatchSnapshot(); 147 + }); 148 + 149 + it('should complete root command project argument with options and partial input', async () => { 150 + const command = `${commandPrefix} --mode development my-`; 151 + const output = await runCommand(command); 152 + expect(output).toMatchSnapshot(); 153 + }); 154 + }); 155 + 156 + describe.runIf(!shouldSkipTest)('root command option tests', () => { 157 + it('should complete root command --mode option values', async () => { 158 + const command = `${commandPrefix} --mode ""`; 159 + const output = await runCommand(command); 160 + expect(output).toMatchSnapshot(); 161 + }); 162 + 163 + it('should complete root command --mode option with partial input', async () => { 164 + const command = `${commandPrefix} --mode dev`; 165 + const output = await runCommand(command); 166 + expect(output).toMatchSnapshot(); 167 + }); 168 + 169 + it('should complete root command --logLevel option values', async () => { 170 + const command = `${commandPrefix} --logLevel ""`; 171 + const output = await runCommand(command); 172 + expect(output).toMatchSnapshot(); 173 + }); 174 + 175 + it('should complete root command --logLevel option with partial input', async () => { 176 + const command = `${commandPrefix} --logLevel i`; 177 + const output = await runCommand(command); 178 + expect(output).toMatchSnapshot(); 179 + }); 180 + 181 + it('should complete root command short flag -m option values', async () => { 182 + const command = `${commandPrefix} -m ""`; 183 + const output = await runCommand(command); 184 + expect(output).toMatchSnapshot(); 185 + }); 186 + 187 + it('should complete root command short flag -l option values', async () => { 188 + const command = `${commandPrefix} -l ""`; 189 + const output = await runCommand(command); 190 + expect(output).toMatchSnapshot(); 191 + }); 192 + 193 + it('should complete root command options after project argument', async () => { 194 + const command = `${commandPrefix} my-app --`; 195 + const output = await runCommand(command); 196 + expect(output).toMatchSnapshot(); 197 + }); 198 + 199 + it('should complete root command options with partial input after project argument', async () => { 200 + const command = `${commandPrefix} my-app --m`; 201 + const output = await runCommand(command); 202 + expect(output).toMatchSnapshot(); 203 + }); 204 + }); 205 + 94 206 describe.runIf(!shouldSkipTest)( 95 207 'edge case completions for end with space', 96 208 () => { 97 209 it('should suggest port values if user ends with space after `--port`', async () => { 98 - const command = `${commandPrefix} ${commandName} --port ""`; 210 + const command = `${commandPrefix} dev --port ""`; 99 211 const output = await runCommand(command); 100 212 expect(output).toMatchSnapshot(); 101 213 }); 102 214 103 215 it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { 104 - const command = `${commandPrefix} ${commandName} --po`; 216 + const command = `${commandPrefix} dev --po`; 105 217 const output = await runCommand(command); 106 218 expect(output).toMatchSnapshot(); 107 219 }); 108 220 109 221 it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { 110 - const command = `${commandPrefix} ${commandName} --port=`; 222 + const command = `${commandPrefix} dev --port=`; 111 223 const output = await runCommand(command); 112 224 expect(output).toMatchSnapshot(); 113 225 }); ··· 116 228 117 229 describe.runIf(!shouldSkipTest)('short flag handling', () => { 118 230 it('should handle short flag value completion', async () => { 119 - const command = `${commandPrefix} ${commandName} -p `; 231 + const command = `${commandPrefix} dev -p `; 120 232 const output = await runCommand(command); 121 233 expect(output).toMatchSnapshot(); 122 234 }); 123 235 124 236 it('should handle short flag with equals sign', async () => { 125 - const command = `${commandPrefix} ${commandName} -p=3`; 237 + const command = `${commandPrefix} dev -p=3`; 126 238 const output = await runCommand(command); 127 239 expect(output).toMatchSnapshot(); 128 240 }); ··· 140 252 }); 141 253 }); 142 254 143 - describe.runIf(!shouldSkipTest && cliTool !== 'citty')( 144 - 'positional argument completions', 145 - () => { 146 - it('should complete multiple positional arguments when ending with space', async () => { 147 - const command = `${commandPrefix} lint ""`; 148 - const output = await runCommand(command); 149 - expect(output).toMatchSnapshot(); 150 - }); 255 + describe.runIf(!shouldSkipTest)('positional argument completions', () => { 256 + it('should complete multiple positional arguments when ending with space', async () => { 257 + const command = `${commandPrefix} lint ""`; 258 + const output = await runCommand(command); 259 + expect(output).toMatchSnapshot(); 260 + }); 261 + 262 + it('should complete multiple positional arguments when ending with part of the value', async () => { 263 + const command = `${commandPrefix} lint ind`; 264 + const output = await runCommand(command); 265 + expect(output).toMatchSnapshot(); 266 + }); 267 + 268 + it('should complete single positional argument when ending with space', async () => { 269 + const command = `${commandPrefix} lint main.ts ""`; 270 + const output = await runCommand(command); 271 + expect(output).toMatchSnapshot(); 272 + }); 273 + }); 274 + 275 + describe.runIf(!shouldSkipTest)('copy command argument handlers', () => { 276 + it('should complete source argument with directory suggestions', async () => { 277 + const command = `${commandPrefix} copy ""`; 278 + const output = await runCommand(command); 279 + expect(output).toMatchSnapshot(); 280 + }); 281 + 282 + it('should complete destination argument with build suggestions', async () => { 283 + const command = `${commandPrefix} copy src/ ""`; 284 + const output = await runCommand(command); 285 + expect(output).toMatchSnapshot(); 286 + }); 287 + 288 + it('should filter source suggestions when typing partial input', async () => { 289 + const command = `${commandPrefix} copy s`; 290 + const output = await runCommand(command); 291 + expect(output).toMatchSnapshot(); 292 + }); 293 + 294 + it('should filter destination suggestions when typing partial input', async () => { 295 + const command = `${commandPrefix} copy src/ b`; 296 + const output = await runCommand(command); 297 + expect(output).toMatchSnapshot(); 298 + }); 299 + }); 300 + 301 + describe.runIf(!shouldSkipTest)('lint command argument handlers', () => { 302 + it('should complete files argument with file suggestions', async () => { 303 + const command = `${commandPrefix} lint ""`; 304 + const output = await runCommand(command); 305 + expect(output).toMatchSnapshot(); 306 + }); 307 + 308 + it('should filter file suggestions when typing partial input', async () => { 309 + const command = `${commandPrefix} lint m`; 310 + const output = await runCommand(command); 311 + expect(output).toMatchSnapshot(); 312 + }); 151 313 152 - it('should complete multiple positional arguments when ending with part of the value', async () => { 153 - const command = `${commandPrefix} lint ind`; 154 - const output = await runCommand(command); 155 - expect(output).toMatchSnapshot(); 156 - }); 314 + it('should continue completing variadic files argument after first file', async () => { 315 + const command = `${commandPrefix} lint main.ts ""`; 316 + const output = await runCommand(command); 317 + expect(output).toMatchSnapshot(); 318 + }); 157 319 158 - it('should complete single positional argument when ending with space', async () => { 159 - const command = `${commandPrefix} lint main.ts ""`; 160 - const output = await runCommand(command); 161 - expect(output).toMatchSnapshot(); 162 - }); 163 - } 164 - ); 320 + it('should continue completing variadic suggestions after first file', async () => { 321 + const command = `${commandPrefix} lint main.ts i`; 322 + const output = await runCommand(command); 323 + expect(output).toMatchSnapshot(); 324 + }); 325 + }); 165 326 }); 166 327 167 328 // Add specific tests for Commander
+7 -1
tsdown.config.ts
··· 1 1 import { defineConfig } from 'tsdown'; 2 2 3 3 export default defineConfig({ 4 - entry: ['src/index.ts', 'src/citty.ts', 'src/cac.ts', 'src/commander.ts'], 4 + entry: [ 5 + 'src/index.ts', 6 + 'src/citty.ts', 7 + 'src/cac.ts', 8 + 'src/commander.ts', 9 + 'bin/cli.ts', 10 + ], 5 11 format: ['esm'], 6 12 dts: true, 7 13 clean: true,