perf: use curl for binary download in postinstall

Replace Node.js https.get with curl for 15x faster downloads.
GitHub's redirect chain causes excessive TLS handshake overhead
in Node's http client.
This commit is contained in:
Lahfir 2026-02-23 01:25:22 -08:00
parent 8f66a9346e
commit ebafb71603

View file

@ -1,10 +1,9 @@
#!/usr/bin/env node
const { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, renameSync, writeFileSync, symlinkSync, lstatSync } = require('fs');
const { existsSync, mkdirSync, chmodSync, unlinkSync, renameSync, writeFileSync, symlinkSync, lstatSync } = require('fs');
const { readFileSync } = require('fs');
const { dirname, join } = require('path');
const { join } = require('path');
const { platform, arch } = require('os');
const { get } = require('https');
const { execSync } = require('child_process');
const { createHash } = require('crypto');
@ -14,8 +13,6 @@ const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), '
const version = packageJson.version;
const GITHUB_REPO = 'lahfir/agent-desktop';
const MAX_RETRIES = 3;
const TIMEOUT_MS = 60000;
const TARGET_MAP = {
'darwin-arm64': 'aarch64-apple-darwin',
@ -43,91 +40,32 @@ function getPlatformKey() {
return `${platform()}-${arch()}`;
}
function downloadWithRedirects(url, dest, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Download timed out after ${timeout}ms`));
}, timeout);
const doRequest = (requestUrl) => {
const parsedUrl = new URL(requestUrl);
const options = {
hostname: parsedUrl.hostname,
path: parsedUrl.pathname + parsedUrl.search,
headers: { 'User-Agent': `agent-desktop/${version}` },
};
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
if (proxy) {
log(`Using proxy: ${proxy}`);
}
get(requestUrl, options, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
doRequest(response.headers.location);
return;
}
if (response.statusCode !== 200) {
clearTimeout(timer);
reject(new Error(`HTTP ${response.statusCode} downloading ${requestUrl}`));
return;
}
const tmpDest = dest + '.tmp';
const file = createWriteStream(tmpDest);
response.pipe(file);
file.on('finish', () => {
file.close();
clearTimeout(timer);
try {
renameSync(tmpDest, dest);
resolve();
} catch (err) {
reject(err);
}
});
}).on('error', (err) => {
clearTimeout(timer);
reject(err);
});
};
doRequest(url);
});
}
async function downloadWithRetry(url, dest) {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
await downloadWithRedirects(url, dest, TIMEOUT_MS);
return;
} catch (err) {
if (attempt === MAX_RETRIES) throw err;
const delay = Math.pow(2, attempt) * 1000;
log(`Download failed (attempt ${attempt}/${MAX_RETRIES}): ${err.message}`);
log(`Retrying in ${delay / 1000}s...`);
await new Promise((r) => setTimeout(r, delay));
}
function download(url, dest) {
const tmpDest = dest + '.tmp';
try {
execSync(`curl -fsSL --retry 3 --retry-delay 2 -o "${tmpDest}" "${url}"`, {
stdio: 'pipe',
timeout: 60000,
});
renameSync(tmpDest, dest);
} catch (err) {
try { unlinkSync(tmpDest); } catch {}
throw new Error(`Failed to download ${url}: ${err.message}`);
}
}
function readFileContent(path) {
return readFileSync(path, 'utf8');
}
function verifyChecksum(filePath, expectedHash) {
const fileBuffer = readFileSync(filePath);
const hash = createHash('sha256').update(fileBuffer).digest('hex');
return hash === expectedHash;
}
async function fixGlobalInstallBin() {
function fixGlobalInstallBin() {
if (platform() === 'win32') return;
let npmBinDir;
try {
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
const prefix = execSync('npm prefix -g', { encoding: 'utf8', timeout: 5000 }).trim();
npmBinDir = join(prefix, 'bin');
} catch {
return;
@ -153,11 +91,10 @@ async function fixGlobalInstallBin() {
log('Optimized: symlink points to native binary (zero overhead)');
} catch (err) {
log(`Could not optimize symlink: ${err.message}`);
log('CLI will work via Node.js wrapper (slightly slower startup)');
}
}
async function main() {
function main() {
if (process.env.AGENT_DESKTOP_SKIP_DOWNLOAD === '1') {
log('Skipping binary download (AGENT_DESKTOP_SKIP_DOWNLOAD=1)');
return;
@ -167,20 +104,9 @@ async function main() {
const target = TARGET_MAP[platformKey];
const binaryName = BINARY_NAME_MAP[platformKey];
if (!target || !binaryName) {
if (!SUPPORTED_PLATFORMS.includes(platform())) {
log(`agent-desktop currently supports macOS only.`);
log(`Windows and Linux support is coming in Phase 2.`);
log(`See: https://github.com/${GITHUB_REPO}`);
return;
}
log(`Unsupported architecture: ${platformKey}`);
return;
}
if (!SUPPORTED_PLATFORMS.includes(platform())) {
log(`agent-desktop currently supports macOS only.`);
log(`${platform()} support is coming in Phase 2.`);
if (!SUPPORTED_PLATFORMS.includes(platform()) || !target || !binaryName) {
log('agent-desktop currently supports macOS only.');
log('Windows and Linux support is coming in Phase 2.');
log(`See: https://github.com/${GITHUB_REPO}`);
return;
}
@ -191,26 +117,21 @@ async function main() {
const customPath = process.env.AGENT_DESKTOP_BINARY_PATH;
if (existsSync(customPath)) {
try {
const content = readFileSync(customPath);
writeFileSync(binaryPath, content);
writeFileSync(binaryPath, readFileSync(customPath));
chmodSync(binaryPath, 0o755);
log(`Using binary from AGENT_DESKTOP_BINARY_PATH: ${customPath}`);
await fixGlobalInstallBin();
fixGlobalInstallBin();
return;
} catch (err) {
log(`Failed to copy from AGENT_DESKTOP_BINARY_PATH: ${err.message}`);
}
} else {
log(`AGENT_DESKTOP_BINARY_PATH not found: ${customPath}`);
}
}
if (existsSync(binaryPath)) {
if (platform() !== 'win32') {
chmodSync(binaryPath, 0o755);
}
chmodSync(binaryPath, 0o755);
log(`Native binary ready: ${binaryName}`);
await fixGlobalInstallBin();
fixGlobalInstallBin();
return;
}
@ -219,26 +140,25 @@ async function main() {
}
const tarball = `agent-desktop-v${version}-${target}.tar.gz`;
const tarballUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${tarball}`;
const checksumsUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/checksums.txt`;
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}`;
const tarballUrl = `${baseUrl}/${tarball}`;
const checksumsUrl = `${baseUrl}/checksums.txt`;
const tarballPath = join(binDir, tarball);
const checksumsPath = join(binDir, 'checksums.txt');
log(`Downloading native binary for ${platformKey}...`);
try {
await downloadWithRetry(tarballUrl, tarballPath);
log(`Downloaded: ${tarball}`);
download(tarballUrl, tarballPath);
try {
await downloadWithRetry(checksumsUrl, checksumsPath);
const checksums = readFileContent(checksumsPath);
download(checksumsUrl, checksumsPath);
const checksums = readFileSync(checksumsPath, 'utf8');
const expectedLine = checksums.split('\n').find((line) => line.includes(tarball));
if (expectedLine) {
const expectedHash = expectedLine.split(/\s+/)[0];
if (!verifyChecksum(tarballPath, expectedHash)) {
log('WARNING: Checksum verification failed. Binary may be corrupted.');
log('Try reinstalling: npm install -g agent-desktop');
log('WARNING: Checksum verification failed.');
unlinkSync(tarballPath);
unlinkSync(checksumsPath);
return;
@ -246,8 +166,8 @@ async function main() {
log('Checksum verified');
}
unlinkSync(checksumsPath);
} catch (err) {
log(`Could not verify checksum: ${err.message}`);
} catch {
log('Checksum verification skipped');
}
execSync(`tar -xzf "${tarballPath}" -C "${binDir}"`, { stdio: 'pipe' });
@ -257,32 +177,22 @@ async function main() {
renameSync(extractedBinary, binaryPath);
}
if (platform() !== 'win32') {
chmodSync(binaryPath, 0o755);
}
chmodSync(binaryPath, 0o755);
unlinkSync(tarballPath);
log(`Installed native binary: ${binaryName}`);
} catch (err) {
log(`Could not download native binary: ${err.message}`);
log('');
log('You can download manually from:');
log('Download manually from:');
log(` ${tarballUrl}`);
log('');
log(`Then place the binary at: ${binaryPath}`);
try {
if (existsSync(tarballPath)) unlinkSync(tarballPath);
if (existsSync(checksumsPath)) unlinkSync(checksumsPath);
} catch {}
log(`Then place at: ${binaryPath}`);
try { if (existsSync(tarballPath)) unlinkSync(tarballPath); } catch {}
try { if (existsSync(checksumsPath)) unlinkSync(checksumsPath); } catch {}
return;
}
await fixGlobalInstallBin();
fixGlobalInstallBin();
}
main().catch((err) => {
log(`Postinstall error: ${err.message}`);
process.exit(0);
});
main();