envDynamic.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { feature } from 'bun:bundle'
  2. import { stat } from 'fs/promises'
  3. import memoize from 'lodash-es/memoize.js'
  4. import { env, JETBRAINS_IDES } from './env.js'
  5. import { isEnvTruthy } from './envUtils.js'
  6. import { execFileNoThrow } from './execFileNoThrow.js'
  7. import { getAncestorCommandsAsync } from './genericProcessUtils.js'
  8. // Functions that require execFileNoThrow and thus cannot be in env.ts
  9. const getIsDocker = memoize(async (): Promise<boolean> => {
  10. if (process.platform !== 'linux') return false
  11. // Check for .dockerenv file
  12. const { code } = await execFileNoThrow('test', ['-f', '/.dockerenv'])
  13. return code === 0
  14. })
  15. function getIsBubblewrapSandbox(): boolean {
  16. return (
  17. process.platform === 'linux' &&
  18. isEnvTruthy(process.env.CLAUDE_CODE_BUBBLEWRAP)
  19. )
  20. }
  21. // Cache for the runtime musl detection fallback (node/unbundled only).
  22. // In native linux builds, feature flags resolve this at compile time, so the
  23. // cache is only consulted when both IS_LIBC_MUSL and IS_LIBC_GLIBC are false.
  24. let muslRuntimeCache: boolean | null = null
  25. // Fire-and-forget: populate the musl cache for the node fallback path.
  26. // Native builds never reach this (feature flags short-circuit), so this only
  27. // matters for unbundled node on Linux. Installer calls on native builds are
  28. // unaffected since feature() resolves at compile time.
  29. if (process.platform === 'linux') {
  30. const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64'
  31. void stat(`/lib/libc.musl-${muslArch}.so.1`).then(
  32. () => {
  33. muslRuntimeCache = true
  34. },
  35. () => {
  36. muslRuntimeCache = false
  37. },
  38. )
  39. }
  40. /**
  41. * Checks if the system is using MUSL libc instead of glibc.
  42. * In native linux builds, this is statically known at compile time via IS_LIBC_MUSL/IS_LIBC_GLIBC flags.
  43. * In node (unbundled), both flags are false and we fall back to a runtime async stat check
  44. * whose result is cached at module load. If the cache isn't populated yet, returns false.
  45. */
  46. function isMuslEnvironment(): boolean {
  47. if (feature('IS_LIBC_MUSL')) return true
  48. if (feature('IS_LIBC_GLIBC')) return false
  49. // Fallback for node: runtime detection via pre-populated cache
  50. if (process.platform !== 'linux') return false
  51. return muslRuntimeCache ?? false
  52. }
  53. // Cache for async JetBrains detection
  54. let jetBrainsIDECache: string | null | undefined
  55. async function detectJetBrainsIDEFromParentProcessAsync(): Promise<
  56. string | null
  57. > {
  58. if (jetBrainsIDECache !== undefined) {
  59. return jetBrainsIDECache
  60. }
  61. if (process.platform === 'darwin') {
  62. jetBrainsIDECache = null
  63. return null // macOS uses bundle ID detection which is already handled
  64. }
  65. try {
  66. // Get ancestor commands in a single call (avoids sync bash in loop)
  67. const commands = await getAncestorCommandsAsync(process.pid, 10)
  68. for (const command of commands) {
  69. const lowerCommand = command.toLowerCase()
  70. // Check for specific JetBrains IDEs in the command line
  71. for (const ide of JETBRAINS_IDES) {
  72. if (lowerCommand.includes(ide)) {
  73. jetBrainsIDECache = ide
  74. return ide
  75. }
  76. }
  77. }
  78. } catch {
  79. // Silently fail - this is a best-effort detection
  80. }
  81. jetBrainsIDECache = null
  82. return null
  83. }
  84. export async function getTerminalWithJetBrainsDetectionAsync(): Promise<
  85. string | null
  86. > {
  87. // Check for JetBrains terminal on Linux/Windows
  88. if (process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm') {
  89. // For macOS, bundle ID detection above already handles JetBrains IDEs
  90. if (env.platform !== 'darwin') {
  91. const specificIDE = await detectJetBrainsIDEFromParentProcessAsync()
  92. return specificIDE || 'pycharm'
  93. }
  94. }
  95. return env.terminal
  96. }
  97. // Synchronous version that returns cached result or falls back to env.terminal
  98. // Used for backward compatibility - callers should migrate to async version
  99. export function getTerminalWithJetBrainsDetection(): string | null {
  100. // Check for JetBrains terminal on Linux/Windows
  101. if (process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm') {
  102. // For macOS, bundle ID detection above already handles JetBrains IDEs
  103. if (env.platform !== 'darwin') {
  104. // Return cached value if available, otherwise fall back to generic detection
  105. // The async version should be called early in app initialization to populate cache
  106. if (jetBrainsIDECache !== undefined) {
  107. return jetBrainsIDECache || 'pycharm'
  108. }
  109. // Fall back to generic 'pycharm' if cache not populated yet
  110. return 'pycharm'
  111. }
  112. }
  113. return env.terminal
  114. }
  115. /**
  116. * Initialize JetBrains IDE detection asynchronously.
  117. * Call this early in app initialization to populate the cache.
  118. * After this resolves, getTerminalWithJetBrainsDetection() will return accurate results.
  119. */
  120. export async function initJetBrainsDetection(): Promise<void> {
  121. if (process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm') {
  122. await detectJetBrainsIDEFromParentProcessAsync()
  123. }
  124. }
  125. // Combined export that includes all env properties plus dynamic functions
  126. export const envDynamic = {
  127. ...env, // Include all properties from env
  128. terminal: getTerminalWithJetBrainsDetection(),
  129. getIsDocker,
  130. getIsBubblewrapSandbox,
  131. isMuslEnvironment,
  132. getTerminalWithJetBrainsDetectionAsync,
  133. initJetBrainsDetection,
  134. }