doctorDiagnostic.ts
utils/doctorDiagnostic.ts
No strong subsystem tag
626
Lines
20502
Bytes
6
Exports
21
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 626 lines, 21 detected imports, and 6 detected exports.
Important relationships
Detected exports
InstallationTypeDiagnosticInfogetCurrentInstallationTypegetInvokedBinarydetectLinuxGlobPatternWarningsgetDoctorDiagnostic
Keywords
pathclaudewarningslocalnativecheckjoininstallationpushissue
Detected imports
execafs/promisesospath./autoUpdater.js./bundledMode.js./config.js./cwd.js./envUtils.js./execFileNoThrow.js./fsOperations.js./localInstaller.js./nativeInstaller/packageManagers.js./platform.js./ripgrep.js./sandbox/sandbox-adapter.js./settings/managedPath.js./settings/types.js./shellConfig.js./slowOperations.js./which.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { execa } from 'execa'
import { readFile, realpath } from 'fs/promises'
import { homedir } from 'os'
import { delimiter, join, posix, win32 } from 'path'
import { checkGlobalInstallPermissions } from './autoUpdater.js'
import { isInBundledMode } from './bundledMode.js'
import {
formatAutoUpdaterDisabledReason,
getAutoUpdaterDisabledReason,
getGlobalConfig,
type InstallMethod,
} from './config.js'
import { getCwd } from './cwd.js'
import { isEnvTruthy } from './envUtils.js'
import { execFileNoThrow } from './execFileNoThrow.js'
import { getFsImplementation } from './fsOperations.js'
import {
getShellType,
isRunningFromLocalInstallation,
localInstallationExists,
} from './localInstaller.js'
import {
detectApk,
detectAsdf,
detectDeb,
detectHomebrew,
detectMise,
detectPacman,
detectRpm,
detectWinget,
getPackageManager,
} from './nativeInstaller/packageManagers.js'
import { getPlatform } from './platform.js'
import { getRipgrepStatus } from './ripgrep.js'
import { SandboxManager } from './sandbox/sandbox-adapter.js'
import { getManagedFilePath } from './settings/managedPath.js'
import { CUSTOMIZATION_SURFACES } from './settings/types.js'
import {
findClaudeAlias,
findValidClaudeAlias,
getShellConfigPaths,
} from './shellConfig.js'
import { jsonParse } from './slowOperations.js'
import { which } from './which.js'
export type InstallationType =
| 'npm-global'
| 'npm-local'
| 'native'
| 'package-manager'
| 'development'
| 'unknown'
export type DiagnosticInfo = {
installationType: InstallationType
version: string
installationPath: string
invokedBinary: string
configInstallMethod: InstallMethod | 'not set'
autoUpdates: string
hasUpdatePermissions: boolean | null
multipleInstallations: Array<{ type: string; path: string }>
warnings: Array<{ issue: string; fix: string }>
recommendation?: string
packageManager?: string
ripgrepStatus: {
working: boolean
mode: 'system' | 'builtin' | 'embedded'
systemPath: string | null
}
}
function getNormalizedPaths(): [invokedPath: string, execPath: string] {
let invokedPath = process.argv[1] || ''
let execPath = process.execPath || process.argv[0] || ''
// On Windows, convert backslashes to forward slashes for consistent path matching
if (getPlatform() === 'windows') {
invokedPath = invokedPath.split(win32.sep).join(posix.sep)
execPath = execPath.split(win32.sep).join(posix.sep)
}
return [invokedPath, execPath]
}
export async function getCurrentInstallationType(): Promise<InstallationType> {
if (process.env.NODE_ENV === 'development') {
return 'development'
}
const [invokedPath] = getNormalizedPaths()
// Check if running in bundled mode first
if (isInBundledMode()) {
// Check if this bundled instance was installed by a package manager
if (
detectHomebrew() ||
detectWinget() ||
detectMise() ||
detectAsdf() ||
(await detectPacman()) ||
(await detectDeb()) ||
(await detectRpm()) ||
(await detectApk())
) {
return 'package-manager'
}
return 'native'
}
// Check if running from local npm installation
if (isRunningFromLocalInstallation()) {
return 'npm-local'
}
// Check if we're in a typical npm global location
const npmGlobalPaths = [
'/usr/local/lib/node_modules',
'/usr/lib/node_modules',
'/opt/homebrew/lib/node_modules',
'/opt/homebrew/bin',
'/usr/local/bin',
'/.nvm/versions/node/', // nvm installations
]
if (npmGlobalPaths.some(path => invokedPath.includes(path))) {
return 'npm-global'
}
// Also check for npm/nvm in the path even if not in standard locations
if (invokedPath.includes('/npm/') || invokedPath.includes('/nvm/')) {
return 'npm-global'
}
const npmConfigResult = await execa('npm config get prefix', {
shell: true,
reject: false,
})
const globalPrefix =
npmConfigResult.exitCode === 0 ? npmConfigResult.stdout.trim() : null
if (globalPrefix && invokedPath.startsWith(globalPrefix)) {
return 'npm-global'
}
// If we can't determine, return unknown
return 'unknown'
}
async function getInstallationPath(): Promise<string> {
if (process.env.NODE_ENV === 'development') {
return getCwd()
}
// For bundled/native builds, show the binary location
if (isInBundledMode()) {
// Try to find the actual binary that was invoked
try {
return await realpath(process.execPath)
} catch {
// This function doesn't expect errors
}
try {
const path = await which('claude')
if (path) {
return path
}
} catch {
// This function doesn't expect errors
}
// If we can't find it, check common locations
try {
await getFsImplementation().stat(join(homedir(), '.local/bin/claude'))
return join(homedir(), '.local/bin/claude')
} catch {
// Not found
}
return 'native'
}
// For npm installations, use the path of the executable
try {
return process.argv[0] || 'unknown'
} catch {
return 'unknown'
}
}
export function getInvokedBinary(): string {
try {
// For bundled/compiled executables, show the actual binary path
if (isInBundledMode()) {
return process.execPath || 'unknown'
}
// For npm/development, show the script path
return process.argv[1] || 'unknown'
} catch {
return 'unknown'
}
}
async function detectMultipleInstallations(): Promise<
Array<{ type: string; path: string }>
> {
const fs = getFsImplementation()
const installations: Array<{ type: string; path: string }> = []
// Check for local installation
const localPath = join(homedir(), '.claude', 'local')
if (await localInstallationExists()) {
installations.push({ type: 'npm-local', path: localPath })
}
// Check for global npm installation
const packagesToCheck = ['@anthropic-ai/claude-code']
if (MACRO.PACKAGE_URL && MACRO.PACKAGE_URL !== '@anthropic-ai/claude-code') {
packagesToCheck.push(MACRO.PACKAGE_URL)
}
const npmResult = await execFileNoThrow('npm', [
'-g',
'config',
'get',
'prefix',
])
if (npmResult.code === 0 && npmResult.stdout) {
const npmPrefix = npmResult.stdout.trim()
const isWindows = getPlatform() === 'windows'
// First check for active installations via bin/claude
// Linux / macOS have prefix/bin/claude and prefix/lib/node_modules
// Windows has prefix/claude and prefix/node_modules
const globalBinPath = isWindows
? join(npmPrefix, 'claude')
: join(npmPrefix, 'bin', 'claude')
let globalBinExists = false
try {
await fs.stat(globalBinPath)
globalBinExists = true
} catch {
// Not found
}
if (globalBinExists) {
// Check if this is actually a Homebrew cask installation, not npm-global
// When npm is installed via Homebrew, both can exist at /opt/homebrew/bin/claude
// We need to resolve the symlink to see where it actually points
let isCurrentHomebrewInstallation = false
try {
// Resolve the symlink to get the actual target
const realPath = await realpath(globalBinPath)
// If the symlink points to a Caskroom directory, it's a Homebrew cask
// Only skip it if it's the same Homebrew installation we're currently running from
if (realPath.includes('/Caskroom/')) {
isCurrentHomebrewInstallation = detectHomebrew()
}
} catch {
// If we can't resolve the symlink, include it anyway
}
if (!isCurrentHomebrewInstallation) {
installations.push({ type: 'npm-global', path: globalBinPath })
}
} else {
// If no bin/claude exists, check for orphaned packages (no bin/claude symlink)
for (const packageName of packagesToCheck) {
const globalPackagePath = isWindows
? join(npmPrefix, 'node_modules', packageName)
: join(npmPrefix, 'lib', 'node_modules', packageName)
try {
await fs.stat(globalPackagePath)
installations.push({
type: 'npm-global-orphan',
path: globalPackagePath,
})
} catch {
// Package not found
}
}
}
}
// Check for native installation
// Check common native installation paths
const nativeBinPath = join(homedir(), '.local', 'bin', 'claude')
try {
await fs.stat(nativeBinPath)
installations.push({ type: 'native', path: nativeBinPath })
} catch {
// Not found
}
// Also check if config indicates native installation
const config = getGlobalConfig()
if (config.installMethod === 'native') {
const nativeDataPath = join(homedir(), '.local', 'share', 'claude')
try {
await fs.stat(nativeDataPath)
if (!installations.some(i => i.type === 'native')) {
installations.push({ type: 'native', path: nativeDataPath })
}
} catch {
// Not found
}
}
return installations
}
async function detectConfigurationIssues(
type: InstallationType,
): Promise<Array<{ issue: string; fix: string }>> {
const warnings: Array<{ issue: string; fix: string }> = []
// Managed-settings forwards-compat: the schema preprocess silently drops
// unknown strictPluginOnlyCustomization surface names so one future enum
// value doesn't null out the entire policy file (settings.ts:101). But
// admins should KNOW — read the raw file and diff. Runs before the
// development-mode early return: this is config correctness, not an
// install-path check, and it's useful to see during dev testing.
try {
const raw = await readFile(
join(getManagedFilePath(), 'managed-settings.json'),
'utf-8',
)
const parsed: unknown = jsonParse(raw)
const field =
parsed && typeof parsed === 'object'
? (parsed as Record<string, unknown>).strictPluginOnlyCustomization
: undefined
if (field !== undefined && typeof field !== 'boolean') {
if (!Array.isArray(field)) {
// .catch(undefined) in the schema silently drops this, so the rest
// of managed settings survive — but the admin typed something
// wrong (an object, a string, etc.).
warnings.push({
issue: `managed-settings.json: strictPluginOnlyCustomization has an invalid value (expected true or an array, got ${typeof field})`,
fix: `The field is silently ignored (schema .catch rescues it). Set it to true, or an array of: ${CUSTOMIZATION_SURFACES.join(', ')}.`,
})
} else {
const unknown = field.filter(
x =>
typeof x === 'string' &&
!(CUSTOMIZATION_SURFACES as readonly string[]).includes(x),
)
if (unknown.length > 0) {
warnings.push({
issue: `managed-settings.json: strictPluginOnlyCustomization has ${unknown.length} value(s) this client doesn't recognize: ${unknown.map(String).join(', ')}`,
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.`,
})
}
}
}
} catch {
// ENOENT (no managed settings) / parse error — not this check's concern.
// Parse errors are surfaced by the settings loader itself.
}
const config = getGlobalConfig()
// Skip most warnings for development mode
if (type === 'development') {
return warnings
}
// Check if ~/.local/bin is in PATH for native installations
if (type === 'native') {
const path = process.env.PATH || ''
const pathDirectories = path.split(delimiter)
const homeDir = homedir()
const localBinPath = join(homeDir, '.local', 'bin')
// On Windows, convert backslashes to forward slashes for consistent path matching
let normalizedLocalBinPath = localBinPath
if (getPlatform() === 'windows') {
normalizedLocalBinPath = localBinPath.split(win32.sep).join(posix.sep)
}
// Check if ~/.local/bin is in PATH (handle both expanded and unexpanded forms)
// Also handle trailing slashes that users may have in their PATH
const localBinInPath = pathDirectories.some(dir => {
let normalizedDir = dir
if (getPlatform() === 'windows') {
normalizedDir = dir.split(win32.sep).join(posix.sep)
}
// Remove trailing slashes for comparison (handles paths like /home/user/.local/bin/)
const trimmedDir = normalizedDir.replace(/\/+$/, '')
const trimmedRawDir = dir.replace(/[/\\]+$/, '')
return (
trimmedDir === normalizedLocalBinPath ||
trimmedRawDir === '~/.local/bin' ||
trimmedRawDir === '$HOME/.local/bin'
)
})
if (!localBinInPath) {
const isWindows = getPlatform() === 'windows'
if (isWindows) {
// Windows-specific PATH instructions
const windowsLocalBinPath = localBinPath
.split(posix.sep)
.join(win32.sep)
warnings.push({
issue: `Native installation exists but ${windowsLocalBinPath} is not in your PATH`,
fix: `Add it by opening: System Properties → Environment Variables → Edit User PATH → New → Add the path above. Then restart your terminal.`,
})
} else {
// Unix-style PATH instructions
const shellType = getShellType()
const configPaths = getShellConfigPaths()
const configFile = configPaths[shellType as keyof typeof configPaths]
const displayPath = configFile
? configFile.replace(homedir(), '~')
: 'your shell config file'
warnings.push({
issue:
'Native installation exists but ~/.local/bin is not in your PATH',
fix: `Run: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${displayPath} then open a new terminal or run: source ${displayPath}`,
})
}
}
}
// Check for configuration mismatches
// Skip these checks if DISABLE_INSTALLATION_CHECKS is set (e.g., in HFI)
if (!isEnvTruthy(process.env.DISABLE_INSTALLATION_CHECKS)) {
if (type === 'npm-local' && config.installMethod !== 'local') {
warnings.push({
issue: `Running from local installation but config install method is '${config.installMethod}'`,
fix: 'Consider using native installation: claude install',
})
}
if (type === 'native' && config.installMethod !== 'native') {
warnings.push({
issue: `Running native installation but config install method is '${config.installMethod}'`,
fix: 'Run claude install to update configuration',
})
}
}
if (type === 'npm-global' && (await localInstallationExists())) {
warnings.push({
issue: 'Local installation exists but not being used',
fix: 'Consider using native installation: claude install',
})
}
const existingAlias = await findClaudeAlias()
const validAlias = await findValidClaudeAlias()
// Check if running local installation but it's not in PATH
if (type === 'npm-local') {
// Check if claude is already accessible via PATH
const whichResult = await which('claude')
const claudeInPath = !!whichResult
// Only show warning if claude is NOT in PATH AND no valid alias exists
if (!claudeInPath && !validAlias) {
if (existingAlias) {
// Alias exists but points to invalid target
warnings.push({
issue: 'Local installation not accessible',
fix: `Alias exists but points to invalid target: ${existingAlias}. Update alias: alias claude="~/.claude/local/claude"`,
})
} else {
// No alias exists and not in PATH
warnings.push({
issue: 'Local installation not accessible',
fix: 'Create alias: alias claude="~/.claude/local/claude"',
})
}
}
}
return warnings
}
export function detectLinuxGlobPatternWarnings(): Array<{
issue: string
fix: string
}> {
if (getPlatform() !== 'linux') {
return []
}
const warnings: Array<{ issue: string; fix: string }> = []
const globPatterns = SandboxManager.getLinuxGlobPatternWarnings()
if (globPatterns.length > 0) {
// Show first 3 patterns, then indicate if there are more
const displayPatterns = globPatterns.slice(0, 3).join(', ')
const remaining = globPatterns.length - 3
const patternList =
remaining > 0 ? `${displayPatterns} (${remaining} more)` : displayPatterns
warnings.push({
issue: `Glob patterns in sandbox permission rules are not fully supported on Linux`,
fix: `Found ${globPatterns.length} pattern(s): ${patternList}. On Linux, glob patterns in Edit/Read rules will be ignored.`,
})
}
return warnings
}
export async function getDoctorDiagnostic(): Promise<DiagnosticInfo> {
const installationType = await getCurrentInstallationType()
const version =
typeof MACRO !== 'undefined' && MACRO.VERSION ? MACRO.VERSION : 'unknown'
const installationPath = await getInstallationPath()
const invokedBinary = getInvokedBinary()
const multipleInstallations = await detectMultipleInstallations()
const warnings = await detectConfigurationIssues(installationType)
// Add glob pattern warnings for Linux sandboxing
warnings.push(...detectLinuxGlobPatternWarnings())
// Add warnings for leftover npm installations when running native
if (installationType === 'native') {
const npmInstalls = multipleInstallations.filter(
i =>
i.type === 'npm-global' ||
i.type === 'npm-global-orphan' ||
i.type === 'npm-local',
)
const isWindows = getPlatform() === 'windows'
for (const install of npmInstalls) {
if (install.type === 'npm-global') {
let uninstallCmd = 'npm -g uninstall @anthropic-ai/claude-code'
if (
MACRO.PACKAGE_URL &&
MACRO.PACKAGE_URL !== '@anthropic-ai/claude-code'
) {
uninstallCmd += ` && npm -g uninstall ${MACRO.PACKAGE_URL}`
}
warnings.push({
issue: `Leftover npm global installation at ${install.path}`,
fix: `Run: ${uninstallCmd}`,
})
} else if (install.type === 'npm-global-orphan') {
warnings.push({
issue: `Orphaned npm global package at ${install.path}`,
fix: isWindows
? `Run: rmdir /s /q "${install.path}"`
: `Run: rm -rf ${install.path}`,
})
} else if (install.type === 'npm-local') {
warnings.push({
issue: `Leftover npm local installation at ${install.path}`,
fix: isWindows
? `Run: rmdir /s /q "${install.path}"`
: `Run: rm -rf ${install.path}`,
})
}
}
}
const config = getGlobalConfig()
// Get config values for display
const configInstallMethod = config.installMethod || 'not set'
// Check permissions for global installations
let hasUpdatePermissions: boolean | null = null
if (installationType === 'npm-global') {
const permCheck = await checkGlobalInstallPermissions()
hasUpdatePermissions = permCheck.hasPermissions
// Add warning if no permissions
if (!hasUpdatePermissions && !getAutoUpdaterDisabledReason()) {
warnings.push({
issue: 'Insufficient permissions for auto-updates',
fix: 'Do one of: (1) Re-install node without sudo, or (2) Use `claude install` for native installation',
})
}
}
// Get ripgrep status and configuration
const ripgrepStatusRaw = getRipgrepStatus()
// Provide simple ripgrep status info
const ripgrepStatus = {
working: ripgrepStatusRaw.working ?? true, // Assume working if not yet tested
mode: ripgrepStatusRaw.mode,
systemPath:
ripgrepStatusRaw.mode === 'system' ? ripgrepStatusRaw.path : null,
}
// Get package manager info if running from package manager
const packageManager =
installationType === 'package-manager'
? await getPackageManager()
: undefined
const diagnostic: DiagnosticInfo = {
installationType,
version,
installationPath,
invokedBinary,
configInstallMethod,
autoUpdates: (() => {
const reason = getAutoUpdaterDisabledReason()
return reason
? `disabled (${formatAutoUpdaterDisabledReason(reason)})`
: 'enabled'
})(),
hasUpdatePermissions,
multipleInstallations,
warnings,
packageManager,
ripgrepStatus,
}
return diagnostic
}