mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix issues/114: bun install报错download-ripgrep MODULE_NOT_FOUND
This commit is contained in:
@@ -6,9 +6,16 @@
|
||||
*
|
||||
* Idempotent — skips download if the binary already exists.
|
||||
* Use --force to re-download.
|
||||
*
|
||||
* Environment:
|
||||
* - HTTPS_PROXY / HTTP_PROXY — when set, download uses `undici` + EnvHttpProxyAgent.
|
||||
* - RIPGREP_DOWNLOAD_BASE — override release URL prefix, e.g. mirror:
|
||||
* `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1`
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, statSync } from 'fs'
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from 'fs'
|
||||
import { setDefaultResultOrder } from 'node:dns'
|
||||
import { tmpdir } from 'os'
|
||||
import { chmodSync } from 'fs'
|
||||
import { spawnSync } from 'child_process'
|
||||
import * as path from 'path'
|
||||
@@ -17,8 +24,16 @@ import { fileURLToPath } from 'url'
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths.
|
||||
try {
|
||||
setDefaultResultOrder('ipv4first')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const RG_VERSION = '15.0.1'
|
||||
const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
|
||||
const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
|
||||
const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, '')
|
||||
|
||||
// --- Platform mapping ---
|
||||
|
||||
@@ -97,10 +112,94 @@ function getBinaryPath(): string {
|
||||
|
||||
// --- Download & extract ---
|
||||
|
||||
function proxyEnvSet(): boolean {
|
||||
const v = (s: string | undefined) => (s ?? '').trim()
|
||||
return !!(
|
||||
v(process.env.HTTPS_PROXY) ||
|
||||
v(process.env.HTTP_PROXY) ||
|
||||
v(process.env.ALL_PROXY) ||
|
||||
v(process.env.https_proxy) ||
|
||||
v(process.env.http_proxy)
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchRelease(url: string): Promise<Response> {
|
||||
if (proxyEnvSet()) {
|
||||
const { EnvHttpProxyAgent, fetch: undiciFetch } = await import('undici')
|
||||
return (await undiciFetch(url, {
|
||||
redirect: 'follow',
|
||||
dispatcher: new EnvHttpProxyAgent(),
|
||||
})) as unknown as Response
|
||||
}
|
||||
return await fetch(url, { redirect: 'follow' })
|
||||
}
|
||||
|
||||
function tryPowerShellDownload(url: string, dest: string): boolean {
|
||||
const u = url.replace(/'/g, "''")
|
||||
const d = dest.replace(/'/g, "''")
|
||||
const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing`
|
||||
const result = spawnSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd],
|
||||
{ stdio: 'pipe', windowsHide: true },
|
||||
)
|
||||
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
|
||||
}
|
||||
|
||||
function tryCurlDownload(url: string, dest: string): boolean {
|
||||
const curl = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
||||
const result = spawnSync(curl, ['-fsSL', '-L', '--fail', '-o', dest, url], {
|
||||
stdio: 'pipe',
|
||||
windowsHide: true,
|
||||
})
|
||||
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
|
||||
}
|
||||
|
||||
/** Bun `fetch` on Windows can fail while browser / WinINET still works — use subprocess fallbacks. */
|
||||
async function downloadUrlToBuffer(url: string): Promise<Buffer> {
|
||||
const response = await fetchRelease(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer())
|
||||
}
|
||||
|
||||
async function downloadUrlToBufferWithFallback(url: string): Promise<Buffer> {
|
||||
let firstError: unknown
|
||||
try {
|
||||
return await downloadUrlToBuffer(url)
|
||||
} catch (e) {
|
||||
firstError = e
|
||||
}
|
||||
|
||||
const tmpRoot = path.join(tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`)
|
||||
const tmpFile = path.join(tmpRoot, 'archive')
|
||||
mkdirSync(tmpRoot, { recursive: true })
|
||||
try {
|
||||
if (process.platform === 'win32' && tryPowerShellDownload(url, tmpFile)) {
|
||||
return readFileSync(tmpFile)
|
||||
}
|
||||
if (tryCurlDownload(url, tmpFile)) {
|
||||
return readFileSync(tmpFile)
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmpRoot, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
throw firstError
|
||||
}
|
||||
|
||||
function findZipEntryKey(files: Record<string, Uint8Array>, want: string): string | undefined {
|
||||
return Object.keys(files).find(k => {
|
||||
const norm = k.replace(/\\/g, '/')
|
||||
return norm === want || norm.endsWith(`/${want}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadAndExtract(): Promise<void> {
|
||||
const { target, ext } = getPlatformMapping()
|
||||
const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}`
|
||||
const downloadUrl = `${BASE_URL}/${assetName}`
|
||||
const downloadUrl = `${RELEASE_BASE}/${assetName}`
|
||||
|
||||
const binaryPath = getBinaryPath()
|
||||
const binaryDir = path.dirname(binaryPath)
|
||||
@@ -118,66 +217,80 @@ async function downloadAndExtract(): Promise<void> {
|
||||
console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`)
|
||||
console.log(`[ripgrep] URL: ${downloadUrl}`)
|
||||
|
||||
// Prepare temp directory
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
const { writeFileSync } = await import('fs')
|
||||
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
|
||||
// Download
|
||||
const response = await fetch(downloadUrl, { redirect: 'follow' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
const { writeFileSync } = await import('fs')
|
||||
writeFileSync(archivePath, buffer)
|
||||
const buffer = await downloadUrlToBufferWithFallback(downloadUrl)
|
||||
console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`)
|
||||
|
||||
// Extract
|
||||
mkdirSync(binaryDir, { recursive: true })
|
||||
|
||||
if (ext === 'tar.gz') {
|
||||
const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
writeFileSync(archivePath, buffer)
|
||||
const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
|
||||
}
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
renameSync(srcBinary, binaryPath)
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
} else {
|
||||
// .zip
|
||||
const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`unzip failed: ${result.stderr?.toString()}`)
|
||||
try {
|
||||
const { unzipSync } = await import('fflate')
|
||||
const unzipped = unzipSync(new Uint8Array(buffer))
|
||||
const key = findZipEntryKey(unzipped, extractedBinary)
|
||||
if (!key) {
|
||||
throw new Error(`Binary ${extractedBinary} not found in zip`)
|
||||
}
|
||||
writeFileSync(binaryPath, Buffer.from(unzipped[key]))
|
||||
} catch {
|
||||
// No fflate or bad archive — try `unzip` CLI (common on Unix / Git for Windows)
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
writeFileSync(archivePath, buffer)
|
||||
const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`unzip failed: ${result.stderr?.toString()}`)
|
||||
}
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
renameSync(srcBinary, binaryPath)
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the rg binary in the extracted directory
|
||||
// microsoft/ripgrep-prebuilt archives extract flat: ./rg (no subdirectory)
|
||||
const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
|
||||
// Move to final location
|
||||
renameSync(srcBinary, binaryPath)
|
||||
|
||||
// Make executable (non-Windows)
|
||||
if (process.platform !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
|
||||
console.log(`[ripgrep] Installed to ${binaryPath}`)
|
||||
} finally {
|
||||
// Cleanup temp directory
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
const hint =
|
||||
'Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header).'
|
||||
throw new Error(`${msg} ${hint}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user