source dump of claude code
at main 347 lines 11 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import { homedir } from 'os' 3import { join } from 'path' 4import { fileSuffixForOauthConfig } from '../constants/oauth.js' 5import { isRunningWithBun } from './bundledMode.js' 6import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js' 7import { findExecutable } from './findExecutable.js' 8import { getFsImplementation } from './fsOperations.js' 9import { which } from './which.js' 10 11type Platform = 'win32' | 'darwin' | 'linux' 12 13// Config and data paths 14export const getGlobalClaudeFile = memoize((): string => { 15 // Legacy fallback for backwards compatibility 16 if ( 17 getFsImplementation().existsSync( 18 join(getClaudeConfigHomeDir(), '.config.json'), 19 ) 20 ) { 21 return join(getClaudeConfigHomeDir(), '.config.json') 22 } 23 24 const filename = `.claude${fileSuffixForOauthConfig()}.json` 25 return join(process.env.CLAUDE_CONFIG_DIR || homedir(), filename) 26}) 27 28const hasInternetAccess = memoize(async (): Promise<boolean> => { 29 try { 30 const { default: axiosClient } = await import('axios') 31 await axiosClient.head('http://1.1.1.1', { 32 signal: AbortSignal.timeout(1000), 33 }) 34 return true 35 } catch { 36 return false 37 } 38}) 39 40async function isCommandAvailable(command: string): Promise<boolean> { 41 try { 42 // which does not execute the file. 43 return !!(await which(command)) 44 } catch { 45 return false 46 } 47} 48 49const detectPackageManagers = memoize(async (): Promise<string[]> => { 50 const packageManagers = [] 51 52 if (await isCommandAvailable('npm')) packageManagers.push('npm') 53 if (await isCommandAvailable('yarn')) packageManagers.push('yarn') 54 if (await isCommandAvailable('pnpm')) packageManagers.push('pnpm') 55 56 return packageManagers 57}) 58 59const detectRuntimes = memoize(async (): Promise<string[]> => { 60 const runtimes = [] 61 62 if (await isCommandAvailable('bun')) runtimes.push('bun') 63 if (await isCommandAvailable('deno')) runtimes.push('deno') 64 if (await isCommandAvailable('node')) runtimes.push('node') 65 66 return runtimes 67}) 68 69/** 70 * Checks if we're running in a WSL environment 71 * @returns true if running in WSL, false otherwise 72 */ 73const isWslEnvironment = memoize((): boolean => { 74 try { 75 // Check for WSLInterop file which is a reliable indicator of WSL 76 return getFsImplementation().existsSync( 77 '/proc/sys/fs/binfmt_misc/WSLInterop', 78 ) 79 } catch (_error) { 80 // If there's an error checking, assume not WSL 81 return false 82 } 83}) 84 85/** 86 * Checks if the npm executable is located in the Windows filesystem within WSL 87 * @returns true if npm is from Windows (starts with /mnt/c/), false otherwise 88 */ 89const isNpmFromWindowsPath = memoize((): boolean => { 90 try { 91 // Only relevant in WSL environment 92 if (!isWslEnvironment()) { 93 return false 94 } 95 96 // Find the actual npm executable path 97 const { cmd } = findExecutable('npm', []) 98 99 // If npm is in Windows path, it will start with /mnt/c/ 100 return cmd.startsWith('/mnt/c/') 101 } catch (_error) { 102 // If there's an error, assume it's not from Windows 103 return false 104 } 105}) 106 107/** 108 * Checks if we're running via Conductor 109 * @returns true if running via Conductor, false otherwise 110 */ 111function isConductor(): boolean { 112 return process.env.__CFBundleIdentifier === 'com.conductor.app' 113} 114 115export const JETBRAINS_IDES = [ 116 'pycharm', 117 'intellij', 118 'webstorm', 119 'phpstorm', 120 'rubymine', 121 'clion', 122 'goland', 123 'rider', 124 'datagrip', 125 'appcode', 126 'dataspell', 127 'aqua', 128 'gateway', 129 'fleet', 130 'jetbrains', 131 'androidstudio', 132] 133 134// Detect terminal type with fallbacks for all platforms 135function detectTerminal(): string | null { 136 if (process.env.CURSOR_TRACE_ID) return 'cursor' 137 // Cursor and Windsurf under WSL have TERM_PROGRAM=vscode 138 if (process.env.VSCODE_GIT_ASKPASS_MAIN?.includes('cursor')) { 139 return 'cursor' 140 } 141 if (process.env.VSCODE_GIT_ASKPASS_MAIN?.includes('windsurf')) { 142 return 'windsurf' 143 } 144 if (process.env.VSCODE_GIT_ASKPASS_MAIN?.includes('antigravity')) { 145 return 'antigravity' 146 } 147 const bundleId = process.env.__CFBundleIdentifier?.toLowerCase() 148 if (bundleId?.includes('vscodium')) return 'codium' 149 if (bundleId?.includes('windsurf')) return 'windsurf' 150 if (bundleId?.includes('com.google.android.studio')) return 'androidstudio' 151 // Check for JetBrains IDEs in bundle ID 152 if (bundleId) { 153 for (const ide of JETBRAINS_IDES) { 154 if (bundleId.includes(ide)) return ide 155 } 156 } 157 158 if (process.env.VisualStudioVersion) { 159 // This is desktop Visual Studio, not VS Code 160 return 'visualstudio' 161 } 162 163 // Check for JetBrains terminal on Linux/Windows 164 if (process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm') { 165 // For macOS, bundle ID detection above already handles JetBrains IDEs 166 if (process.platform === 'darwin') return 'pycharm' 167 168 // For finegrained detection on Linux/Windows use envDynamic.getTerminalWithJetBrainsDetection() 169 return 'pycharm' 170 } 171 172 // Check for specific terminals by TERM before TERM_PROGRAM 173 // This handles cases where TERM and TERM_PROGRAM might be inconsistent 174 if (process.env.TERM === 'xterm-ghostty') { 175 return 'ghostty' 176 } 177 if (process.env.TERM?.includes('kitty')) { 178 return 'kitty' 179 } 180 181 if (process.env.TERM_PROGRAM) { 182 return process.env.TERM_PROGRAM 183 } 184 185 if (process.env.TMUX) return 'tmux' 186 if (process.env.STY) return 'screen' 187 188 // Check for terminal-specific environment variables (common on Linux) 189 if (process.env.KONSOLE_VERSION) return 'konsole' 190 if (process.env.GNOME_TERMINAL_SERVICE) return 'gnome-terminal' 191 if (process.env.XTERM_VERSION) return 'xterm' 192 if (process.env.VTE_VERSION) return 'vte-based' 193 if (process.env.TERMINATOR_UUID) return 'terminator' 194 if (process.env.KITTY_WINDOW_ID) { 195 return 'kitty' 196 } 197 if (process.env.ALACRITTY_LOG) return 'alacritty' 198 if (process.env.TILIX_ID) return 'tilix' 199 200 // Windows-specific detection 201 if (process.env.WT_SESSION) return 'windows-terminal' 202 if (process.env.SESSIONNAME && process.env.TERM === 'cygwin') return 'cygwin' 203 if (process.env.MSYSTEM) return process.env.MSYSTEM.toLowerCase() // MINGW64, MSYS2, etc. 204 if ( 205 process.env.ConEmuANSI || 206 process.env.ConEmuPID || 207 process.env.ConEmuTask 208 ) { 209 return 'conemu' 210 } 211 212 // WSL detection 213 if (process.env.WSL_DISTRO_NAME) return `wsl-${process.env.WSL_DISTRO_NAME}` 214 215 // SSH session detection 216 if (isSSHSession()) { 217 return 'ssh-session' 218 } 219 220 // Fall back to TERM which is more universally available 221 // Special case for common terminal identifiers in TERM 222 if (process.env.TERM) { 223 const term = process.env.TERM 224 if (term.includes('alacritty')) return 'alacritty' 225 if (term.includes('rxvt')) return 'rxvt' 226 if (term.includes('termite')) return 'termite' 227 return process.env.TERM 228 } 229 230 // Detect non-interactive environment 231 if (!process.stdout.isTTY) return 'non-interactive' 232 233 return null 234} 235 236/** 237 * Detects the deployment environment/platform based on environment variables 238 * @returns The deployment platform name, or 'unknown' if not detected 239 */ 240export const detectDeploymentEnvironment = memoize((): string => { 241 // Cloud development environments 242 if (isEnvTruthy(process.env.CODESPACES)) return 'codespaces' 243 if (process.env.GITPOD_WORKSPACE_ID) return 'gitpod' 244 if (process.env.REPL_ID || process.env.REPL_SLUG) return 'replit' 245 if (process.env.PROJECT_DOMAIN) return 'glitch' 246 247 // Cloud platforms 248 if (isEnvTruthy(process.env.VERCEL)) return 'vercel' 249 if ( 250 process.env.RAILWAY_ENVIRONMENT_NAME || 251 process.env.RAILWAY_SERVICE_NAME 252 ) { 253 return 'railway' 254 } 255 if (isEnvTruthy(process.env.RENDER)) return 'render' 256 if (isEnvTruthy(process.env.NETLIFY)) return 'netlify' 257 if (process.env.DYNO) return 'heroku' 258 if (process.env.FLY_APP_NAME || process.env.FLY_MACHINE_ID) return 'fly.io' 259 if (isEnvTruthy(process.env.CF_PAGES)) return 'cloudflare-pages' 260 if (process.env.DENO_DEPLOYMENT_ID) return 'deno-deploy' 261 if (process.env.AWS_LAMBDA_FUNCTION_NAME) return 'aws-lambda' 262 if (process.env.AWS_EXECUTION_ENV === 'AWS_ECS_FARGATE') return 'aws-fargate' 263 if (process.env.AWS_EXECUTION_ENV === 'AWS_ECS_EC2') return 'aws-ecs' 264 // Check for EC2 via hypervisor UUID 265 try { 266 const uuid = getFsImplementation() 267 .readFileSync('/sys/hypervisor/uuid', { encoding: 'utf8' }) 268 .trim() 269 .toLowerCase() 270 if (uuid.startsWith('ec2')) return 'aws-ec2' 271 } catch { 272 // Ignore errors reading hypervisor UUID (ENOENT on non-EC2, etc.) 273 } 274 if (process.env.K_SERVICE) return 'gcp-cloud-run' 275 if (process.env.GOOGLE_CLOUD_PROJECT) return 'gcp' 276 if (process.env.WEBSITE_SITE_NAME || process.env.WEBSITE_SKU) 277 return 'azure-app-service' 278 if (process.env.AZURE_FUNCTIONS_ENVIRONMENT) return 'azure-functions' 279 if (process.env.APP_URL?.includes('ondigitalocean.app')) { 280 return 'digitalocean-app-platform' 281 } 282 if (process.env.SPACE_CREATOR_USER_ID) return 'huggingface-spaces' 283 284 // CI/CD platforms 285 if (isEnvTruthy(process.env.GITHUB_ACTIONS)) return 'github-actions' 286 if (isEnvTruthy(process.env.GITLAB_CI)) return 'gitlab-ci' 287 if (process.env.CIRCLECI) return 'circleci' 288 if (process.env.BUILDKITE) return 'buildkite' 289 if (isEnvTruthy(process.env.CI)) return 'ci' 290 291 // Container orchestration 292 if (process.env.KUBERNETES_SERVICE_HOST) return 'kubernetes' 293 try { 294 if (getFsImplementation().existsSync('/.dockerenv')) return 'docker' 295 } catch { 296 // Ignore errors checking for Docker 297 } 298 299 // Platform-specific fallback for undetected environments 300 if (env.platform === 'darwin') return 'unknown-darwin' 301 if (env.platform === 'linux') return 'unknown-linux' 302 if (env.platform === 'win32') return 'unknown-win32' 303 304 return 'unknown' 305}) 306 307// all of these should be immutable 308function isSSHSession(): boolean { 309 return !!( 310 process.env.SSH_CONNECTION || 311 process.env.SSH_CLIENT || 312 process.env.SSH_TTY 313 ) 314} 315 316export const env = { 317 hasInternetAccess, 318 isCI: isEnvTruthy(process.env.CI), 319 platform: (['win32', 'darwin'].includes(process.platform) 320 ? process.platform 321 : 'linux') as Platform, 322 arch: process.arch, 323 nodeVersion: process.version, 324 terminal: detectTerminal(), 325 isSSH: isSSHSession, 326 getPackageManagers: detectPackageManagers, 327 getRuntimes: detectRuntimes, 328 isRunningWithBun: memoize(isRunningWithBun), 329 isWslEnvironment, 330 isNpmFromWindowsPath, 331 isConductor, 332 detectDeploymentEnvironment, 333} 334 335/** 336 * Returns the host platform for analytics reporting. 337 * If CLAUDE_CODE_HOST_PLATFORM is set to a valid platform value, that overrides 338 * the detected platform. This is useful for container/remote environments where 339 * process.platform reports the container OS but the actual host platform differs. 340 */ 341export function getHostPlatformForAnalytics(): Platform { 342 const override = process.env.CLAUDE_CODE_HOST_PLATFORM 343 if (override === 'win32' || override === 'darwin' || override === 'linux') { 344 return override 345 } 346 return env.platform 347}