#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const os = require("os"); const https = require("https"); const crypto = require("crypto"); const { execSync } = require("child_process"); const { BINARY_NAME, REPO_OWNER, REPO_NAME, GITHUB_RELEASES_BASE, BINARY_PATTERNS, } = require("./config"); // Read version from package.json with strict validation function getValidatedVersion() { try { const packageJson = JSON.parse( fs.readFileSync(path.join(__dirname, "package.json"), "utf8"), ); const version = packageJson.version; // Strict validation: must be valid semver (x.y.z) if (!/^\d+\.\d+\.\d+$/.test(version)) { throw new Error(`Invalid version format: ${version}`); } return `v${version}`; } catch (error) { throw new Error(`Failed to read valid version: ${error.message}`); } } const RELEASE_VERSION = getValidatedVersion(); const BASE_URL = `${GITHUB_RELEASES_BASE}/${RELEASE_VERSION}`; // Platform-specific binary URLs (constructed from config) const BINARY_URLS = {}; Object.keys(BINARY_PATTERNS).forEach((platform) => { BINARY_URLS[platform] = `${BASE_URL}/${BINARY_PATTERNS[platform]}`; }); const CHECKSUMS_URL = `${BASE_URL}/checksums.txt`; function getPlatformKey() { const platform = process.platform; const arch = process.arch; return `${platform}-${arch}`; } function downloadFile(url, dest, maxRedirects = 5) { return new Promise((resolve, reject) => { if (maxRedirects < 0) { reject(new Error("Too many redirects")); return; } const file = fs.createWriteStream(dest); https .get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { file.close(); fs.unlink(dest, () => {}); return downloadFile(response.headers.location, dest, maxRedirects - 1) .then(resolve) .catch(reject); } if (response.statusCode !== 200) { file.close(); fs.unlink(dest, () => {}); reject(new Error(`Download failed: ${response.statusCode}`)); return; } response.pipe(file); file.on("finish", () => { file.close(); resolve(); }); file.on("error", (err) => { fs.unlink(dest, () => {}); reject(err); }); }) .on("error", reject); }); } function calculateChecksum(filePath) { const fileBuffer = fs.readFileSync(filePath); const hashSum = crypto.createHash("sha256"); hashSum.update(fileBuffer); return hashSum.digest("hex"); } function validateChecksum(filePath, expectedChecksum) { const actualChecksum = calculateChecksum(filePath); return actualChecksum === expectedChecksum; } function extractArchive(archivePath, extractDir) { const isZip = archivePath.endsWith(".zip"); if (isZip) { if (process.platform === "win32") { execSync( `powershell -NoProfile -Command "Expand-Archive -Force -Path '${archivePath}' -DestinationPath '${extractDir}'"`, { stdio: "pipe" }, ); } else { execSync(`unzip -o "${archivePath}" -d "${extractDir}"`, { stdio: "pipe", }); } } else { execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, { stdio: "pipe" }); } } async function install() { let tempWorkspace; try { console.log("📦 Installing PMG binary..."); // Get platform-specific URL const platformKey = getPlatformKey(); const binaryUrl = BINARY_URLS[platformKey]; if (!binaryUrl) { throw new Error(`Unsupported platform: ${platformKey}`); } console.log(`🔍 Platform: ${platformKey}`); console.log(`📡 Version: ${RELEASE_VERSION}`); // Create directories const binDir = path.join(__dirname, "bin"); tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "pmg-install-")); fs.mkdirSync(binDir, { recursive: true }); // Download binary archive const archiveFilename = path.basename(binaryUrl); const archivePath = path.join(tempWorkspace, archiveFilename); console.log(`⬇️ Downloading binary...`); await downloadFile(binaryUrl, archivePath); // Download checksums const checksumsPath = path.join(tempWorkspace, "checksums.txt"); console.log(`⬇️ Downloading checksums...`); await downloadFile(CHECKSUMS_URL, checksumsPath); // Parse checksums file const checksumsContent = fs.readFileSync(checksumsPath, "utf8"); const checksumLines = checksumsContent.split("\n"); let expectedChecksum = null; for (const line of checksumLines) { if (line.includes(archiveFilename)) { expectedChecksum = line.split(/\s+/)[0]; break; } } if (!expectedChecksum) { throw new Error(`Checksum not found for ${archiveFilename}`); } // Validate checksum console.log(`🔐 Validating checksum...`); if (!validateChecksum(archivePath, expectedChecksum)) { throw new Error( "Checksum validation failed - binary may be corrupted or tampered", ); } console.log(`✅ Checksum validated`); // Extract archive console.log(`📂 Extracting binary...`); extractArchive(archivePath, tempWorkspace); // Find and move binary const binaryName = process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME; const extractedBinaryPath = path.join(tempWorkspace, binaryName); const finalBinaryPath = path.join(binDir, binaryName); if (!fs.existsSync(extractedBinaryPath)) { throw new Error( `Binary not found at expected location: ${extractedBinaryPath}`, ); } // Move binary to final location (handle cross-device links) try { fs.renameSync(extractedBinaryPath, finalBinaryPath); } catch (error) { if (error.code === "EXDEV") { // Cross-device link not permitted, copy and delete instead fs.copyFileSync(extractedBinaryPath, finalBinaryPath); fs.unlinkSync(extractedBinaryPath); } else { throw error; } } // Make executable on Unix systems if (process.platform !== "win32") { fs.chmodSync(finalBinaryPath, "755"); } // Clean up fs.rmSync(tempWorkspace, { recursive: true, force: true }); console.log("✅ PMG binary installed successfully!"); } catch (error) { console.error("❌ Installation failed:", error.message); // Clean up on failure try { if (tempWorkspace && fs.existsSync(tempWorkspace)) { fs.rmSync(tempWorkspace, { recursive: true, force: true }); } } catch (cleanupError) { console.warn("⚠️ Failed to clean up:", cleanupError.message); } process.exit(1); } } // Run installation install();