source dump of claude code
at main 162 lines 4.8 kB view raw
1/** 2 * Utilities for handling local installation 3 */ 4 5import { access, chmod, writeFile } from 'fs/promises' 6import { join } from 'path' 7import { type ReleaseChannel, saveGlobalConfig } from './config.js' 8import { getClaudeConfigHomeDir } from './envUtils.js' 9import { getErrnoCode } from './errors.js' 10import { execFileNoThrowWithCwd } from './execFileNoThrow.js' 11import { getFsImplementation } from './fsOperations.js' 12import { logError } from './log.js' 13import { jsonStringify } from './slowOperations.js' 14 15// Lazy getters: getClaudeConfigHomeDir() is memoized and reads process.env. 16// Evaluating at module scope would capture the value before entrypoints like 17// hfi.tsx get a chance to set CLAUDE_CONFIG_DIR in main(), and would also 18// populate the memoize cache with that stale value for all 150+ other callers. 19function getLocalInstallDir(): string { 20 return join(getClaudeConfigHomeDir(), 'local') 21} 22export function getLocalClaudePath(): string { 23 return join(getLocalInstallDir(), 'claude') 24} 25 26/** 27 * Check if we're running from our managed local installation 28 */ 29export function isRunningFromLocalInstallation(): boolean { 30 const execPath = process.argv[1] || '' 31 return execPath.includes('/.claude/local/node_modules/') 32} 33 34/** 35 * Write `content` to `path` only if the file does not already exist. 36 * Uses O_EXCL ('wx') for atomic create-if-missing. 37 */ 38async function writeIfMissing( 39 path: string, 40 content: string, 41 mode?: number, 42): Promise<boolean> { 43 try { 44 await writeFile(path, content, { encoding: 'utf8', flag: 'wx', mode }) 45 return true 46 } catch (e) { 47 if (getErrnoCode(e) === 'EEXIST') return false 48 throw e 49 } 50} 51 52/** 53 * Ensure the local package environment is set up 54 * Creates the directory, package.json, and wrapper script 55 */ 56export async function ensureLocalPackageEnvironment(): Promise<boolean> { 57 try { 58 const localInstallDir = getLocalInstallDir() 59 60 // Create installation directory (recursive, idempotent) 61 await getFsImplementation().mkdir(localInstallDir) 62 63 // Create package.json if it doesn't exist 64 await writeIfMissing( 65 join(localInstallDir, 'package.json'), 66 jsonStringify( 67 { name: 'claude-local', version: '0.0.1', private: true }, 68 null, 69 2, 70 ), 71 ) 72 73 // Create the wrapper script if it doesn't exist 74 const wrapperPath = join(localInstallDir, 'claude') 75 const created = await writeIfMissing( 76 wrapperPath, 77 `#!/bin/sh\nexec "${localInstallDir}/node_modules/.bin/claude" "$@"`, 78 0o755, 79 ) 80 if (created) { 81 // Mode in writeFile is masked by umask; chmod to ensure executable bit. 82 await chmod(wrapperPath, 0o755) 83 } 84 85 return true 86 } catch (error) { 87 logError(error) 88 return false 89 } 90} 91 92/** 93 * Install or update Claude CLI package in the local directory 94 * @param channel - Release channel to use (latest or stable) 95 * @param specificVersion - Optional specific version to install (overrides channel) 96 */ 97export async function installOrUpdateClaudePackage( 98 channel: ReleaseChannel, 99 specificVersion?: string | null, 100): Promise<'in_progress' | 'success' | 'install_failed'> { 101 try { 102 // First ensure the environment is set up 103 if (!(await ensureLocalPackageEnvironment())) { 104 return 'install_failed' 105 } 106 107 // Use specific version if provided, otherwise use channel tag 108 const versionSpec = specificVersion 109 ? specificVersion 110 : channel === 'stable' 111 ? 'stable' 112 : 'latest' 113 const result = await execFileNoThrowWithCwd( 114 'npm', 115 ['install', `${MACRO.PACKAGE_URL}@${versionSpec}`], 116 { cwd: getLocalInstallDir(), maxBuffer: 1000000 }, 117 ) 118 119 if (result.code !== 0) { 120 const error = new Error( 121 `Failed to install Claude CLI package: ${result.stderr}`, 122 ) 123 logError(error) 124 return result.code === 190 ? 'in_progress' : 'install_failed' 125 } 126 127 // Set installMethod to 'local' to prevent npm permission warnings 128 saveGlobalConfig(current => ({ 129 ...current, 130 installMethod: 'local', 131 })) 132 133 return 'success' 134 } catch (error) { 135 logError(error) 136 return 'install_failed' 137 } 138} 139 140/** 141 * Check if local installation exists. 142 * Pure existence probe — callers use this to choose update path / UI hints. 143 */ 144export async function localInstallationExists(): Promise<boolean> { 145 try { 146 await access(join(getLocalInstallDir(), 'node_modules', '.bin', 'claude')) 147 return true 148 } catch { 149 return false 150 } 151} 152 153/** 154 * Get shell type to determine appropriate path setup 155 */ 156export function getShellType(): string { 157 const shellPath = process.env.SHELL || '' 158 if (shellPath.includes('zsh')) return 'zsh' 159 if (shellPath.includes('bash')) return 'bash' 160 if (shellPath.includes('fish')) return 'fish' 161 return 'unknown' 162}