import { homedir } from 'os' import { getGlobalConfig, saveGlobalConfig } from '../../../utils/config.js' import { logForDebugging } from '../../../utils/debug.js' import { execFileNoThrow, execFileNoThrowWithCwd, } from '../../../utils/execFileNoThrow.js' import { logError } from '../../../utils/log.js' /** * Package manager types for installing it2. * Listed in order of preference. */ export type PythonPackageManager = 'uvx' | 'pipx' | 'pip' /** * Result of attempting to install it2. */ export type It2InstallResult = { success: boolean error?: string packageManager?: PythonPackageManager } /** * Result of verifying it2 setup. */ export type It2VerifyResult = { success: boolean error?: string needsPythonApiEnabled?: boolean } /** * Detects which Python package manager is available on the system. * Checks in order of preference: uvx, pipx, pip. * * @returns The detected package manager, or null if none found */ export async function detectPythonPackageManager(): Promise { // Check uv first (preferred for isolated environments) // We check for 'uv' since 'uv tool install' is the install command const uvResult = await execFileNoThrow('which', ['uv']) if (uvResult.code === 0) { logForDebugging('[it2Setup] Found uv (will use uv tool install)') return 'uvx' // Keep the type name for compatibility } // Check pipx (good for isolated environments) const pipxResult = await execFileNoThrow('which', ['pipx']) if (pipxResult.code === 0) { logForDebugging('[it2Setup] Found pipx package manager') return 'pipx' } // Check pip (fallback) const pipResult = await execFileNoThrow('which', ['pip']) if (pipResult.code === 0) { logForDebugging('[it2Setup] Found pip package manager') return 'pip' } // Also check pip3 const pip3Result = await execFileNoThrow('which', ['pip3']) if (pip3Result.code === 0) { logForDebugging('[it2Setup] Found pip3 package manager') return 'pip' } logForDebugging('[it2Setup] No Python package manager found') return null } /** * Checks if the it2 CLI tool is installed and accessible. * * @returns true if it2 is available */ export async function isIt2CliAvailable(): Promise { const result = await execFileNoThrow('which', ['it2']) return result.code === 0 } /** * Installs the it2 CLI tool using the detected package manager. * * @param packageManager - The package manager to use for installation * @returns Result indicating success or failure */ export async function installIt2( packageManager: PythonPackageManager, ): Promise { logForDebugging(`[it2Setup] Installing it2 using ${packageManager}`) // Run from home directory to avoid reading project-level pip.conf/uv.toml // which could be maliciously crafted to redirect to an attacker's PyPI server let result switch (packageManager) { case 'uvx': // uv tool install it2 installs it globally in isolated env // (uvx is for running, uv tool install is for installing) result = await execFileNoThrowWithCwd('uv', ['tool', 'install', 'it2'], { cwd: homedir(), }) break case 'pipx': result = await execFileNoThrowWithCwd('pipx', ['install', 'it2'], { cwd: homedir(), }) break case 'pip': // Use --user to install without sudo result = await execFileNoThrowWithCwd( 'pip', ['install', '--user', 'it2'], { cwd: homedir() }, ) if (result.code !== 0) { // Try pip3 if pip fails result = await execFileNoThrowWithCwd( 'pip3', ['install', '--user', 'it2'], { cwd: homedir() }, ) } break } if (result.code !== 0) { const error = result.stderr || 'Unknown installation error' logError(new Error(`[it2Setup] Failed to install it2: ${error}`)) return { success: false, error, packageManager, } } logForDebugging('[it2Setup] it2 installed successfully') return { success: true, packageManager, } } /** * Verifies that it2 is properly configured and can communicate with iTerm2. * This tests the Python API connection by running a simple it2 command. * * @returns Result indicating success or the specific failure reason */ export async function verifyIt2Setup(): Promise { logForDebugging('[it2Setup] Verifying it2 setup...') // First check if it2 is installed const installed = await isIt2CliAvailable() if (!installed) { return { success: false, error: 'it2 CLI is not installed or not in PATH', } } // Try to list sessions - this tests the Python API connection const result = await execFileNoThrow('it2', ['session', 'list']) if (result.code !== 0) { const stderr = result.stderr.toLowerCase() // Check for common Python API errors if ( stderr.includes('api') || stderr.includes('python') || stderr.includes('connection refused') || stderr.includes('not enabled') ) { logForDebugging('[it2Setup] Python API not enabled in iTerm2') return { success: false, error: 'Python API not enabled in iTerm2 preferences', needsPythonApiEnabled: true, } } return { success: false, error: result.stderr || 'Failed to communicate with iTerm2', } } logForDebugging('[it2Setup] it2 setup verified successfully') return { success: true, } } /** * Returns instructions for enabling the Python API in iTerm2. */ export function getPythonApiInstructions(): string[] { return [ 'Almost done! Enable the Python API in iTerm2:', '', ' iTerm2 → Settings → General → Magic → Enable Python API', '', 'After enabling, you may need to restart iTerm2.', ] } /** * Marks that it2 setup has been completed successfully. * This prevents showing the setup prompt again. */ export function markIt2SetupComplete(): void { const config = getGlobalConfig() if (config.iterm2It2SetupComplete !== true) { saveGlobalConfig(current => ({ ...current, iterm2It2SetupComplete: true, })) logForDebugging('[it2Setup] Marked it2 setup as complete') } } /** * Marks that the user prefers to use tmux over iTerm2 split panes. * This prevents showing the setup prompt when in iTerm2. */ export function setPreferTmuxOverIterm2(prefer: boolean): void { const config = getGlobalConfig() if (config.preferTmuxOverIterm2 !== prefer) { saveGlobalConfig(current => ({ ...current, preferTmuxOverIterm2: prefer, })) logForDebugging(`[it2Setup] Set preferTmuxOverIterm2 = ${prefer}`) } } /** * Checks if the user prefers tmux over iTerm2 split panes. */ export function getPreferTmuxOverIterm2(): boolean { return getGlobalConfig().preferTmuxOverIterm2 === true }