localInstaller.ts
utils/localInstaller.ts
No strong subsystem tag
163
Lines
4810
Bytes
6
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 163 lines, 9 detected imports, and 6 detected exports.
Important relationships
Detected exports
getLocalClaudePathisRunningFromLocalInstallationensureLocalPackageEnvironmentinstallOrUpdateClaudePackagelocalInstallationExistsgetShellType
Keywords
localclaudejoinpathpackagechannelgetlocalinstalldirlocalinstalldirinstallationlogerror
Detected imports
fs/promisespath./config.js./envUtils.js./errors.js./execFileNoThrow.js./fsOperations.js./log.js./slowOperations.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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'
}