const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const https = require("node:https");
const http = require("node:http");
const crypto = require("node:crypto");
const tar = require("tar");
const AdmZip = require("adm-zip");
const { version } = require("./package.json");
function getPlatformTriple() {
const type = os.type();
const arch = os.arch();
if (type === "Windows_NT") {
if (arch === "x64") return "x86_64-pc-windows-msvc";
if (arch === "ia32") throw new Error("32-bit Windows is not supported");
}
if (type === "Linux") {
if (arch === "x64") return "x86_64-unknown-linux-gnu";
if (arch === "arm64") return "aarch64-unknown-linux-gnu";
return "x86_64-unknown-linux-gnu";
}
if (type === "Darwin") {
if (arch === "x64") {
throw new Error(
"Intel macOS (x86_64) is not supported; basemind ships only Apple Silicon (arm64) macOS binaries",
);
}
return "aarch64-apple-darwin";
}
throw new Error(`Unsupported platform: ${type} ${arch}`);
}
function getReleaseAssets() {
const platform = getPlatformTriple();
const baseUrl = `https://github.com/Goldziher/basemind/releases/download/v${version}`;
const ext = platform.includes("windows") ? "zip" : "tar.gz";
const assetName = `basemind-${platform}.${ext}`;
return {
assetName,
archiveUrl: `${baseUrl}/${assetName}`,
checksumsUrl: `${baseUrl}/basemind_${version}_checksums.txt`,
};
}
function downloadWithRedirects(url, dest, maxRedirects = 5) {
return new Promise((resolve, reject) => {
if (maxRedirects <= 0) {
return reject(new Error("Too many redirects"));
}
const urlObj = new URL(url);
const client = urlObj.protocol === "https:" ? https : http;
const req = client.get(
url,
{
headers: {
"User-Agent": "basemind-npm-wrapper",
},
},
(res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return downloadWithRedirects(res.headers.location, dest, maxRedirects - 1)
.then(resolve)
.catch(reject);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
}
const file = fs.createWriteStream(dest);
res.pipe(file);
file.on("finish", () => {
file.close();
resolve();
});
file.on("error", (err) => {
fs.unlink(dest, () => {});
reject(err);
});
},
);
req.on("error", reject);
req.setTimeout(30000, () => {
req.destroy();
reject(new Error("Download timeout"));
});
});
}
function retryWithBackoff(fn, maxAttempts = 3) {
const delays = [1000, 2000, 4000]; return (async function attempt(index = 0) {
try {
return await fn();
} catch (err) {
const isRetryable =
err.message.includes("Download timeout") ||
err.message.includes("ECONNREFUSED") ||
err.message.includes("ECONNRESET") ||
err.message.includes("ETIMEDOUT") ||
err.message.includes("EHOSTUNREACH") ||
(err.message.match(/HTTP ([0-9]+)/) && parseInt(RegExp.$1) >= 500);
if (!isRetryable || index >= maxAttempts - 1) {
throw err;
}
const delay = delays[index];
console.log(
`Transient error (attempt ${index + 1}/${maxAttempts}): ${err.message}; retrying in ${delay}ms...`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
return attempt(index + 1);
}
})();
}
function fetchTextWithRedirects(url, maxRedirects = 5) {
return new Promise((resolve, reject) => {
if (maxRedirects <= 0) {
return reject(new Error("Too many redirects"));
}
const urlObj = new URL(url);
const client = urlObj.protocol === "https:" ? https : http;
const req = client.get(
url,
{
headers: {
"User-Agent": "basemind-npm-wrapper",
},
},
(res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetchTextWithRedirects(res.headers.location, maxRedirects - 1)
.then(resolve)
.catch(reject);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
}
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
res.on("error", reject);
},
);
req.on("error", reject);
req.setTimeout(30000, () => {
req.destroy();
reject(new Error("Download timeout"));
});
});
}
function retryFetchText(url) {
return retryWithBackoff(() => fetchTextWithRedirects(url));
}
function sha256File(filePath) {
const hash = crypto.createHash("sha256");
hash.update(fs.readFileSync(filePath));
return hash.digest("hex");
}
function expectedDigest(checksumsText, assetName) {
for (const line of checksumsText.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
const parts = trimmed.split(/\s+/);
if (parts.length < 2) continue;
const digest = parts[0];
const name = parts[parts.length - 1].replace(/^\*/, "");
if (name === assetName) return digest.toLowerCase();
}
return null;
}
async function verifyChecksum(archivePath, assetName, checksumsUrl) {
let checksumsText;
try {
checksumsText = await retryFetchText(checksumsUrl);
} catch (error) {
throw new Error(
`could not fetch checksums (${checksumsUrl}): ${error.message} — refusing to install unverified binary`,
);
}
const expected = expectedDigest(checksumsText, assetName);
if (!expected) {
throw new Error(
`no checksum entry for ${assetName} in ${checksumsUrl} — refusing to install unverified binary`,
);
}
const actual = sha256File(archivePath).toLowerCase();
if (actual !== expected) {
throw new Error(`checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`);
}
console.log("Checksum verified.");
}
async function installBinary() {
try {
const { assetName, archiveUrl, checksumsUrl } = getReleaseAssets();
const isZip = archiveUrl.endsWith(".zip");
const binDir = path.join(__dirname, "bin");
const archivePath = path.join(binDir, assetName);
const binaryName = os.type() === "Windows_NT" ? "basemind.exe" : "basemind";
const binaryPath = path.join(binDir, binaryName);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
if (fs.existsSync(binaryPath)) {
return;
}
console.log(`Downloading basemind binary from ${archiveUrl}...`);
await retryWithBackoff(() => downloadWithRedirects(archiveUrl, archivePath));
await verifyChecksum(archivePath, assetName, checksumsUrl);
console.log("Extracting archive (binary + bundled libraries)...");
if (isZip) {
const zip = new AdmZip(archivePath);
zip.extractAllTo(binDir, true);
} else {
await tar.extract({
file: archivePath,
cwd: binDir,
});
}
fs.unlinkSync(archivePath);
if (!fs.existsSync(binaryPath)) {
throw new Error(`binary ${binaryName} not found after extracting ${assetName}`);
}
if (os.type() !== "Windows_NT") {
fs.chmodSync(binaryPath, 0o755);
}
console.log("basemind binary installed successfully!");
} catch (error) {
console.error("Error installing basemind binary:", error.message);
process.exit(1);
}
}
installBinary();