source dump of claude code
at main 166 lines 5.7 kB view raw
1import chalk from 'chalk' 2import { mkdir, readFile, writeFile } from 'fs/promises' 3import { homedir } from 'os' 4import { dirname, join } from 'path' 5import { pathToFileURL } from 'url' 6import { color } from '../components/design-system/color.js' 7import { supportsHyperlinks } from '../ink/supports-hyperlinks.js' 8import { logForDebugging } from './debug.js' 9import { isENOENT } from './errors.js' 10import { execFileNoThrow } from './execFileNoThrow.js' 11import { logError } from './log.js' 12import type { ThemeName } from './theme.js' 13 14const EOL = '\n' 15 16type ShellInfo = { 17 name: string 18 rcFile: string 19 cacheFile: string 20 completionLine: string 21 shellFlag: string 22} 23 24function detectShell(): ShellInfo | null { 25 const shell = process.env.SHELL || '' 26 const home = homedir() 27 const claudeDir = join(home, '.claude') 28 29 if (shell.endsWith('/zsh') || shell.endsWith('/zsh.exe')) { 30 const cacheFile = join(claudeDir, 'completion.zsh') 31 return { 32 name: 'zsh', 33 rcFile: join(home, '.zshrc'), 34 cacheFile, 35 completionLine: `[[ -f "${cacheFile}" ]] && source "${cacheFile}"`, 36 shellFlag: 'zsh', 37 } 38 } 39 if (shell.endsWith('/bash') || shell.endsWith('/bash.exe')) { 40 const cacheFile = join(claudeDir, 'completion.bash') 41 return { 42 name: 'bash', 43 rcFile: join(home, '.bashrc'), 44 cacheFile, 45 completionLine: `[ -f "${cacheFile}" ] && source "${cacheFile}"`, 46 shellFlag: 'bash', 47 } 48 } 49 if (shell.endsWith('/fish') || shell.endsWith('/fish.exe')) { 50 const xdg = process.env.XDG_CONFIG_HOME || join(home, '.config') 51 const cacheFile = join(claudeDir, 'completion.fish') 52 return { 53 name: 'fish', 54 rcFile: join(xdg, 'fish', 'config.fish'), 55 cacheFile, 56 completionLine: `[ -f "${cacheFile}" ] && source "${cacheFile}"`, 57 shellFlag: 'fish', 58 } 59 } 60 return null 61} 62 63function formatPathLink(filePath: string): string { 64 if (!supportsHyperlinks()) { 65 return filePath 66 } 67 const fileUrl = pathToFileURL(filePath).href 68 return `\x1b]8;;${fileUrl}\x07${filePath}\x1b]8;;\x07` 69} 70 71/** 72 * Generate and cache the completion script, then add a source line to the 73 * shell's rc file. Returns a user-facing status message. 74 */ 75export async function setupShellCompletion(theme: ThemeName): Promise<string> { 76 const shell = detectShell() 77 if (!shell) { 78 return '' 79 } 80 81 // Ensure the cache directory exists 82 try { 83 await mkdir(dirname(shell.cacheFile), { recursive: true }) 84 } catch (e: unknown) { 85 logError(e) 86 return `${EOL}${color('warning', theme)(`Could not write ${shell.name} completion cache`)}${EOL}${chalk.dim(`Run manually: claude completion ${shell.shellFlag} > ${shell.cacheFile}`)}${EOL}` 87 } 88 89 // Generate the completion script by writing directly to the cache file. 90 // Using --output avoids piping through stdout where process.exit() can 91 // truncate output before the pipe buffer drains. 92 const claudeBin = process.argv[1] || 'claude' 93 const result = await execFileNoThrow(claudeBin, [ 94 'completion', 95 shell.shellFlag, 96 '--output', 97 shell.cacheFile, 98 ]) 99 if (result.code !== 0) { 100 return `${EOL}${color('warning', theme)(`Could not generate ${shell.name} shell completions`)}${EOL}${chalk.dim(`Run manually: claude completion ${shell.shellFlag} > ${shell.cacheFile}`)}${EOL}` 101 } 102 103 // Check if rc file already sources completions 104 let existing = '' 105 try { 106 existing = await readFile(shell.rcFile, { encoding: 'utf-8' }) 107 if ( 108 existing.includes('claude completion') || 109 existing.includes(shell.cacheFile) 110 ) { 111 return `${EOL}${color('success', theme)(`Shell completions updated for ${shell.name}`)}${EOL}${chalk.dim(`See ${formatPathLink(shell.rcFile)}`)}${EOL}` 112 } 113 } catch (e: unknown) { 114 if (!isENOENT(e)) { 115 logError(e) 116 return `${EOL}${color('warning', theme)(`Could not install ${shell.name} shell completions`)}${EOL}${chalk.dim(`Add this to ${formatPathLink(shell.rcFile)}:`)}${EOL}${chalk.dim(shell.completionLine)}${EOL}` 117 } 118 } 119 120 // Append source line to rc file 121 try { 122 const configDir = dirname(shell.rcFile) 123 await mkdir(configDir, { recursive: true }) 124 125 const separator = existing && !existing.endsWith('\n') ? '\n' : '' 126 const content = `${existing}${separator}\n# Claude Code shell completions\n${shell.completionLine}\n` 127 await writeFile(shell.rcFile, content, { encoding: 'utf-8' }) 128 129 return `${EOL}${color('success', theme)(`Installed ${shell.name} shell completions`)}${EOL}${chalk.dim(`Added to ${formatPathLink(shell.rcFile)}`)}${EOL}${chalk.dim(`Run: source ${shell.rcFile}`)}${EOL}` 130 } catch (error) { 131 logError(error) 132 return `${EOL}${color('warning', theme)(`Could not install ${shell.name} shell completions`)}${EOL}${chalk.dim(`Add this to ${formatPathLink(shell.rcFile)}:`)}${EOL}${chalk.dim(shell.completionLine)}${EOL}` 133 } 134} 135 136/** 137 * Regenerate cached shell completion scripts in ~/.claude/. 138 * Called after `claude update` so completions stay in sync with the new binary. 139 */ 140export async function regenerateCompletionCache(): Promise<void> { 141 const shell = detectShell() 142 if (!shell) { 143 return 144 } 145 146 logForDebugging(`update: Regenerating ${shell.name} completion cache`) 147 148 const claudeBin = process.argv[1] || 'claude' 149 const result = await execFileNoThrow(claudeBin, [ 150 'completion', 151 shell.shellFlag, 152 '--output', 153 shell.cacheFile, 154 ]) 155 156 if (result.code !== 0) { 157 logForDebugging( 158 `update: Failed to regenerate ${shell.name} completion cache`, 159 ) 160 return 161 } 162 163 logForDebugging( 164 `update: Regenerated ${shell.name} completion cache at ${shell.cacheFile}`, 165 ) 166}