import ProfileDetector from '../auth/profile-detector'; import { API_SUBCOMMANDS, AUTH_SUBCOMMANDS, BUILTIN_PROVIDER_SHORTCUTS, CLEANUP_FLAGS, COMMAND_FLAG_SUGGESTIONS, CONFIG_SUBCOMMANDS, COPILOT_COMPLETION_SUBCOMMANDS, DOCKER_SUBCOMMANDS, ROOT_COMMAND_FLAGS, ROOT_HELP_TOPICS, TOKENS_FLAGS, PROXY_SUBCOMMANDS, PROVIDER_FLAGS, uniqueStrings, getPublicRootCommandTokens, CLIPROXY_SUBCOMMANDS, MIGRATE_FLAGS, CURSOR_COMPLETION_SUBCOMMANDS, } from './command-catalog'; export interface CompletionSuggestion { value: string; description?: string; } type CompletionShell = 'bash' | 'zsh' | 'fish' | 'powershell'; interface CompletionRequest { current: string; shell: CompletionShell; tokensBeforeCurrent: string[]; } function suggestion(value: string, description?: string): CompletionSuggestion { return { value, description }; } function filterSuggestions( suggestions: readonly CompletionSuggestion[], current: string ): CompletionSuggestion[] { const needle = current.trim().toLowerCase(); const deduped = [...new Map(suggestions.map((entry) => [entry.value, entry])).values()]; if (!needle) return deduped.sort((left, right) => left.value.localeCompare(right.value)); return deduped .filter((entry) => entry.value.toLowerCase().startsWith(needle)) .sort((left, right) => left.value.localeCompare(right.value)); } function getDynamicProfileSuggestions(): CompletionSuggestion[] { const detector = new ProfileDetector(); const profiles = detector.getAllProfiles(); return uniqueStrings([ ...profiles.settings, ...profiles.accounts, ...profiles.cliproxyVariants, ]).map((value) => suggestion(value)); } function getTopLevelSuggestions(): CompletionSuggestion[] { return [ ...getPublicRootCommandTokens().map((value) => suggestion(value)), ...ROOT_COMMAND_FLAGS.map((value) => suggestion(value)), ...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)), ...getDynamicProfileSuggestions(), ]; } function getProfileDetector(): ProfileDetector { return new ProfileDetector(); } function getProfileNames(type: 'settings' | 'accounts' | 'cliproxyVariants'): string[] { return getProfileDetector().getAllProfiles()[type]; } function completeSubcommands( values: readonly string[], flags: readonly string[] = ['--help', '-h'] ): CompletionSuggestion[] { return uniqueStrings([...values, ...flags]).map((value) => suggestion(value)); } function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSuggestion[] { const [command, subcommand] = tokensBeforeCurrent; const lastToken = tokensBeforeCurrent[tokensBeforeCurrent.length - 1]; switch (command) { case undefined: return getTopLevelSuggestions(); case 'help': return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name)); case 'auth': if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS); if (subcommand === 'backup') return completeSubcommands(['default', ...getProfileNames('accounts')], ['--json']); if (subcommand === 'show') return completeSubcommands(getProfileNames('accounts'), ['--json']); if (subcommand === 'remove') return completeSubcommands(getProfileNames('accounts'), ['--yes', '-y']); if (subcommand === 'default') return completeSubcommands(getProfileNames('accounts')); if (subcommand === 'create') { return completeSubcommands( [], ['--force', '--share-context', '--context-group', '--deeper-continuity', '--bare'] ); } if (subcommand === 'list') return completeSubcommands([], ['--verbose', '--json']); return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.auth); case 'api': if (!subcommand) return completeSubcommands(API_SUBCOMMANDS); if (subcommand === 'create') { return completeSubcommands( [], [ '--preset', '--cliproxy-provider', '--base-url', '--api-key', '--model', '--1m', '--no-1m', '--target', '--force', '--yes', '-y', ] ); } if (subcommand === 'discover') return completeSubcommands([], ['--register', '--json', '--force']); if (subcommand === 'copy') return completeSubcommands(getProfileNames('settings'), [ '--target', '--force', '--yes', '-y', ]); if (subcommand === 'export') return completeSubcommands(getProfileNames('settings'), ['--out', '--include-secrets']); if (subcommand === 'import') return completeSubcommands([], ['--name', '--yes', '-y', '--force']); if (subcommand === 'remove') return completeSubcommands(getProfileNames('settings'), ['--yes', '-y']); return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.api); case 'cliproxy': if (!subcommand) return completeSubcommands(CLIPROXY_SUBCOMMANDS, [ '--install', '--latest', '--update', '--backend', '--verbose', '-v', '--help', '-h', ]); if (subcommand === 'routing') { if (lastToken === 'set') { return completeSubcommands(['round-robin', 'fill-first']); } return completeSubcommands(['set', 'explain']); } if (['remove', 'edit'].includes(subcommand)) { return completeSubcommands(getProfileNames('cliproxyVariants'), ['--yes', '-y']); } if (subcommand === 'create' && lastToken === '--provider') { return BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)); } return completeSubcommands( [], ['--provider', '--model', '--target', '--backend', '--force', '--yes', '-y', '--help', '-h'] ); case 'config': if (!subcommand || subcommand.startsWith('-')) { return completeSubcommands(CONFIG_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.config); } return []; case 'docker': if (!subcommand || subcommand.startsWith('-')) { return completeSubcommands(DOCKER_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.docker); } if (subcommand === 'up') return completeSubcommands([], ['--port', '--proxy-port', '--host', '--help', '-h']); if (subcommand === 'logs') return completeSubcommands([], ['--follow', '--service', '--host', '--help', '-h']); return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.docker); case 'cursor': return completeSubcommands(CURSOR_COMPLETION_SUBCOMMANDS); case 'proxy': if (lastToken === '--shell') return completeSubcommands(['auto', 'bash', 'zsh', 'fish', 'powershell']); return completeSubcommands( [...PROXY_SUBCOMMANDS], ['--port', '--shell', '--insecure', '--help', '-h'] ); case 'copilot': return completeSubcommands(COPILOT_COMPLETION_SUBCOMMANDS); case 'env': if (lastToken === '--format') return completeSubcommands(['openai', 'anthropic', 'raw', 'claude-extension']); if (lastToken === '--shell') return completeSubcommands(['auto', 'bash', 'zsh', 'fish', 'powershell']); if (lastToken === '--ide') return completeSubcommands(['vscode', 'cursor', 'windsurf']); return completeSubcommands( uniqueStrings([ ...getProfileNames('settings'), ...getProfileNames('accounts'), ...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => entry.name), ]), COMMAND_FLAG_SUGGESTIONS.env ); case 'tokens': if (lastToken === '--variant') return completeSubcommands(getProfileNames('cliproxyVariants')); return completeSubcommands([], TOKENS_FLAGS); case 'migrate': return completeSubcommands([], MIGRATE_FLAGS); case 'cleanup': return completeSubcommands([], CLEANUP_FLAGS); case '--shell-completion': case '-sc': return completeSubcommands( [], ['--bash', '--zsh', '--fish', '--powershell', '--force', '-f'] ); default: if (BUILTIN_PROVIDER_SHORTCUTS.some((entry) => entry.name === command)) { if (command === 'agy') return completeSubcommands([], [...PROVIDER_FLAGS, '--accept-agr-risk']); if (command === 'kiro') { return completeSubcommands( [], [ ...PROVIDER_FLAGS, '--kiro-auth-method', '--kiro-idc-start-url', '--kiro-idc-region', '--kiro-idc-flow', '--import', '--incognito', ] ); } return completeSubcommands([], PROVIDER_FLAGS); } return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS[command] || []); } } export function getCompletionSuggestions(request: CompletionRequest): CompletionSuggestion[] { return filterSuggestions(getSuggestionsForCommand(request.tokensBeforeCurrent), request.current); } function parseCompletionArgs(args: string[]): CompletionRequest { const shellIndex = args.indexOf('--shell'); const currentIndex = args.indexOf('--current'); const separatorIndex = args.indexOf('--'); const shell = (shellIndex !== -1 ? args[shellIndex + 1] : 'bash') as CompletionShell; const current = currentIndex !== -1 ? args[currentIndex + 1] || '' : ''; const tokensBeforeCurrent = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1); return { shell, current, tokensBeforeCurrent }; } function formatForShell( shell: CompletionShell, suggestions: readonly CompletionSuggestion[] ): string[] { if (shell === 'fish' || shell === 'powershell') { return suggestions.map((entry) => entry.description ? `${entry.value}\t${entry.description}` : entry.value ); } return suggestions.map((entry) => entry.value); } export async function handleCompletionCommand(args: string[]): Promise { try { const request = parseCompletionArgs(args); const suggestions = getCompletionSuggestions(request); process.stdout.write(formatForShell(request.shell, suggestions).join('\n')); } catch { // Completion must fail closed and quietly so shell TAB does not surface stack traces. } }