π File detail
utils/localInstaller.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes getLocalClaudePath, isRunningFromLocalInstallation, ensureLocalPackageEnvironment, installOrUpdateClaudePackage, and localInstallationExists (and more) β mainly functions, hooks, or classes. Dependencies touch Node filesystem and Node path helpers. It composes internal code from config, envUtils, errors, execFileNoThrow, and fsOperations (relative imports). What the file header says: Utilities for handling local installation.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Utilities for handling local installation
π€ Exports (heuristic)
getLocalClaudePathisRunningFromLocalInstallationensureLocalPackageEnvironmentinstallOrUpdateClaudePackagelocalInstallationExistsgetShellType
π External import roots
Package roots from from "β¦" (relative paths omitted).
fspath
π₯οΈ Source preview
/**
* Utilities for handling local installation
*/
import { access, chmod, writeFile } from 'fs/promises'
import { join } from 'path'
import { type ReleaseChannel, saveGlobalConfig } from './config.js'
import { getClaudeConfigHomeDir } from './envUtils.js'
import { getErrnoCode } from './errors.js'
import { execFileNoThrowWithCwd } from './execFileNoThrow.js'
import { getFsImplementation } from './fsOperations.js'
import { logError } from './log.js'
import { jsonStringify } from './slowOperations.js'
// Lazy getters: getClaudeConfigHomeDir() is memoized and reads process.env.
// Evaluating at module scope would capture the value before entrypoints like
// hfi.tsx get a chance to set CLAUDE_CONFIG_DIR in main(), and would also
// populate the memoize cache with that stale value for all 150+ other callers.
function getLocalInstallDir(): string {
return join(getClaudeConfigHomeDir(), 'local')
}
export function getLocalClaudePath(): string {
return join(getLocalInstallDir(), 'claude')
}
/**
* Check if we're running from our managed local installation
*/
export function isRunningFromLocalInstallation(): boolean {
const execPath = process.argv[1] || ''
return execPath.includes('/.claude/local/node_modules/')
}
/**
* Write `content` to `path` only if the file does not already exist.
* Uses O_EXCL ('wx') for atomic create-if-missing.
*/
async function writeIfMissing(
path: string,
content: string,
mode?: number,
): Promise<boolean> {
try {
await writeFile(path, content, { encoding: 'utf8', flag: 'wx', mode })
return true
} catch (e) {
if (getErrnoCode(e) === 'EEXIST') return false
throw e
}
}
/**
* Ensure the local package environment is set up
* Creates the directory, package.json, and wrapper script
*/
export async function ensureLocalPackageEnvironment(): Promise<boolean> {
try {
const localInstallDir = getLocalInstallDir()
// Create installation directory (recursive, idempotent)
await getFsImplementation().mkdir(localInstallDir)
// Create package.json if it doesn't exist
await writeIfMissing(
join(localInstallDir, 'package.json'),
jsonStringify(
{ name: 'claude-local', version: '0.0.1', private: true },
null,
2,
),
)
// Create the wrapper script if it doesn't exist
const wrapperPath = join(localInstallDir, 'claude')
const created = await writeIfMissing(
wrapperPath,
`#!/bin/sh\nexec "${localInstallDir}/node_modules/.bin/claude" "$@"`,
0o755,
)
if (created) {
// Mode in writeFile is masked by umask; chmod to ensure executable bit.
await chmod(wrapperPath, 0o755)
}
return true
} catch (error) {
logError(error)
return false
}
}
/**
* Install or update Claude CLI package in the local directory
* @param channel - Release channel to use (latest or stable)
* @param specificVersion - Optional specific version to install (overrides channel)
*/
export async function installOrUpdateClaudePackage(
channel: ReleaseChannel,
specificVersion?: string | null,
): Promise<'in_progress' | 'success' | 'install_failed'> {
try {
// First ensure the environment is set up
if (!(await ensureLocalPackageEnvironment())) {
return 'install_failed'
}
// Use specific version if provided, otherwise use channel tag
const versionSpec = specificVersion
? specificVersion
: channel === 'stable'
? 'stable'
: 'latest'
const result = await execFileNoThrowWithCwd(
'npm',
['install', `${MACRO.PACKAGE_URL}@${versionSpec}`],
{ cwd: getLocalInstallDir(), maxBuffer: 1000000 },
)
if (result.code !== 0) {
const error = new Error(
`Failed to install Claude CLI package: ${result.stderr}`,
)
logError(error)
return result.code === 190 ? 'in_progress' : 'install_failed'
}
// Set installMethod to 'local' to prevent npm permission warnings
saveGlobalConfig(current => ({
...current,
installMethod: 'local',
}))
return 'success'
} catch (error) {
logError(error)
return 'install_failed'
}
}
/**
* Check if local installation exists.
* Pure existence probe β callers use this to choose update path / UI hints.
*/
export async function localInstallationExists(): Promise<boolean> {
try {
await access(join(getLocalInstallDir(), 'node_modules', '.bin', 'claude'))
return true
} catch {
return false
}
}
/**
* Get shell type to determine appropriate path setup
*/
export function getShellType(): string {
const shellPath = process.env.SHELL || ''
if (shellPath.includes('zsh')) return 'zsh'
if (shellPath.includes('bash')) return 'bash'
if (shellPath.includes('fish')) return 'fish'
return 'unknown'
}