env.ts 11 KB

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