const https = require("https");
const fs = require("fs");
const os = require("os");
const path = require("path");
const crypto = require("crypto");
const { execFileSync } = require("child_process");
const VERSION = require("./package.json").version;
const REPO = "yazeed/proc";
const ALLOWED_HOSTS = [
"github.com",
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
"github-releases.githubusercontent.com",
];
const MAX_REDIRECTS = 5;
function getPlatformInfo() {
const platform = process.platform;
const arch = process.arch;
let os, archName, archiveExt;
switch (platform) {
case "darwin":
os = "darwin";
archiveExt = ".tar.gz";
break;
case "linux":
os = "linux";
archiveExt = ".tar.gz";
break;
case "win32":
os = "windows";
archiveExt = ".exe.zip";
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
switch (arch) {
case "x64":
archName = "x86_64";
break;
case "arm64":
archName = "aarch64";
break;
default:
throw new Error(`Unsupported architecture: ${arch}`);
}
const binaryName = `proc-${os}-${archName}${platform === "win32" ? ".exe" : ""}`;
const archiveName = `proc-${os}-${archName}${archiveExt}`;
return { os, archName, binaryName, archiveName, platform };
}
function binaryPath(dir) {
return path.join(dir, process.platform === "win32" ? "proc.exe" : "proc");
}
function cacheDir() {
const base =
process.platform === "win32"
? process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")
: process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
return path.join(base, "proc-cli", VERSION);
}
function download(url, redirectDepth = 0) {
return new Promise((resolve, reject) => {
if (redirectDepth > MAX_REDIRECTS) {
return reject(new Error("Too many redirects"));
}
const parsed = new URL(url);
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
return reject(
new Error(`Redirect to untrusted host: ${parsed.hostname}`)
);
}
https
.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
download(response.headers.location, redirectDepth + 1)
.then(resolve)
.catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => resolve(Buffer.concat(chunks)));
response.on("error", reject);
})
.on("error", reject);
});
}
function verifyChecksum(buffer, expectedHash, archiveName) {
const actualHash = crypto.createHash("sha256").update(buffer).digest("hex");
if (actualHash !== expectedHash) {
throw new Error(
`Checksum mismatch for ${archiveName}!\n` +
` Expected: ${expectedHash}\n` +
` Actual: ${actualHash}\n` +
` The download may be corrupted or tampered with.`
);
}
}
function parseChecksums(raw, archiveName) {
const lines = raw.toString().split("\n");
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2 && parts[1] === archiveName) {
return parts[0];
}
}
throw new Error(
`Checksum not found for ${archiveName} in checksums.txt`
);
}
async function install(binDir) {
const { binaryName, archiveName, platform } = getPlatformInfo();
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
const checksumsUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
const binPath = binaryPath(binDir);
console.error(`Downloading proc v${VERSION}...`);
console.error(` URL: ${url}`);
const [checksumsRaw, buffer] = await Promise.all([
download(checksumsUrl),
download(url),
]);
const expectedHash = parseChecksums(checksumsRaw, archiveName);
verifyChecksum(buffer, expectedHash, archiveName);
console.error(` Checksum verified: ${expectedHash.slice(0, 16)}...`);
fs.mkdirSync(binDir, { recursive: true });
const workDir = fs.mkdtempSync(path.join(binDir, ".proc-install-"));
try {
const tmpFile = path.join(workDir, archiveName);
fs.writeFileSync(tmpFile, buffer);
if (archiveName.endsWith(".tar.gz")) {
execFileSync("tar", ["-xzf", tmpFile, "-C", workDir], {
stdio: "pipe",
});
} else if (archiveName.endsWith(".zip")) {
execFileSync(
"powershell",
[
"-Command",
`Expand-Archive -Force -Path '${tmpFile.replace(/'/g, "''")}' -DestinationPath '${workDir.replace(/'/g, "''")}'`,
],
{ stdio: "pipe" }
);
}
const extractedBin = path.join(workDir, binaryName);
if (!fs.existsSync(extractedBin)) {
throw new Error(`${binaryName} not found in ${archiveName}`);
}
if (platform !== "win32") {
fs.chmodSync(extractedBin, 0o755);
}
fs.renameSync(extractedBin, binPath);
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
console.error(` Installed to: ${binPath}`);
return binPath;
}
if (require.main === module) {
install(__dirname)
.then(() => console.error("Done!"))
.catch((err) => {
console.error(`Could not download proc during install: ${err.message}`);
console.error("It will be downloaded the first time you run `proc`.");
});
}
module.exports = { install, binaryPath, cacheDir };