import { spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { get } from "node:https"; import { homedir, platform, tmpdir } from "node:os"; import path from "node:path"; import { getElectrobunVersion, hutchIsInstalled, resolveHutchCommand, } from "./resolve-hutch-command.mjs"; const installerBaseUrl = "https://hutch.blackboard.sh/hutch"; const maxInstallerBytes = 1024 * 1024; function download(url, redirects = 0) { if (redirects > 5) { return Promise.reject(new Error("too many installer redirects")); } return new Promise((resolve, reject) => { const request = get(url, (response) => { if ( response.statusCode >= 300 && response.statusCode < 400 && response.headers.location ) { response.resume(); resolve( download(new URL(response.headers.location, url).href, redirects + 1), ); return; } if (response.statusCode !== 200) { response.resume(); reject( new Error(`installer download returned HTTP ${response.statusCode}`), ); return; } const chunks = []; let size = 0; response.on("data", (chunk) => { size += chunk.length; if (size > maxInstallerBytes) { request.destroy(new Error("installer download exceeded 1 MiB")); return; } chunks.push(chunk); }); response.on("end", () => resolve(Buffer.concat(chunks))); }); request.on("error", reject); }); } function checkedSpawn(command, args, options) { const result = spawnSync(command, args, options); if (result.error) { throw result.error; } if (result.status !== 0) { throw new Error(`${command} exited with status ${result.status ?? "unknown"}`); } } async function installHutch(channel) { const temporary = mkdtempSync(path.join(tmpdir(), "electrobun-hutch-")); try { if (platform() === "win32") { const installer = path.join(temporary, "install.ps1"); writeFileSync(installer, await download(`${installerBaseUrl}/install.ps1`)); checkedSpawn( "powershell.exe", [ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", installer, "-Channel", channel, ], { env: process.env, stdio: "inherit" }, ); } else { const installer = path.join(temporary, "install.sh"); writeFileSync(installer, await download(`${installerBaseUrl}/install.sh`), { mode: 0o700, }); checkedSpawn("sh", [installer, "--channel", channel, "--no-modify-path"], { env: process.env, stdio: "inherit", }); } } finally { rmSync(temporary, { force: true, recursive: true }); } } const command = resolveHutchCommand(); if (hutchIsInstalled(command)) { console.log(`ensure-hutch: ${command} already present`); process.exit(0); } const version = getElectrobunVersion(); const channel = version.includes("-") ? "canary" : "production"; if (process.env.ELECTROBUN_HUTCH_BINARY) { console.error(`ensure-hutch: ELECTROBUN_HUTCH_BINARY does not exist: ${command}`); process.exit(1); } console.error( `ensure-hutch: installing Hutch ${channel} for electrobun ${version}...`, ); await installHutch(channel); if (!existsSync(command)) { console.error(`ensure-hutch: Hutch was not installed at ${command}`); process.exit(1); } console.log(`ensure-hutch: ${command} ready`);