|
| 1 | +#!/usr/bin/env node |
| 2 | +const fs = require("fs"); |
| 3 | +const path = require("path"); |
| 4 | +const os = require("os"); |
| 5 | +const https = require("https"); |
| 6 | +const crypto = require("crypto"); |
| 7 | +const { spawnSync } = require("child_process"); |
| 8 | + |
| 9 | +const OWNER = "dvcrn"; |
| 10 | +const REPO = "gemini-code-assist-proxy"; |
| 11 | +const BIN = "gemini-code-assist-proxy"; |
| 12 | +const VERSION_ENV = "GEMINI_CODE_ASSIST_PROXY_VERSION"; |
| 13 | +const BASE_URL_ENV = "GEMINI_CODE_ASSIST_PROXY_BASE_URL"; |
| 14 | +const ARCH_ENV = "GEMINI_CODE_ASSIST_PROXY_ARCH"; |
| 15 | +const PLATFORM_ENV = "GEMINI_CODE_ASSIST_PROXY_PLATFORM"; |
| 16 | + |
| 17 | +function httpGet(url, { headers } = {}) { |
| 18 | + return new Promise((resolve, reject) => { |
| 19 | + const req = https.get(url, { headers }, (res) => { |
| 20 | + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { |
| 21 | + resolve(httpGet(res.headers.location, { headers })); |
| 22 | + return; |
| 23 | + } |
| 24 | + if (res.statusCode !== 200) { |
| 25 | + reject(new Error(`GET ${url} -> ${res.statusCode}`)); |
| 26 | + return; |
| 27 | + } |
| 28 | + const chunks = []; |
| 29 | + res.on("data", (c) => chunks.push(c)); |
| 30 | + res.on("end", () => resolve(Buffer.concat(chunks))); |
| 31 | + }); |
| 32 | + req.on("error", reject); |
| 33 | + }); |
| 34 | +} |
| 35 | + |
| 36 | +function sha256(buf) { |
| 37 | + const h = crypto.createHash("sha256"); |
| 38 | + h.update(buf); |
| 39 | + return h.digest("hex"); |
| 40 | +} |
| 41 | + |
| 42 | +function parseChecksums(text) { |
| 43 | + const map = new Map(); |
| 44 | + const lines = text |
| 45 | + .split(/\r?\n/) |
| 46 | + .map((l) => l.trim()) |
| 47 | + .filter(Boolean); |
| 48 | + for (const line of lines) { |
| 49 | + let m = line.match(/^([a-f0-9]{64})\s+(.+)$/i); |
| 50 | + if (m) { |
| 51 | + map.set(m[2], m[1]); |
| 52 | + continue; |
| 53 | + } |
| 54 | + m = line.match(/^sha256:([a-f0-9]{64})\s+(.+)$/i); |
| 55 | + if (m) { |
| 56 | + map.set(m[2], m[1]); |
| 57 | + continue; |
| 58 | + } |
| 59 | + m = line.match(/^SHA256\s+\((.+)\)\s+=\s+([a-f0-9]{64})$/i); |
| 60 | + if (m) { |
| 61 | + map.set(m[1], m[2]); |
| 62 | + continue; |
| 63 | + } |
| 64 | + } |
| 65 | + return map; |
| 66 | +} |
| 67 | + |
| 68 | +(async function main() { |
| 69 | + try { |
| 70 | + const platformRaw = process.env[PLATFORM_ENV] || process.platform; |
| 71 | + const platform = platformRaw === "win32" ? "windows" : platformRaw; |
| 72 | + if (!["darwin", "linux", "windows"].includes(platform)) { |
| 73 | + console.error( |
| 74 | + "gemini-code-assist-proxy: npm install supports macOS (darwin), Linux, and Windows only" |
| 75 | + ); |
| 76 | + process.exit(1); |
| 77 | + } |
| 78 | + |
| 79 | + const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")); |
| 80 | + const version = process.env[VERSION_ENV] || pkg.version || ""; |
| 81 | + if (!version) { |
| 82 | + console.error("postinstall: could not determine version"); |
| 83 | + process.exit(1); |
| 84 | + } |
| 85 | + |
| 86 | + const detectedArch = |
| 87 | + process.arch === "x64" |
| 88 | + ? "amd64" |
| 89 | + : process.arch === "arm64" |
| 90 | + ? "arm64" |
| 91 | + : process.arch === "arm" |
| 92 | + ? "armv7" |
| 93 | + : process.arch; |
| 94 | + const arch = process.env[ARCH_ENV] || detectedArch; |
| 95 | + if (!["amd64", "arm64", "armv7"].includes(arch)) { |
| 96 | + console.error(`Unsupported arch: ${arch}`); |
| 97 | + process.exit(1); |
| 98 | + } |
| 99 | + if (platform === "windows" && arch === "armv7") { |
| 100 | + console.error(`Unsupported Windows arch: ${arch}`); |
| 101 | + process.exit(1); |
| 102 | + } |
| 103 | + |
| 104 | + const assetName = `${BIN}_${version}_${platform}_${arch}.tar.gz`; |
| 105 | + const baseOverride = process.env[BASE_URL_ENV]; |
| 106 | + const bases = baseOverride |
| 107 | + ? [baseOverride] |
| 108 | + : [ |
| 109 | + `https://github.com/${OWNER}/${REPO}/releases/download/${version}`, |
| 110 | + `https://github.com/${OWNER}/${REPO}/releases/download/v${version}`, |
| 111 | + ]; |
| 112 | + |
| 113 | + const headers = { "User-Agent": `${REPO}-postinstall` }; |
| 114 | + const outDir = __dirname; |
| 115 | + const exe = platform === "windows" ? `${BIN}.exe` : BIN; |
| 116 | + const binPath = path.join(outDir, exe); |
| 117 | + |
| 118 | + if (fs.existsSync(binPath)) { |
| 119 | + try { |
| 120 | + fs.chmodSync(binPath, 0o755); |
| 121 | + } catch {} |
| 122 | + return; |
| 123 | + } |
| 124 | + |
| 125 | + let tarGz = null; |
| 126 | + let baseUsed = ""; |
| 127 | + let lastErr = null; |
| 128 | + for (const base of bases) { |
| 129 | + const url = `${base}/${assetName}`; |
| 130 | + console.log(`postinstall: downloading ${assetName} from ${url}`); |
| 131 | + try { |
| 132 | + tarGz = await httpGet(url, { headers }); |
| 133 | + baseUsed = base; |
| 134 | + break; |
| 135 | + } catch (e) { |
| 136 | + lastErr = e; |
| 137 | + } |
| 138 | + } |
| 139 | + if (!tarGz) throw lastErr || new Error("failed to download binary"); |
| 140 | + |
| 141 | + // checksum (best effort) |
| 142 | + try { |
| 143 | + const checksumsUrl = `${baseUsed}/checksums.txt`; |
| 144 | + const checksumsBuf = await httpGet(checksumsUrl, { headers }); |
| 145 | + const checksums = parseChecksums(checksumsBuf.toString("utf8")); |
| 146 | + const sumExpected = checksums.get(assetName); |
| 147 | + if (!sumExpected) throw new Error("asset not in checksums.txt"); |
| 148 | + const sumActual = sha256(tarGz); |
| 149 | + if (sumActual.toLowerCase() !== sumExpected.toLowerCase()) throw new Error("checksum mismatch"); |
| 150 | + console.log("postinstall: checksum OK"); |
| 151 | + } catch (e) { |
| 152 | + console.warn(`postinstall: checksum skipped/failed: ${e.message}`); |
| 153 | + } |
| 154 | + |
| 155 | + // extract only the binary into npm directory (archive contains binary at root) |
| 156 | + const tmpFile = path.join(os.tmpdir(), `${REPO}-${Date.now()}.tar.gz`); |
| 157 | + fs.writeFileSync(tmpFile, tarGz); |
| 158 | + const tarRes = spawnSync("tar", ["-xzf", tmpFile, "-C", outDir, exe], { stdio: "inherit" }); |
| 159 | + if (tarRes.status !== 0) { |
| 160 | + console.error("postinstall: failed to extract binary"); |
| 161 | + process.exit(1); |
| 162 | + } |
| 163 | + try { |
| 164 | + fs.chmodSync(binPath, 0o755); |
| 165 | + } catch {} |
| 166 | + try { |
| 167 | + fs.unlinkSync(tmpFile); |
| 168 | + } catch {} |
| 169 | + console.log(`postinstall: installed ${exe} to ${outDir}`); |
| 170 | + } catch (err) { |
| 171 | + console.error(`postinstall error: ${err.message}`); |
| 172 | + process.exit(1); |
| 173 | + } |
| 174 | +})(); |
| 175 | + |
0 commit comments