source dump of claude code
at main 167 lines 4.7 kB view raw
1/** 2 * Utilities for managing shell configuration files (like .bashrc, .zshrc) 3 * Used for managing claude aliases and PATH entries 4 */ 5 6import { open, readFile, stat } from 'fs/promises' 7import { homedir as osHomedir } from 'os' 8import { join } from 'path' 9import { isFsInaccessible } from './errors.js' 10import { getLocalClaudePath } from './localInstaller.js' 11 12export const CLAUDE_ALIAS_REGEX = /^\s*alias\s+claude\s*=/ 13 14type EnvLike = Record<string, string | undefined> 15 16type ShellConfigOptions = { 17 env?: EnvLike 18 homedir?: string 19} 20 21/** 22 * Get the paths to shell configuration files 23 * Respects ZDOTDIR for zsh users 24 * @param options Optional overrides for testing (env, homedir) 25 */ 26export function getShellConfigPaths( 27 options?: ShellConfigOptions, 28): Record<string, string> { 29 const home = options?.homedir ?? osHomedir() 30 const env = options?.env ?? process.env 31 const zshConfigDir = env.ZDOTDIR || home 32 return { 33 zsh: join(zshConfigDir, '.zshrc'), 34 bash: join(home, '.bashrc'), 35 fish: join(home, '.config/fish/config.fish'), 36 } 37} 38 39/** 40 * Filter out installer-created claude aliases from an array of lines 41 * Only removes aliases pointing to $HOME/.claude/local/claude 42 * Preserves custom user aliases that point to other locations 43 * Returns the filtered lines and whether our default installer alias was found 44 */ 45export function filterClaudeAliases(lines: string[]): { 46 filtered: string[] 47 hadAlias: boolean 48} { 49 let hadAlias = false 50 const filtered = lines.filter(line => { 51 // Check if this is a claude alias 52 if (CLAUDE_ALIAS_REGEX.test(line)) { 53 // Extract the alias target - handle spaces, quotes, and various formats 54 // First try with quotes 55 let match = line.match(/alias\s+claude\s*=\s*["']([^"']+)["']/) 56 if (!match) { 57 // Try without quotes (capturing until end of line or comment) 58 match = line.match(/alias\s+claude\s*=\s*([^#\n]+)/) 59 } 60 61 if (match && match[1]) { 62 const target = match[1].trim() 63 // Only remove if it points to the installer location 64 // The installer always creates aliases with the full expanded path 65 if (target === getLocalClaudePath()) { 66 hadAlias = true 67 return false // Remove this line 68 } 69 } 70 // Keep custom aliases that don't point to the installer location 71 } 72 return true 73 }) 74 return { filtered, hadAlias } 75} 76 77/** 78 * Read a file and split it into lines 79 * Returns null if file doesn't exist or can't be read 80 */ 81export async function readFileLines( 82 filePath: string, 83): Promise<string[] | null> { 84 try { 85 const content = await readFile(filePath, { encoding: 'utf8' }) 86 return content.split('\n') 87 } catch (e: unknown) { 88 if (isFsInaccessible(e)) return null 89 throw e 90 } 91} 92 93/** 94 * Write lines back to a file 95 */ 96export async function writeFileLines( 97 filePath: string, 98 lines: string[], 99): Promise<void> { 100 const fh = await open(filePath, 'w') 101 try { 102 await fh.writeFile(lines.join('\n'), { encoding: 'utf8' }) 103 await fh.datasync() 104 } finally { 105 await fh.close() 106 } 107} 108 109/** 110 * Check if a claude alias exists in any shell config file 111 * Returns the alias target if found, null otherwise 112 * @param options Optional overrides for testing (env, homedir) 113 */ 114export async function findClaudeAlias( 115 options?: ShellConfigOptions, 116): Promise<string | null> { 117 const configs = getShellConfigPaths(options) 118 119 for (const configPath of Object.values(configs)) { 120 const lines = await readFileLines(configPath) 121 if (!lines) continue 122 123 for (const line of lines) { 124 if (CLAUDE_ALIAS_REGEX.test(line)) { 125 // Extract the alias target 126 const match = line.match(/alias\s+claude=["']?([^"'\s]+)/) 127 if (match && match[1]) { 128 return match[1] 129 } 130 } 131 } 132 } 133 134 return null 135} 136 137/** 138 * Check if a claude alias exists and points to a valid executable 139 * Returns the alias target if valid, null otherwise 140 * @param options Optional overrides for testing (env, homedir) 141 */ 142export async function findValidClaudeAlias( 143 options?: ShellConfigOptions, 144): Promise<string | null> { 145 const aliasTarget = await findClaudeAlias(options) 146 if (!aliasTarget) return null 147 148 const home = options?.homedir ?? osHomedir() 149 150 // Expand ~ to home directory 151 const expandedPath = aliasTarget.startsWith('~') 152 ? aliasTarget.replace('~', home) 153 : aliasTarget 154 155 // Check if the target exists and is executable 156 try { 157 const stats = await stat(expandedPath) 158 // Check if it's a file (could be executable or symlink) 159 if (stats.isFile() || stats.isSymbolicLink()) { 160 return aliasTarget 161 } 162 } catch { 163 // Target doesn't exist or can't be accessed 164 } 165 166 return null 167}