/ utils / swarm / backends / detection.ts
detection.ts
  1  import { env } from '../../../utils/env.js'
  2  import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
  3  import { TMUX_COMMAND } from '../constants.js'
  4  
  5  /**
  6   * Captured at module load time to detect if the user started Claude from within tmux.
  7   * Shell.ts may override TMUX env var later, so we capture the original value.
  8   */
  9  // eslint-disable-next-line custom-rules/no-process-env-top-level
 10  const ORIGINAL_USER_TMUX = process.env.TMUX
 11  
 12  /**
 13   * Captured at module load time to get the leader's tmux pane ID.
 14   * TMUX_PANE is set by tmux to the pane ID (e.g., %0, %1) when a process runs inside tmux.
 15   * We capture this at startup so we always know the leader's original pane, even if
 16   * the user switches to a different pane later.
 17   */
 18  // eslint-disable-next-line custom-rules/no-process-env-top-level
 19  const ORIGINAL_TMUX_PANE = process.env.TMUX_PANE
 20  
 21  /** Cached result for isInsideTmux */
 22  let isInsideTmuxCached: boolean | null = null
 23  
 24  /** Cached result for isInITerm2 */
 25  let isInITerm2Cached: boolean | null = null
 26  
 27  /**
 28   * Checks if we're currently running inside a tmux session (synchronous version).
 29   * Uses the original TMUX value captured at module load, not process.env.TMUX,
 30   * because Shell.ts overrides TMUX when Claude's socket is initialized.
 31   *
 32   * IMPORTANT: We ONLY check the TMUX env var. We do NOT run `tmux display-message`
 33   * as a fallback because that command will succeed if ANY tmux server is running
 34   * on the system, not just if THIS process is inside tmux.
 35   */
 36  export function isInsideTmuxSync(): boolean {
 37    return !!ORIGINAL_USER_TMUX
 38  }
 39  
 40  /**
 41   * Checks if we're currently running inside a tmux session.
 42   * Uses the original TMUX value captured at module load, not process.env.TMUX,
 43   * because Shell.ts overrides TMUX when Claude's socket is initialized.
 44   * Caches the result since this won't change during the process lifetime.
 45   *
 46   * IMPORTANT: We ONLY check the TMUX env var. We do NOT run `tmux display-message`
 47   * as a fallback because that command will succeed if ANY tmux server is running
 48   * on the system, not just if THIS process is inside tmux.
 49   */
 50  export async function isInsideTmux(): Promise<boolean> {
 51    if (isInsideTmuxCached !== null) {
 52      return isInsideTmuxCached
 53    }
 54  
 55    // Check the original TMUX env var (captured at module load)
 56    // This tells us if the user started Claude from within their tmux session
 57    // If TMUX is not set, we are NOT inside tmux - period.
 58    isInsideTmuxCached = !!ORIGINAL_USER_TMUX
 59    return isInsideTmuxCached
 60  }
 61  
 62  /**
 63   * Gets the leader's tmux pane ID captured at module load.
 64   * Returns null if not running inside tmux.
 65   */
 66  export function getLeaderPaneId(): string | null {
 67    return ORIGINAL_TMUX_PANE || null
 68  }
 69  
 70  /**
 71   * Checks if tmux is available on the system (installed and in PATH).
 72   */
 73  export async function isTmuxAvailable(): Promise<boolean> {
 74    const result = await execFileNoThrow(TMUX_COMMAND, ['-V'])
 75    return result.code === 0
 76  }
 77  
 78  /**
 79   * Checks if we're currently running inside iTerm2.
 80   * Uses multiple detection methods:
 81   * 1. TERM_PROGRAM env var set to "iTerm.app"
 82   * 2. ITERM_SESSION_ID env var is present
 83   * 3. env.terminal detection from utils/env.ts
 84   *
 85   * Caches the result since this won't change during the process lifetime.
 86   *
 87   * Note: iTerm2 backend uses AppleScript (osascript) which is built into macOS,
 88   * so no external CLI tool installation is required.
 89   */
 90  export function isInITerm2(): boolean {
 91    if (isInITerm2Cached !== null) {
 92      return isInITerm2Cached
 93    }
 94  
 95    // Check multiple indicators for iTerm2
 96    const termProgram = process.env.TERM_PROGRAM
 97    const hasItermSessionId = !!process.env.ITERM_SESSION_ID
 98    const terminalIsITerm = env.terminal === 'iTerm.app'
 99  
100    isInITerm2Cached =
101      termProgram === 'iTerm.app' || hasItermSessionId || terminalIsITerm
102  
103    return isInITerm2Cached
104  }
105  
106  /**
107   * The it2 CLI command name.
108   */
109  export const IT2_COMMAND = 'it2'
110  
111  /**
112   * Checks if the it2 CLI tool is available AND can reach the iTerm2 Python API.
113   * Uses 'session list' (not '--version') because --version succeeds even when
114   * the Python API is disabled in iTerm2 preferences — which would cause
115   * 'session split' to fail later with no fallback.
116   */
117  export async function isIt2CliAvailable(): Promise<boolean> {
118    const result = await execFileNoThrow(IT2_COMMAND, ['session', 'list'])
119    return result.code === 0
120  }
121  
122  /**
123   * Resets all cached detection results. Used for testing.
124   */
125  export function resetDetectionCache(): void {
126    isInsideTmuxCached = null
127    isInITerm2Cached = null
128  }