source dump of claude code
at main 245 lines 6.9 kB view raw
1import { homedir } from 'os' 2import { getGlobalConfig, saveGlobalConfig } from '../../../utils/config.js' 3import { logForDebugging } from '../../../utils/debug.js' 4import { 5 execFileNoThrow, 6 execFileNoThrowWithCwd, 7} from '../../../utils/execFileNoThrow.js' 8import { logError } from '../../../utils/log.js' 9 10/** 11 * Package manager types for installing it2. 12 * Listed in order of preference. 13 */ 14export type PythonPackageManager = 'uvx' | 'pipx' | 'pip' 15 16/** 17 * Result of attempting to install it2. 18 */ 19export type It2InstallResult = { 20 success: boolean 21 error?: string 22 packageManager?: PythonPackageManager 23} 24 25/** 26 * Result of verifying it2 setup. 27 */ 28export type It2VerifyResult = { 29 success: boolean 30 error?: string 31 needsPythonApiEnabled?: boolean 32} 33 34/** 35 * Detects which Python package manager is available on the system. 36 * Checks in order of preference: uvx, pipx, pip. 37 * 38 * @returns The detected package manager, or null if none found 39 */ 40export async function detectPythonPackageManager(): Promise<PythonPackageManager | null> { 41 // Check uv first (preferred for isolated environments) 42 // We check for 'uv' since 'uv tool install' is the install command 43 const uvResult = await execFileNoThrow('which', ['uv']) 44 if (uvResult.code === 0) { 45 logForDebugging('[it2Setup] Found uv (will use uv tool install)') 46 return 'uvx' // Keep the type name for compatibility 47 } 48 49 // Check pipx (good for isolated environments) 50 const pipxResult = await execFileNoThrow('which', ['pipx']) 51 if (pipxResult.code === 0) { 52 logForDebugging('[it2Setup] Found pipx package manager') 53 return 'pipx' 54 } 55 56 // Check pip (fallback) 57 const pipResult = await execFileNoThrow('which', ['pip']) 58 if (pipResult.code === 0) { 59 logForDebugging('[it2Setup] Found pip package manager') 60 return 'pip' 61 } 62 63 // Also check pip3 64 const pip3Result = await execFileNoThrow('which', ['pip3']) 65 if (pip3Result.code === 0) { 66 logForDebugging('[it2Setup] Found pip3 package manager') 67 return 'pip' 68 } 69 70 logForDebugging('[it2Setup] No Python package manager found') 71 return null 72} 73 74/** 75 * Checks if the it2 CLI tool is installed and accessible. 76 * 77 * @returns true if it2 is available 78 */ 79export async function isIt2CliAvailable(): Promise<boolean> { 80 const result = await execFileNoThrow('which', ['it2']) 81 return result.code === 0 82} 83 84/** 85 * Installs the it2 CLI tool using the detected package manager. 86 * 87 * @param packageManager - The package manager to use for installation 88 * @returns Result indicating success or failure 89 */ 90export async function installIt2( 91 packageManager: PythonPackageManager, 92): Promise<It2InstallResult> { 93 logForDebugging(`[it2Setup] Installing it2 using ${packageManager}`) 94 95 // Run from home directory to avoid reading project-level pip.conf/uv.toml 96 // which could be maliciously crafted to redirect to an attacker's PyPI server 97 let result 98 switch (packageManager) { 99 case 'uvx': 100 // uv tool install it2 installs it globally in isolated env 101 // (uvx is for running, uv tool install is for installing) 102 result = await execFileNoThrowWithCwd('uv', ['tool', 'install', 'it2'], { 103 cwd: homedir(), 104 }) 105 break 106 case 'pipx': 107 result = await execFileNoThrowWithCwd('pipx', ['install', 'it2'], { 108 cwd: homedir(), 109 }) 110 break 111 case 'pip': 112 // Use --user to install without sudo 113 result = await execFileNoThrowWithCwd( 114 'pip', 115 ['install', '--user', 'it2'], 116 { cwd: homedir() }, 117 ) 118 if (result.code !== 0) { 119 // Try pip3 if pip fails 120 result = await execFileNoThrowWithCwd( 121 'pip3', 122 ['install', '--user', 'it2'], 123 { cwd: homedir() }, 124 ) 125 } 126 break 127 } 128 129 if (result.code !== 0) { 130 const error = result.stderr || 'Unknown installation error' 131 logError(new Error(`[it2Setup] Failed to install it2: ${error}`)) 132 return { 133 success: false, 134 error, 135 packageManager, 136 } 137 } 138 139 logForDebugging('[it2Setup] it2 installed successfully') 140 return { 141 success: true, 142 packageManager, 143 } 144} 145 146/** 147 * Verifies that it2 is properly configured and can communicate with iTerm2. 148 * This tests the Python API connection by running a simple it2 command. 149 * 150 * @returns Result indicating success or the specific failure reason 151 */ 152export async function verifyIt2Setup(): Promise<It2VerifyResult> { 153 logForDebugging('[it2Setup] Verifying it2 setup...') 154 155 // First check if it2 is installed 156 const installed = await isIt2CliAvailable() 157 if (!installed) { 158 return { 159 success: false, 160 error: 'it2 CLI is not installed or not in PATH', 161 } 162 } 163 164 // Try to list sessions - this tests the Python API connection 165 const result = await execFileNoThrow('it2', ['session', 'list']) 166 167 if (result.code !== 0) { 168 const stderr = result.stderr.toLowerCase() 169 170 // Check for common Python API errors 171 if ( 172 stderr.includes('api') || 173 stderr.includes('python') || 174 stderr.includes('connection refused') || 175 stderr.includes('not enabled') 176 ) { 177 logForDebugging('[it2Setup] Python API not enabled in iTerm2') 178 return { 179 success: false, 180 error: 'Python API not enabled in iTerm2 preferences', 181 needsPythonApiEnabled: true, 182 } 183 } 184 185 return { 186 success: false, 187 error: result.stderr || 'Failed to communicate with iTerm2', 188 } 189 } 190 191 logForDebugging('[it2Setup] it2 setup verified successfully') 192 return { 193 success: true, 194 } 195} 196 197/** 198 * Returns instructions for enabling the Python API in iTerm2. 199 */ 200export function getPythonApiInstructions(): string[] { 201 return [ 202 'Almost done! Enable the Python API in iTerm2:', 203 '', 204 ' iTerm2 → Settings → General → Magic → Enable Python API', 205 '', 206 'After enabling, you may need to restart iTerm2.', 207 ] 208} 209 210/** 211 * Marks that it2 setup has been completed successfully. 212 * This prevents showing the setup prompt again. 213 */ 214export function markIt2SetupComplete(): void { 215 const config = getGlobalConfig() 216 if (config.iterm2It2SetupComplete !== true) { 217 saveGlobalConfig(current => ({ 218 ...current, 219 iterm2It2SetupComplete: true, 220 })) 221 logForDebugging('[it2Setup] Marked it2 setup as complete') 222 } 223} 224 225/** 226 * Marks that the user prefers to use tmux over iTerm2 split panes. 227 * This prevents showing the setup prompt when in iTerm2. 228 */ 229export function setPreferTmuxOverIterm2(prefer: boolean): void { 230 const config = getGlobalConfig() 231 if (config.preferTmuxOverIterm2 !== prefer) { 232 saveGlobalConfig(current => ({ 233 ...current, 234 preferTmuxOverIterm2: prefer, 235 })) 236 logForDebugging(`[it2Setup] Set preferTmuxOverIterm2 = ${prefer}`) 237 } 238} 239 240/** 241 * Checks if the user prefers tmux over iTerm2 split panes. 242 */ 243export function getPreferTmuxOverIterm2(): boolean { 244 return getGlobalConfig().preferTmuxOverIterm2 === true 245}