source dump of claude code
at main 336 lines 9.0 kB view raw
1/** 2 * Package manager detection for Claude CLI 3 */ 4 5import { readFile } from 'fs/promises' 6import memoize from 'lodash-es/memoize.js' 7import { logForDebugging } from '../debug.js' 8import { execFileNoThrow } from '../execFileNoThrow.js' 9import { getPlatform } from '../platform.js' 10 11export type PackageManager = 12 | 'homebrew' 13 | 'winget' 14 | 'pacman' 15 | 'deb' 16 | 'rpm' 17 | 'apk' 18 | 'mise' 19 | 'asdf' 20 | 'unknown' 21 22/** 23 * Parses /etc/os-release to extract the distro ID and ID_LIKE fields. 24 * ID_LIKE identifies the distro family (e.g. Ubuntu has ID_LIKE=debian), 25 * letting us skip package manager execs on distros that can't have them. 26 * Returns null if the file is unreadable (pre-systemd or non-standard systems); 27 * callers fall through to the exec in that case as a conservative fallback. 28 */ 29export const getOsRelease = memoize( 30 async (): Promise<{ id: string; idLike: string[] } | null> => { 31 try { 32 const content = await readFile('/etc/os-release', 'utf8') 33 const idMatch = content.match(/^ID=["']?(\S+?)["']?\s*$/m) 34 const idLikeMatch = content.match(/^ID_LIKE=["']?(.+?)["']?\s*$/m) 35 return { 36 id: idMatch?.[1] ?? '', 37 idLike: idLikeMatch?.[1]?.split(' ') ?? [], 38 } 39 } catch { 40 return null 41 } 42 }, 43) 44 45function isDistroFamily( 46 osRelease: { id: string; idLike: string[] }, 47 families: string[], 48): boolean { 49 return ( 50 families.includes(osRelease.id) || 51 osRelease.idLike.some(like => families.includes(like)) 52 ) 53} 54 55/** 56 * Detects if the currently running Claude instance was installed via mise 57 * (a polyglot tool version manager) by checking if the executable path 58 * is within a mise installs directory. 59 * 60 * mise installs to: ~/.local/share/mise/installs/<tool>/<version>/ 61 */ 62export function detectMise(): boolean { 63 const execPath = process.execPath || process.argv[0] || '' 64 65 // Check if the executable is within a mise installs directory 66 if (/[/\\]mise[/\\]installs[/\\]/i.test(execPath)) { 67 logForDebugging(`Detected mise installation: ${execPath}`) 68 return true 69 } 70 71 return false 72} 73 74/** 75 * Detects if the currently running Claude instance was installed via asdf 76 * (another polyglot tool version manager) by checking if the executable path 77 * is within an asdf installs directory. 78 * 79 * asdf installs to: ~/.asdf/installs/<tool>/<version>/ 80 */ 81export function detectAsdf(): boolean { 82 const execPath = process.execPath || process.argv[0] || '' 83 84 // Check if the executable is within an asdf installs directory 85 if (/[/\\]\.?asdf[/\\]installs[/\\]/i.test(execPath)) { 86 logForDebugging(`Detected asdf installation: ${execPath}`) 87 return true 88 } 89 90 return false 91} 92 93/** 94 * Detects if the currently running Claude instance was installed via Homebrew 95 * by checking if the executable path is within a Homebrew Caskroom directory. 96 * 97 * Note: We specifically check for Caskroom because npm can also be installed via 98 * Homebrew, which would place npm global packages under the same Homebrew prefix 99 * (e.g., /opt/homebrew/lib/node_modules). We need to distinguish between: 100 * - Homebrew cask: /opt/homebrew/Caskroom/claude-code/... 101 * - npm-global (via Homebrew's npm): /opt/homebrew/lib/node_modules/@anthropic-ai/... 102 */ 103export function detectHomebrew(): boolean { 104 const platform = getPlatform() 105 106 // Homebrew is only for macOS and Linux 107 if (platform !== 'macos' && platform !== 'linux' && platform !== 'wsl') { 108 return false 109 } 110 111 // Get the path of the currently running executable 112 const execPath = process.execPath || process.argv[0] || '' 113 114 // Check if the executable is within a Homebrew Caskroom directory 115 // This is specific to Homebrew cask installations 116 if (execPath.includes('/Caskroom/')) { 117 logForDebugging(`Detected Homebrew cask installation: ${execPath}`) 118 return true 119 } 120 121 return false 122} 123 124/** 125 * Detects if the currently running Claude instance was installed via winget 126 * by checking if the executable path is within a WinGet directory. 127 * 128 * Winget installs to: 129 * - User: %LOCALAPPDATA%\Microsoft\WinGet\Packages 130 * - System: C:\Program Files\WinGet\Packages 131 * And creates links at: %LOCALAPPDATA%\Microsoft\WinGet\Links\ 132 */ 133export function detectWinget(): boolean { 134 const platform = getPlatform() 135 136 // Winget is only for Windows 137 if (platform !== 'windows') { 138 return false 139 } 140 141 const execPath = process.execPath || process.argv[0] || '' 142 143 // Check for WinGet paths (handles both forward and backslashes) 144 const wingetPatterns = [ 145 /Microsoft[/\\]WinGet[/\\]Packages/i, 146 /Microsoft[/\\]WinGet[/\\]Links/i, 147 ] 148 149 for (const pattern of wingetPatterns) { 150 if (pattern.test(execPath)) { 151 logForDebugging(`Detected winget installation: ${execPath}`) 152 return true 153 } 154 } 155 156 return false 157} 158 159/** 160 * Detects if the currently running Claude instance was installed via pacman 161 * by querying pacman's database for file ownership. 162 * 163 * We gate on the Arch distro family before invoking pacman. On other distros 164 * like Ubuntu/Debian, 'pacman' in PATH may resolve to the pacman game 165 * (/usr/games/pacman) rather than the Arch package manager. 166 */ 167export const detectPacman = memoize(async (): Promise<boolean> => { 168 const platform = getPlatform() 169 170 if (platform !== 'linux') { 171 return false 172 } 173 174 const osRelease = await getOsRelease() 175 if (osRelease && !isDistroFamily(osRelease, ['arch'])) { 176 return false 177 } 178 179 const execPath = process.execPath || process.argv[0] || '' 180 181 const result = await execFileNoThrow('pacman', ['-Qo', execPath], { 182 timeout: 5000, 183 useCwd: false, 184 }) 185 186 if (result.code === 0 && result.stdout) { 187 logForDebugging(`Detected pacman installation: ${result.stdout.trim()}`) 188 return true 189 } 190 191 return false 192}) 193 194/** 195 * Detects if the currently running Claude instance was installed via a .deb package 196 * by querying dpkg's database for file ownership. 197 * 198 * We use `dpkg -S <execPath>` to check if the executable is owned by a dpkg-managed package. 199 */ 200export const detectDeb = memoize(async (): Promise<boolean> => { 201 const platform = getPlatform() 202 203 if (platform !== 'linux') { 204 return false 205 } 206 207 const osRelease = await getOsRelease() 208 if (osRelease && !isDistroFamily(osRelease, ['debian'])) { 209 return false 210 } 211 212 const execPath = process.execPath || process.argv[0] || '' 213 214 const result = await execFileNoThrow('dpkg', ['-S', execPath], { 215 timeout: 5000, 216 useCwd: false, 217 }) 218 219 if (result.code === 0 && result.stdout) { 220 logForDebugging(`Detected deb installation: ${result.stdout.trim()}`) 221 return true 222 } 223 224 return false 225}) 226 227/** 228 * Detects if the currently running Claude instance was installed via an RPM package 229 * by querying the RPM database for file ownership. 230 * 231 * We use `rpm -qf <execPath>` to check if the executable is owned by an RPM package. 232 */ 233export const detectRpm = memoize(async (): Promise<boolean> => { 234 const platform = getPlatform() 235 236 if (platform !== 'linux') { 237 return false 238 } 239 240 const osRelease = await getOsRelease() 241 if (osRelease && !isDistroFamily(osRelease, ['fedora', 'rhel', 'suse'])) { 242 return false 243 } 244 245 const execPath = process.execPath || process.argv[0] || '' 246 247 const result = await execFileNoThrow('rpm', ['-qf', execPath], { 248 timeout: 5000, 249 useCwd: false, 250 }) 251 252 if (result.code === 0 && result.stdout) { 253 logForDebugging(`Detected rpm installation: ${result.stdout.trim()}`) 254 return true 255 } 256 257 return false 258}) 259 260/** 261 * Detects if the currently running Claude instance was installed via Alpine APK 262 * by querying apk's database for file ownership. 263 * 264 * We use `apk info --who-owns <execPath>` to check if the executable is owned 265 * by an apk-managed package. 266 */ 267export const detectApk = memoize(async (): Promise<boolean> => { 268 const platform = getPlatform() 269 270 if (platform !== 'linux') { 271 return false 272 } 273 274 const osRelease = await getOsRelease() 275 if (osRelease && !isDistroFamily(osRelease, ['alpine'])) { 276 return false 277 } 278 279 const execPath = process.execPath || process.argv[0] || '' 280 281 const result = await execFileNoThrow( 282 'apk', 283 ['info', '--who-owns', execPath], 284 { 285 timeout: 5000, 286 useCwd: false, 287 }, 288 ) 289 290 if (result.code === 0 && result.stdout) { 291 logForDebugging(`Detected apk installation: ${result.stdout.trim()}`) 292 return true 293 } 294 295 return false 296}) 297 298/** 299 * Memoized function to detect which package manager installed Claude 300 * Returns 'unknown' if no package manager is detected 301 */ 302export const getPackageManager = memoize(async (): Promise<PackageManager> => { 303 if (detectHomebrew()) { 304 return 'homebrew' 305 } 306 307 if (detectWinget()) { 308 return 'winget' 309 } 310 311 if (detectMise()) { 312 return 'mise' 313 } 314 315 if (detectAsdf()) { 316 return 'asdf' 317 } 318 319 if (await detectPacman()) { 320 return 'pacman' 321 } 322 323 if (await detectApk()) { 324 return 'apk' 325 } 326 327 if (await detectDeb()) { 328 return 'deb' 329 } 330 331 if (await detectRpm()) { 332 return 'rpm' 333 } 334 335 return 'unknown' 336})