Skip to content

Commit 8888794

Browse files
committed
Add npm postinstall and index.js
1 parent 3290521 commit 8888794

6 files changed

Lines changed: 395 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.env
2+
dist/
23
gemini-proxy
34
node_modules
45
*.wasm

npm/.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
gemini-code-assist-proxy
2+
gemini-code-assist-proxy.exe

npm/index.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env node
2+
const fs = require("fs");
3+
const path = require("path");
4+
const { spawn } = require("child_process");
5+
6+
const { startUpdateCheck } = require("./update_check");
7+
8+
const exe = process.platform === "win32" ? "gemini-code-assist-proxy.exe" : "gemini-code-assist-proxy";
9+
const binPath = path.join(__dirname, exe);
10+
11+
const PKG = (() => {
12+
try {
13+
return require("./package.json");
14+
} catch {
15+
return null;
16+
}
17+
})();
18+
19+
if (!fs.existsSync(binPath)) {
20+
console.error(`Binary not found: ${binPath}`);
21+
process.exit(1);
22+
}
23+
24+
if (process.platform !== "win32") {
25+
try {
26+
fs.chmodSync(binPath, 0o755);
27+
} catch {}
28+
}
29+
30+
const child = spawn(binPath, process.argv.slice(2), { stdio: "inherit", windowsHide: true });
31+
32+
// Best-effort update notice (cached + short timeout).
33+
// Aborts on command exit so it never delays shutdown.
34+
const updateAbort = new AbortController();
35+
startUpdateCheck({ installedVersion: PKG && PKG.version, signal: updateAbort.signal });
36+
37+
child.on("exit", (code) => {
38+
try {
39+
updateAbort.abort();
40+
} catch {}
41+
process.exitCode = code == null ? 1 : code;
42+
});
43+
child.on("error", (err) => {
44+
console.error(err.message);
45+
process.exitCode = 1;
46+
});
47+

npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dvcrn/gemini-code-assist-proxy",
3-
"version": "0.0.0",
3+
"version": "1.0.0",
44
"description": "Release package for gemini-code-assist-proxy",
55
"scripts": {
66
"postinstall": "node postinstall.js"

npm/postinstall.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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

Comments
 (0)