Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
15 kB · 523 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524/** * Download functionality for native installer * * Handles downloading Claude binaries from various sources: * - Artifactory NPM packages * - GCS bucket */
import { feature } from 'bun:bundle'import axios from 'axios'import { createHash } from 'crypto'import { chmod, writeFile } from 'fs/promises'import { join } from 'path'import { logEvent } from 'src/services/analytics/index.js'import type { ReleaseChannel } from '../config.js'import { logForDebugging } from '../debug.js'import { toError } from '../errors.js'import { execFileNoThrowWithCwd } from '../execFileNoThrow.js'import { getFsImplementation } from '../fsOperations.js'import { logError } from '../log.js'import { sleep } from '../sleep.js'import { jsonStringify, writeFileSync_DEPRECATED } from '../slowOperations.js'import { getBinaryName, getPlatform } from './installer.js'
const GCS_BUCKET_URL = 'https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases'export const ARTIFACTORY_REGISTRY_URL = 'https://artifactory.infra.ant.dev/artifactory/api/npm/npm-all/'
export async function getLatestVersionFromArtifactory( tag: string = 'latest',): Promise<string> { const startTime = Date.now() const { stdout, code, stderr } = await execFileNoThrowWithCwd( 'npm', [ 'view', `${MACRO.NATIVE_PACKAGE_URL}@${tag}`, 'version', '--prefer-online', '--registry', ARTIFACTORY_REGISTRY_URL, ], { timeout: 30000, preserveOutputOnError: true, }, )
const latencyMs = Date.now() - startTime
if (code !== 0) { logEvent('tengu_version_check_failure', { latency_ms: latencyMs, source_npm: true, exit_code: code, }) const error = new Error(`npm view failed with code ${code}: ${stderr}`) logError(error) throw error }
logEvent('tengu_version_check_success', { latency_ms: latencyMs, source_npm: true, }) logForDebugging( `npm view ${MACRO.NATIVE_PACKAGE_URL}@${tag} version: ${stdout}`, ) const latestVersion = stdout.trim() return latestVersion}
export async function getLatestVersionFromBinaryRepo( channel: ReleaseChannel = 'latest', baseUrl: string, authConfig?: { auth: { username: string; password: string } },): Promise<string> { const startTime = Date.now() try { const response = await axios.get(`${baseUrl}/${channel}`, { timeout: 30000, responseType: 'text', ...authConfig, }) const latencyMs = Date.now() - startTime logEvent('tengu_version_check_success', { latency_ms: latencyMs, }) return response.data.trim() } catch (error) { const latencyMs = Date.now() - startTime const errorMessage = error instanceof Error ? error.message : String(error) let httpStatus: number | undefined if (axios.isAxiosError(error) && error.response) { httpStatus = error.response.status }
logEvent('tengu_version_check_failure', { latency_ms: latencyMs, http_status: httpStatus, is_timeout: errorMessage.includes('timeout'), }) const fetchError = new Error( `Failed to fetch version from ${baseUrl}/${channel}: ${errorMessage}`, ) logError(fetchError) throw fetchError }}
export async function getLatestVersion( channelOrVersion: string,): Promise<string> { // Direct version - match internal format too (e.g. 1.0.30-dev.shaf4937ce) if (/^v?\d+\.\d+\.\d+(-\S+)?$/.test(channelOrVersion)) { const normalized = channelOrVersion.startsWith('v') ? channelOrVersion.slice(1) : channelOrVersion // 99.99.x is reserved for CI smoke-test fixtures on real GCS. // feature() is false in all shipped builds — DCE collapses this to an // unconditional throw. Only `bun --feature=ALLOW_TEST_VERSIONS` (the // smoke test's source-level invocation) bypasses. if (/^99\.99\./.test(normalized) && !feature('ALLOW_TEST_VERSIONS')) { throw new Error( `Version ${normalized} is not available for installation. Use 'stable' or 'latest'.`, ) } return normalized }
// ReleaseChannel validation const channel = channelOrVersion as ReleaseChannel if (channel !== 'stable' && channel !== 'latest') { throw new Error( `Invalid channel: ${channelOrVersion}. Use 'stable' or 'latest'`, ) }
// Route to appropriate source if (process.env.USER_TYPE === 'ant') { // Use Artifactory for ant users const npmTag = channel === 'stable' ? 'stable' : 'latest' return getLatestVersionFromArtifactory(npmTag) }
// Use GCS for external users return getLatestVersionFromBinaryRepo(channel, GCS_BUCKET_URL)}
export async function downloadVersionFromArtifactory( version: string, stagingPath: string,) { const fs = getFsImplementation()
// If we get here, we own the lock and can delete a partial download await fs.rm(stagingPath, { recursive: true, force: true })
// Get the platform-specific package name const platform = getPlatform() const platformPackageName = `${MACRO.NATIVE_PACKAGE_URL}-${platform}`
// Fetch integrity hash for the platform-specific package logForDebugging( `Fetching integrity hash for ${platformPackageName}@${version}`, ) const { stdout: integrityOutput, code, stderr, } = await execFileNoThrowWithCwd( 'npm', [ 'view', `${platformPackageName}@${version}`, 'dist.integrity', '--registry', ARTIFACTORY_REGISTRY_URL, ], { timeout: 30000, preserveOutputOnError: true, }, )
if (code !== 0) { throw new Error(`npm view integrity failed with code ${code}: ${stderr}`) }
const integrity = integrityOutput.trim() if (!integrity) { throw new Error( `Failed to fetch integrity hash for ${platformPackageName}@${version}`, ) }
logForDebugging(`Got integrity hash for ${platform}: ${integrity}`)
// Create isolated npm project in staging await fs.mkdir(stagingPath)
const packageJson = { name: 'claude-native-installer', version: '0.0.1', dependencies: { [MACRO.NATIVE_PACKAGE_URL!]: version, }, }
// Create package-lock.json with integrity verification for platform-specific package const packageLock = { name: 'claude-native-installer', version: '0.0.1', lockfileVersion: 3, requires: true, packages: { '': { name: 'claude-native-installer', version: '0.0.1', dependencies: { [MACRO.NATIVE_PACKAGE_URL!]: version, }, }, [`node_modules/${MACRO.NATIVE_PACKAGE_URL}`]: { version: version, optionalDependencies: { [platformPackageName]: version, }, }, [`node_modules/${platformPackageName}`]: { version: version, integrity: integrity, }, }, }
writeFileSync_DEPRECATED( join(stagingPath, 'package.json'), jsonStringify(packageJson, null, 2), { encoding: 'utf8', flush: true }, )
writeFileSync_DEPRECATED( join(stagingPath, 'package-lock.json'), jsonStringify(packageLock, null, 2), { encoding: 'utf8', flush: true }, )
// Install with npm - it will verify integrity from package-lock.json // Use --prefer-online to force fresh metadata checks, helping with Artifactory replication delays const result = await execFileNoThrowWithCwd( 'npm', ['ci', '--prefer-online', '--registry', ARTIFACTORY_REGISTRY_URL], { timeout: 60000, preserveOutputOnError: true, cwd: stagingPath, }, )
if (result.code !== 0) { throw new Error(`npm ci failed with code ${result.code}: ${result.stderr}`) }
logForDebugging( `Successfully downloaded and verified ${MACRO.NATIVE_PACKAGE_URL}@${version}`, )}
// Stall timeout: abort if no bytes received for this durationconst DEFAULT_STALL_TIMEOUT_MS = 60000 // 60 secondsconst MAX_DOWNLOAD_RETRIES = 3
function getStallTimeoutMs(): number { return ( Number(process.env.CLAUDE_CODE_STALL_TIMEOUT_MS_FOR_TESTING) || DEFAULT_STALL_TIMEOUT_MS )}
class StallTimeoutError extends Error { constructor() { super('Download stalled: no data received for 60 seconds') this.name = 'StallTimeoutError' }}
/** * Common logic for downloading and verifying a binary. * Includes stall detection (aborts if no bytes for 60s) and retry logic. */async function downloadAndVerifyBinary( binaryUrl: string, expectedChecksum: string, binaryPath: string, requestConfig: Record<string, unknown> = {},) { let lastError: Error | undefined
for (let attempt = 1; attempt <= MAX_DOWNLOAD_RETRIES; attempt++) { const controller = new AbortController() let stallTimer: ReturnType<typeof setTimeout> | undefined
const clearStallTimer = () => { if (stallTimer) { clearTimeout(stallTimer) stallTimer = undefined } }
const resetStallTimer = () => { clearStallTimer() stallTimer = setTimeout(c => c.abort(), getStallTimeoutMs(), controller) }
try { // Start the stall timer before the request resetStallTimer()
const response = await axios.get(binaryUrl, { timeout: 5 * 60000, // 5 minute total timeout responseType: 'arraybuffer', signal: controller.signal, onDownloadProgress: () => { // Reset stall timer on each chunk of data received resetStallTimer() }, ...requestConfig, })
clearStallTimer()
// Verify checksum const hash = createHash('sha256') hash.update(response.data) const actualChecksum = hash.digest('hex')
if (actualChecksum !== expectedChecksum) { throw new Error( `Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`, ) }
// Write binary to disk await writeFile(binaryPath, Buffer.from(response.data)) await chmod(binaryPath, 0o755)
// Success - return early return } catch (error) { clearStallTimer()
// Check if this was a stall timeout (axios wraps abort signals in CanceledError) const isStallTimeout = axios.isCancel(error)
if (isStallTimeout) { lastError = new StallTimeoutError() } else { lastError = toError(error) }
// Only retry on stall timeouts if (isStallTimeout && attempt < MAX_DOWNLOAD_RETRIES) { logForDebugging( `Download stalled on attempt ${attempt}/${MAX_DOWNLOAD_RETRIES}, retrying...`, ) // Brief pause before retry to let network recover await sleep(1000) continue }
// Don't retry other errors (HTTP errors, checksum mismatches, etc.) throw lastError } }
// Should not reach here, but just in case throw lastError ?? new Error('Download failed after all retries')}
export async function downloadVersionFromBinaryRepo( version: string, stagingPath: string, baseUrl: string, authConfig?: { auth?: { username: string; password: string } headers?: Record<string, string> },) { const fs = getFsImplementation()
// If we get here, we own the lock and can delete a partial download await fs.rm(stagingPath, { recursive: true, force: true })
// Get platform const platform = getPlatform() const startTime = Date.now()
// Log download attempt start logEvent('tengu_binary_download_attempt', {})
// Fetch manifest to get checksum let manifest try { const manifestResponse = await axios.get( `${baseUrl}/${version}/manifest.json`, { timeout: 10000, responseType: 'json', ...authConfig, }, ) manifest = manifestResponse.data } catch (error) { const latencyMs = Date.now() - startTime const errorMessage = error instanceof Error ? error.message : String(error) let httpStatus: number | undefined if (axios.isAxiosError(error) && error.response) { httpStatus = error.response.status }
logEvent('tengu_binary_manifest_fetch_failure', { latency_ms: latencyMs, http_status: httpStatus, is_timeout: errorMessage.includes('timeout'), }) logError( new Error( `Failed to fetch manifest from ${baseUrl}/${version}/manifest.json: ${errorMessage}`, ), ) throw error }
const platformInfo = manifest.platforms[platform]
if (!platformInfo) { logEvent('tengu_binary_platform_not_found', {}) throw new Error( `Platform ${platform} not found in manifest for version ${version}`, ) }
const expectedChecksum = platformInfo.checksum
// Both GCS and generic bucket use identical layout: ${baseUrl}/${version}/${platform}/${binaryName} const binaryName = getBinaryName(platform) const binaryUrl = `${baseUrl}/${version}/${platform}/${binaryName}`
// Write to staging await fs.mkdir(stagingPath) const binaryPath = join(stagingPath, binaryName)
try { await downloadAndVerifyBinary( binaryUrl, expectedChecksum, binaryPath, authConfig || {}, ) const latencyMs = Date.now() - startTime logEvent('tengu_binary_download_success', { latency_ms: latencyMs, }) } catch (error) { const latencyMs = Date.now() - startTime const errorMessage = error instanceof Error ? error.message : String(error) let httpStatus: number | undefined if (axios.isAxiosError(error) && error.response) { httpStatus = error.response.status }
logEvent('tengu_binary_download_failure', { latency_ms: latencyMs, http_status: httpStatus, is_timeout: errorMessage.includes('timeout'), is_checksum_mismatch: errorMessage.includes('Checksum mismatch'), }) logError( new Error(`Failed to download binary from ${binaryUrl}: ${errorMessage}`), ) throw error }}
export async function downloadVersion( version: string, stagingPath: string,): Promise<'npm' | 'binary'> { // Test-fixture versions route to the private sentinel bucket. DCE'd in all // shipped builds — the string 'claude-code-ci-sentinel' and the gcloud call // never exist in compiled binaries. Same gcloud-token pattern as // remoteSkillLoader.ts:175-195. if (feature('ALLOW_TEST_VERSIONS') && /^99\.99\./.test(version)) { const { stdout } = await execFileNoThrowWithCwd('gcloud', [ 'auth', 'print-access-token', ]) await downloadVersionFromBinaryRepo( version, stagingPath, 'https://storage.googleapis.com/claude-code-ci-sentinel', { headers: { Authorization: `Bearer ${stdout.trim()}` } }, ) return 'binary' }
if (process.env.USER_TYPE === 'ant') { // Use Artifactory for ant users await downloadVersionFromArtifactory(version, stagingPath) return 'npm' }
// Use GCS for external users await downloadVersionFromBinaryRepo(version, stagingPath, GCS_BUCKET_URL) return 'binary'}
// Exported for testingexport { StallTimeoutError, MAX_DOWNLOAD_RETRIES }export const STALL_TIMEOUT_MS = DEFAULT_STALL_TIMEOUT_MSexport const _downloadAndVerifyBinaryForTesting = downloadAndVerifyBinary