source dump of claude code
at main 625 lines 20 kB view raw
1import { execa } from 'execa' 2import { readFile, realpath } from 'fs/promises' 3import { homedir } from 'os' 4import { delimiter, join, posix, win32 } from 'path' 5import { checkGlobalInstallPermissions } from './autoUpdater.js' 6import { isInBundledMode } from './bundledMode.js' 7import { 8 formatAutoUpdaterDisabledReason, 9 getAutoUpdaterDisabledReason, 10 getGlobalConfig, 11 type InstallMethod, 12} from './config.js' 13import { getCwd } from './cwd.js' 14import { isEnvTruthy } from './envUtils.js' 15import { execFileNoThrow } from './execFileNoThrow.js' 16import { getFsImplementation } from './fsOperations.js' 17import { 18 getShellType, 19 isRunningFromLocalInstallation, 20 localInstallationExists, 21} from './localInstaller.js' 22import { 23 detectApk, 24 detectAsdf, 25 detectDeb, 26 detectHomebrew, 27 detectMise, 28 detectPacman, 29 detectRpm, 30 detectWinget, 31 getPackageManager, 32} from './nativeInstaller/packageManagers.js' 33import { getPlatform } from './platform.js' 34import { getRipgrepStatus } from './ripgrep.js' 35import { SandboxManager } from './sandbox/sandbox-adapter.js' 36import { getManagedFilePath } from './settings/managedPath.js' 37import { CUSTOMIZATION_SURFACES } from './settings/types.js' 38import { 39 findClaudeAlias, 40 findValidClaudeAlias, 41 getShellConfigPaths, 42} from './shellConfig.js' 43import { jsonParse } from './slowOperations.js' 44import { which } from './which.js' 45 46export type InstallationType = 47 | 'npm-global' 48 | 'npm-local' 49 | 'native' 50 | 'package-manager' 51 | 'development' 52 | 'unknown' 53 54export type DiagnosticInfo = { 55 installationType: InstallationType 56 version: string 57 installationPath: string 58 invokedBinary: string 59 configInstallMethod: InstallMethod | 'not set' 60 autoUpdates: string 61 hasUpdatePermissions: boolean | null 62 multipleInstallations: Array<{ type: string; path: string }> 63 warnings: Array<{ issue: string; fix: string }> 64 recommendation?: string 65 packageManager?: string 66 ripgrepStatus: { 67 working: boolean 68 mode: 'system' | 'builtin' | 'embedded' 69 systemPath: string | null 70 } 71} 72 73function getNormalizedPaths(): [invokedPath: string, execPath: string] { 74 let invokedPath = process.argv[1] || '' 75 let execPath = process.execPath || process.argv[0] || '' 76 77 // On Windows, convert backslashes to forward slashes for consistent path matching 78 if (getPlatform() === 'windows') { 79 invokedPath = invokedPath.split(win32.sep).join(posix.sep) 80 execPath = execPath.split(win32.sep).join(posix.sep) 81 } 82 83 return [invokedPath, execPath] 84} 85 86export async function getCurrentInstallationType(): Promise<InstallationType> { 87 if (process.env.NODE_ENV === 'development') { 88 return 'development' 89 } 90 91 const [invokedPath] = getNormalizedPaths() 92 93 // Check if running in bundled mode first 94 if (isInBundledMode()) { 95 // Check if this bundled instance was installed by a package manager 96 if ( 97 detectHomebrew() || 98 detectWinget() || 99 detectMise() || 100 detectAsdf() || 101 (await detectPacman()) || 102 (await detectDeb()) || 103 (await detectRpm()) || 104 (await detectApk()) 105 ) { 106 return 'package-manager' 107 } 108 return 'native' 109 } 110 111 // Check if running from local npm installation 112 if (isRunningFromLocalInstallation()) { 113 return 'npm-local' 114 } 115 116 // Check if we're in a typical npm global location 117 const npmGlobalPaths = [ 118 '/usr/local/lib/node_modules', 119 '/usr/lib/node_modules', 120 '/opt/homebrew/lib/node_modules', 121 '/opt/homebrew/bin', 122 '/usr/local/bin', 123 '/.nvm/versions/node/', // nvm installations 124 ] 125 126 if (npmGlobalPaths.some(path => invokedPath.includes(path))) { 127 return 'npm-global' 128 } 129 130 // Also check for npm/nvm in the path even if not in standard locations 131 if (invokedPath.includes('/npm/') || invokedPath.includes('/nvm/')) { 132 return 'npm-global' 133 } 134 135 const npmConfigResult = await execa('npm config get prefix', { 136 shell: true, 137 reject: false, 138 }) 139 const globalPrefix = 140 npmConfigResult.exitCode === 0 ? npmConfigResult.stdout.trim() : null 141 142 if (globalPrefix && invokedPath.startsWith(globalPrefix)) { 143 return 'npm-global' 144 } 145 146 // If we can't determine, return unknown 147 return 'unknown' 148} 149 150async function getInstallationPath(): Promise<string> { 151 if (process.env.NODE_ENV === 'development') { 152 return getCwd() 153 } 154 155 // For bundled/native builds, show the binary location 156 if (isInBundledMode()) { 157 // Try to find the actual binary that was invoked 158 try { 159 return await realpath(process.execPath) 160 } catch { 161 // This function doesn't expect errors 162 } 163 164 try { 165 const path = await which('claude') 166 if (path) { 167 return path 168 } 169 } catch { 170 // This function doesn't expect errors 171 } 172 173 // If we can't find it, check common locations 174 try { 175 await getFsImplementation().stat(join(homedir(), '.local/bin/claude')) 176 return join(homedir(), '.local/bin/claude') 177 } catch { 178 // Not found 179 } 180 return 'native' 181 } 182 183 // For npm installations, use the path of the executable 184 try { 185 return process.argv[0] || 'unknown' 186 } catch { 187 return 'unknown' 188 } 189} 190 191export function getInvokedBinary(): string { 192 try { 193 // For bundled/compiled executables, show the actual binary path 194 if (isInBundledMode()) { 195 return process.execPath || 'unknown' 196 } 197 198 // For npm/development, show the script path 199 return process.argv[1] || 'unknown' 200 } catch { 201 return 'unknown' 202 } 203} 204 205async function detectMultipleInstallations(): Promise< 206 Array<{ type: string; path: string }> 207> { 208 const fs = getFsImplementation() 209 const installations: Array<{ type: string; path: string }> = [] 210 211 // Check for local installation 212 const localPath = join(homedir(), '.claude', 'local') 213 if (await localInstallationExists()) { 214 installations.push({ type: 'npm-local', path: localPath }) 215 } 216 217 // Check for global npm installation 218 const packagesToCheck = ['@anthropic-ai/claude-code'] 219 if (MACRO.PACKAGE_URL && MACRO.PACKAGE_URL !== '@anthropic-ai/claude-code') { 220 packagesToCheck.push(MACRO.PACKAGE_URL) 221 } 222 const npmResult = await execFileNoThrow('npm', [ 223 '-g', 224 'config', 225 'get', 226 'prefix', 227 ]) 228 if (npmResult.code === 0 && npmResult.stdout) { 229 const npmPrefix = npmResult.stdout.trim() 230 const isWindows = getPlatform() === 'windows' 231 232 // First check for active installations via bin/claude 233 // Linux / macOS have prefix/bin/claude and prefix/lib/node_modules 234 // Windows has prefix/claude and prefix/node_modules 235 const globalBinPath = isWindows 236 ? join(npmPrefix, 'claude') 237 : join(npmPrefix, 'bin', 'claude') 238 239 let globalBinExists = false 240 try { 241 await fs.stat(globalBinPath) 242 globalBinExists = true 243 } catch { 244 // Not found 245 } 246 247 if (globalBinExists) { 248 // Check if this is actually a Homebrew cask installation, not npm-global 249 // When npm is installed via Homebrew, both can exist at /opt/homebrew/bin/claude 250 // We need to resolve the symlink to see where it actually points 251 let isCurrentHomebrewInstallation = false 252 253 try { 254 // Resolve the symlink to get the actual target 255 const realPath = await realpath(globalBinPath) 256 257 // If the symlink points to a Caskroom directory, it's a Homebrew cask 258 // Only skip it if it's the same Homebrew installation we're currently running from 259 if (realPath.includes('/Caskroom/')) { 260 isCurrentHomebrewInstallation = detectHomebrew() 261 } 262 } catch { 263 // If we can't resolve the symlink, include it anyway 264 } 265 266 if (!isCurrentHomebrewInstallation) { 267 installations.push({ type: 'npm-global', path: globalBinPath }) 268 } 269 } else { 270 // If no bin/claude exists, check for orphaned packages (no bin/claude symlink) 271 for (const packageName of packagesToCheck) { 272 const globalPackagePath = isWindows 273 ? join(npmPrefix, 'node_modules', packageName) 274 : join(npmPrefix, 'lib', 'node_modules', packageName) 275 276 try { 277 await fs.stat(globalPackagePath) 278 installations.push({ 279 type: 'npm-global-orphan', 280 path: globalPackagePath, 281 }) 282 } catch { 283 // Package not found 284 } 285 } 286 } 287 } 288 289 // Check for native installation 290 291 // Check common native installation paths 292 const nativeBinPath = join(homedir(), '.local', 'bin', 'claude') 293 try { 294 await fs.stat(nativeBinPath) 295 installations.push({ type: 'native', path: nativeBinPath }) 296 } catch { 297 // Not found 298 } 299 300 // Also check if config indicates native installation 301 const config = getGlobalConfig() 302 if (config.installMethod === 'native') { 303 const nativeDataPath = join(homedir(), '.local', 'share', 'claude') 304 try { 305 await fs.stat(nativeDataPath) 306 if (!installations.some(i => i.type === 'native')) { 307 installations.push({ type: 'native', path: nativeDataPath }) 308 } 309 } catch { 310 // Not found 311 } 312 } 313 314 return installations 315} 316 317async function detectConfigurationIssues( 318 type: InstallationType, 319): Promise<Array<{ issue: string; fix: string }>> { 320 const warnings: Array<{ issue: string; fix: string }> = [] 321 322 // Managed-settings forwards-compat: the schema preprocess silently drops 323 // unknown strictPluginOnlyCustomization surface names so one future enum 324 // value doesn't null out the entire policy file (settings.ts:101). But 325 // admins should KNOW — read the raw file and diff. Runs before the 326 // development-mode early return: this is config correctness, not an 327 // install-path check, and it's useful to see during dev testing. 328 try { 329 const raw = await readFile( 330 join(getManagedFilePath(), 'managed-settings.json'), 331 'utf-8', 332 ) 333 const parsed: unknown = jsonParse(raw) 334 const field = 335 parsed && typeof parsed === 'object' 336 ? (parsed as Record<string, unknown>).strictPluginOnlyCustomization 337 : undefined 338 if (field !== undefined && typeof field !== 'boolean') { 339 if (!Array.isArray(field)) { 340 // .catch(undefined) in the schema silently drops this, so the rest 341 // of managed settings survive — but the admin typed something 342 // wrong (an object, a string, etc.). 343 warnings.push({ 344 issue: `managed-settings.json: strictPluginOnlyCustomization has an invalid value (expected true or an array, got ${typeof field})`, 345 fix: `The field is silently ignored (schema .catch rescues it). Set it to true, or an array of: ${CUSTOMIZATION_SURFACES.join(', ')}.`, 346 }) 347 } else { 348 const unknown = field.filter( 349 x => 350 typeof x === 'string' && 351 !(CUSTOMIZATION_SURFACES as readonly string[]).includes(x), 352 ) 353 if (unknown.length > 0) { 354 warnings.push({ 355 issue: `managed-settings.json: strictPluginOnlyCustomization has ${unknown.length} value(s) this client doesn't recognize: ${unknown.map(String).join(', ')}`, 356 fix: `These are silently ignored (forwards-compat). Known surfaces for this version: ${CUSTOMIZATION_SURFACES.join(', ')}. Either remove them, or this client is older than the managed-settings intended.`, 357 }) 358 } 359 } 360 } 361 } catch { 362 // ENOENT (no managed settings) / parse error — not this check's concern. 363 // Parse errors are surfaced by the settings loader itself. 364 } 365 366 const config = getGlobalConfig() 367 368 // Skip most warnings for development mode 369 if (type === 'development') { 370 return warnings 371 } 372 373 // Check if ~/.local/bin is in PATH for native installations 374 if (type === 'native') { 375 const path = process.env.PATH || '' 376 const pathDirectories = path.split(delimiter) 377 const homeDir = homedir() 378 const localBinPath = join(homeDir, '.local', 'bin') 379 380 // On Windows, convert backslashes to forward slashes for consistent path matching 381 let normalizedLocalBinPath = localBinPath 382 if (getPlatform() === 'windows') { 383 normalizedLocalBinPath = localBinPath.split(win32.sep).join(posix.sep) 384 } 385 386 // Check if ~/.local/bin is in PATH (handle both expanded and unexpanded forms) 387 // Also handle trailing slashes that users may have in their PATH 388 const localBinInPath = pathDirectories.some(dir => { 389 let normalizedDir = dir 390 if (getPlatform() === 'windows') { 391 normalizedDir = dir.split(win32.sep).join(posix.sep) 392 } 393 // Remove trailing slashes for comparison (handles paths like /home/user/.local/bin/) 394 const trimmedDir = normalizedDir.replace(/\/+$/, '') 395 const trimmedRawDir = dir.replace(/[/\\]+$/, '') 396 return ( 397 trimmedDir === normalizedLocalBinPath || 398 trimmedRawDir === '~/.local/bin' || 399 trimmedRawDir === '$HOME/.local/bin' 400 ) 401 }) 402 403 if (!localBinInPath) { 404 const isWindows = getPlatform() === 'windows' 405 if (isWindows) { 406 // Windows-specific PATH instructions 407 const windowsLocalBinPath = localBinPath 408 .split(posix.sep) 409 .join(win32.sep) 410 warnings.push({ 411 issue: `Native installation exists but ${windowsLocalBinPath} is not in your PATH`, 412 fix: `Add it by opening: System Properties → Environment Variables → Edit User PATH → New → Add the path above. Then restart your terminal.`, 413 }) 414 } else { 415 // Unix-style PATH instructions 416 const shellType = getShellType() 417 const configPaths = getShellConfigPaths() 418 const configFile = configPaths[shellType as keyof typeof configPaths] 419 const displayPath = configFile 420 ? configFile.replace(homedir(), '~') 421 : 'your shell config file' 422 423 warnings.push({ 424 issue: 425 'Native installation exists but ~/.local/bin is not in your PATH', 426 fix: `Run: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${displayPath} then open a new terminal or run: source ${displayPath}`, 427 }) 428 } 429 } 430 } 431 432 // Check for configuration mismatches 433 // Skip these checks if DISABLE_INSTALLATION_CHECKS is set (e.g., in HFI) 434 if (!isEnvTruthy(process.env.DISABLE_INSTALLATION_CHECKS)) { 435 if (type === 'npm-local' && config.installMethod !== 'local') { 436 warnings.push({ 437 issue: `Running from local installation but config install method is '${config.installMethod}'`, 438 fix: 'Consider using native installation: claude install', 439 }) 440 } 441 442 if (type === 'native' && config.installMethod !== 'native') { 443 warnings.push({ 444 issue: `Running native installation but config install method is '${config.installMethod}'`, 445 fix: 'Run claude install to update configuration', 446 }) 447 } 448 } 449 450 if (type === 'npm-global' && (await localInstallationExists())) { 451 warnings.push({ 452 issue: 'Local installation exists but not being used', 453 fix: 'Consider using native installation: claude install', 454 }) 455 } 456 457 const existingAlias = await findClaudeAlias() 458 const validAlias = await findValidClaudeAlias() 459 460 // Check if running local installation but it's not in PATH 461 if (type === 'npm-local') { 462 // Check if claude is already accessible via PATH 463 const whichResult = await which('claude') 464 const claudeInPath = !!whichResult 465 466 // Only show warning if claude is NOT in PATH AND no valid alias exists 467 if (!claudeInPath && !validAlias) { 468 if (existingAlias) { 469 // Alias exists but points to invalid target 470 warnings.push({ 471 issue: 'Local installation not accessible', 472 fix: `Alias exists but points to invalid target: ${existingAlias}. Update alias: alias claude="~/.claude/local/claude"`, 473 }) 474 } else { 475 // No alias exists and not in PATH 476 warnings.push({ 477 issue: 'Local installation not accessible', 478 fix: 'Create alias: alias claude="~/.claude/local/claude"', 479 }) 480 } 481 } 482 } 483 484 return warnings 485} 486 487export function detectLinuxGlobPatternWarnings(): Array<{ 488 issue: string 489 fix: string 490}> { 491 if (getPlatform() !== 'linux') { 492 return [] 493 } 494 495 const warnings: Array<{ issue: string; fix: string }> = [] 496 const globPatterns = SandboxManager.getLinuxGlobPatternWarnings() 497 498 if (globPatterns.length > 0) { 499 // Show first 3 patterns, then indicate if there are more 500 const displayPatterns = globPatterns.slice(0, 3).join(', ') 501 const remaining = globPatterns.length - 3 502 const patternList = 503 remaining > 0 ? `${displayPatterns} (${remaining} more)` : displayPatterns 504 505 warnings.push({ 506 issue: `Glob patterns in sandbox permission rules are not fully supported on Linux`, 507 fix: `Found ${globPatterns.length} pattern(s): ${patternList}. On Linux, glob patterns in Edit/Read rules will be ignored.`, 508 }) 509 } 510 511 return warnings 512} 513 514export async function getDoctorDiagnostic(): Promise<DiagnosticInfo> { 515 const installationType = await getCurrentInstallationType() 516 const version = 517 typeof MACRO !== 'undefined' && MACRO.VERSION ? MACRO.VERSION : 'unknown' 518 const installationPath = await getInstallationPath() 519 const invokedBinary = getInvokedBinary() 520 const multipleInstallations = await detectMultipleInstallations() 521 const warnings = await detectConfigurationIssues(installationType) 522 523 // Add glob pattern warnings for Linux sandboxing 524 warnings.push(...detectLinuxGlobPatternWarnings()) 525 526 // Add warnings for leftover npm installations when running native 527 if (installationType === 'native') { 528 const npmInstalls = multipleInstallations.filter( 529 i => 530 i.type === 'npm-global' || 531 i.type === 'npm-global-orphan' || 532 i.type === 'npm-local', 533 ) 534 535 const isWindows = getPlatform() === 'windows' 536 537 for (const install of npmInstalls) { 538 if (install.type === 'npm-global') { 539 let uninstallCmd = 'npm -g uninstall @anthropic-ai/claude-code' 540 if ( 541 MACRO.PACKAGE_URL && 542 MACRO.PACKAGE_URL !== '@anthropic-ai/claude-code' 543 ) { 544 uninstallCmd += ` && npm -g uninstall ${MACRO.PACKAGE_URL}` 545 } 546 warnings.push({ 547 issue: `Leftover npm global installation at ${install.path}`, 548 fix: `Run: ${uninstallCmd}`, 549 }) 550 } else if (install.type === 'npm-global-orphan') { 551 warnings.push({ 552 issue: `Orphaned npm global package at ${install.path}`, 553 fix: isWindows 554 ? `Run: rmdir /s /q "${install.path}"` 555 : `Run: rm -rf ${install.path}`, 556 }) 557 } else if (install.type === 'npm-local') { 558 warnings.push({ 559 issue: `Leftover npm local installation at ${install.path}`, 560 fix: isWindows 561 ? `Run: rmdir /s /q "${install.path}"` 562 : `Run: rm -rf ${install.path}`, 563 }) 564 } 565 } 566 } 567 568 const config = getGlobalConfig() 569 570 // Get config values for display 571 const configInstallMethod = config.installMethod || 'not set' 572 573 // Check permissions for global installations 574 let hasUpdatePermissions: boolean | null = null 575 if (installationType === 'npm-global') { 576 const permCheck = await checkGlobalInstallPermissions() 577 hasUpdatePermissions = permCheck.hasPermissions 578 579 // Add warning if no permissions 580 if (!hasUpdatePermissions && !getAutoUpdaterDisabledReason()) { 581 warnings.push({ 582 issue: 'Insufficient permissions for auto-updates', 583 fix: 'Do one of: (1) Re-install node without sudo, or (2) Use `claude install` for native installation', 584 }) 585 } 586 } 587 588 // Get ripgrep status and configuration 589 const ripgrepStatusRaw = getRipgrepStatus() 590 591 // Provide simple ripgrep status info 592 const ripgrepStatus = { 593 working: ripgrepStatusRaw.working ?? true, // Assume working if not yet tested 594 mode: ripgrepStatusRaw.mode, 595 systemPath: 596 ripgrepStatusRaw.mode === 'system' ? ripgrepStatusRaw.path : null, 597 } 598 599 // Get package manager info if running from package manager 600 const packageManager = 601 installationType === 'package-manager' 602 ? await getPackageManager() 603 : undefined 604 605 const diagnostic: DiagnosticInfo = { 606 installationType, 607 version, 608 installationPath, 609 invokedBinary, 610 configInstallMethod, 611 autoUpdates: (() => { 612 const reason = getAutoUpdaterDisabledReason() 613 return reason 614 ? `disabled (${formatAutoUpdaterDisabledReason(reason)})` 615 : 'enabled' 616 })(), 617 hasUpdatePermissions, 618 multipleInstallations, 619 warnings, 620 packageManager, 621 ripgrepStatus, 622 } 623 624 return diagnostic 625}