#!/usr/bin/env bun
* Download ripgrep and mcp-bridge binaries to .build-cache/.
*
* Run automatically via `bun install` (postinstall hook),
* or manually: `bun run script/postinstall.ts`
*
* Idempotent — skips download if the binary already exists in .build-cache.
*
* Environment:
* - HTTPS_PROXY / HTTP_PROXY — when set, download uses `undici` + EnvHttpProxyAgent.
* - RIPGREP_DOWNLOAD_BASE — override ripgrep release URL prefix (default: ghproxy.net mirror).
* - NPM_REGISTRY — override npm registry for mcp-bridge (default: https://registry.npmmirror.com).
*/
import fs from "fs"
import path from "path"
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "fs"
import { spawnSync } from "child_process"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
import pkg from "../package.json" with { type: "json" }
const RG_VERSION = "15.1.0"
const DEFAULT_RG_BASE = `https://ghproxy.net/https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}`
const RG_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RG_BASE).replace(/\/$/, "")
const cacheDir = path.join(dir, ".build-cache")
const rgCacheDir = path.join(cacheDir, "ripgrep")
const mcpCacheDir = path.join(cacheDir, "mcp-bridge")
const supportedPlatforms = new Set(["darwin-arm64", "darwin-x64", "win32-x64"])
const rgArchiveMap: Record<string, { archive: string; binary: string }> = {
"darwin-arm64": { archive: `ripgrep-${RG_VERSION}-aarch64-apple-darwin.tar.gz`, binary: "rg" },
"darwin-x64": { archive: `ripgrep-${RG_VERSION}-x86_64-apple-darwin.tar.gz`, binary: "rg" },
"win32-x64": { archive: `ripgrep-${RG_VERSION}-x86_64-pc-windows-msvc.zip`, binary: "rg.exe" },
}
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))
}
async function fetchWithProxy(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" })
}
async function downloadBuffer(url: string): Promise<Buffer> {
const response = await fetchWithProxy(url)
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
}
return Buffer.from(await response.arrayBuffer())
}
async function downloadRipgrep(platform: string) {
const info = rgArchiveMap[platform]
if (!info) return
const cachePath = path.join(rgCacheDir, platform, info.binary)
if (fs.existsSync(cachePath) && fs.statSync(cachePath).size > 0) {
return
}
const url = `${RG_BASE}/${info.archive}`
console.log(` Downloading rg for ${platform}...`)
const cacheSubDir = path.join(rgCacheDir, platform)
mkdirSync(cacheSubDir, { recursive: true })
const buffer = await downloadBuffer(url)
console.log(` Downloaded ${Math.round(buffer.length / 1024)} KB`)
if (info.archive.endsWith(".tar.gz")) {
const tmpDir = path.join(cacheSubDir, ".tmp-download")
fs.rmSync(tmpDir, { recursive: true, force: true })
mkdirSync(tmpDir, { recursive: true })
try {
const archivePath = path.join(tmpDir, info.archive)
writeFileSync(archivePath, buffer)
const result = spawnSync("tar", ["xzf", archivePath, "-C", tmpDir, "--strip-components=1"], { stdio: "pipe" })
if (result.status !== 0) {
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
}
renameSync(path.join(tmpDir, info.binary), cachePath)
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true })
}
} else {
const tmpDir = path.join(cacheSubDir, ".tmp-download")
fs.rmSync(tmpDir, { recursive: true, force: true })
mkdirSync(tmpDir, { recursive: true })
try {
const archivePath = path.join(tmpDir, info.archive)
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 findBinary = (d: string): string | null => {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const full = path.join(d, entry.name)
if (entry.isDirectory()) {
const found = findBinary(full)
if (found) return found
} else if (entry.name === info.binary) {
return full
}
}
return null
}
const binaryPath = findBinary(tmpDir)
if (!binaryPath) throw new Error(`Binary ${info.binary} not found in zip`)
renameSync(binaryPath, cachePath)
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true })
}
}
if (!platform.startsWith("win32")) {
chmodSync(cachePath, 0o755)
}
console.log(` Cached rg for ${platform}`)
}
async function downloadMcpBridge(platform: string) {
const os_ = platform.split("-")[0]
const arch = platform.split("-")[1]
const pkgName = `@deveco-codegenie/mcp-bridge-${os_}-${arch}`
const version = (pkg.dependencies as Record<string, string>)["@deveco-codegenie/mcp-bridge"]
const cacheSubDir = path.join(mcpCacheDir, platform)
const cachedNode = path.join(cacheSubDir, "napi_bridge.node")
const cachedPkgJson = path.join(cacheSubDir, "package.json")
if (fs.existsSync(cachedNode) && fs.existsSync(cachedPkgJson)) {
return
}
mkdirSync(cacheSubDir, { recursive: true })
const registry = (process.env.NPM_REGISTRY ?? "https://registry.npmmirror.com").replace(/\/$/, "")
const tarballUrl = `${registry}/${pkgName}/-/${pkgName.split("/")[1]}-${version}.tgz`
console.log(` Downloading mcp-bridge for ${platform}...`)
const buffer = await downloadBuffer(tarballUrl)
console.log(` Downloaded ${Math.round(buffer.length / 1024)} KB`)
const tmpDir = path.join(cacheSubDir, ".tmp-download")
fs.rmSync(tmpDir, { recursive: true, force: true })
mkdirSync(tmpDir, { recursive: true })
try {
const archivePath = path.join(tmpDir, "package.tgz")
writeFileSync(archivePath, buffer)
const result = spawnSync("tar", ["xzf", archivePath, "-C", tmpDir, "--strip-components=1"], { stdio: "pipe" })
if (result.status !== 0) {
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
}
const extractedNode = path.join(tmpDir, "napi_bridge.node")
const extractedPkgJson = path.join(tmpDir, "package.json")
if (fs.existsSync(extractedNode)) {
renameSync(extractedNode, cachedNode)
renameSync(extractedPkgJson, cachedPkgJson)
console.log(` Cached mcp-bridge for ${platform}`)
} else {
throw new Error("napi_bridge.node not found in tarball")
}
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true })
}
}
const platforms = Array.from(supportedPlatforms)
console.log(`Downloading vendored binaries... (${platforms.join(", ")})`)
for (const platform of platforms) {
await downloadRipgrep(platform).catch((e) => console.log(` Failed to download rg for ${platform}: ${e.message}`))
await downloadMcpBridge(platform).catch((e) => console.log(` Failed to download mcp-bridge for ${platform}: ${e.message}`))
}
console.log("Done.")