Download src/utils/nativeInstaller/download.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/nativeInstaller/download.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/nativeInstaller/download.ts
-
curl -L -o download.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/nativeInstaller/download.ts
15.2 kB
| /** | |
| * 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 duration | |
| const DEFAULT_STALL_TIMEOUT_MS = 60000 // 60 seconds | |
| const 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 testing | |
| export { StallTimeoutError, MAX_DOWNLOAD_RETRIES } | |
| export const STALL_TIMEOUT_MS = DEFAULT_STALL_TIMEOUT_MS | |
| export const _downloadAndVerifyBinaryForTesting = downloadAndVerifyBinary | |