#!/usr/bin/env node // Onset's `on` command from npm. The first run fetches the release this // package's version names for this machine (from downloads.coline.dev, as // the install script does), checks it against the release's checksum, and // keeps it in this package's folder (uninstalling removes it), or in the // user's cache folder where this one can't be written (a global install // made with sudo). Every run starts that binary with the same arguments and // exits as it does. No install scripts, so it works where they're turned // off (pnpm, npm --ignore-scripts). "use strict"; const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const crypto = require("node:crypto"); const { spawn, execFileSync } = require("node:child_process"); const { version, platforms } = require("../manifest.json"); const exe = process.platform === "win32" ? "on.exe" : "on"; function platform() { const p = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform]; const a = { arm64: "arm64", x64: "x64" }[process.arch]; return p && a ? `${p}-${a}` : `${process.platform}-${process.arch}`; } function fail(message) { process.stderr.write(`on: ${message}\n`); process.exit(1); } async function fetchBinary(dest) { const name = platform(); const release = platforms[name]; if (!release) fail(`Onset ${version} has no build for ${name}; see https://upstream.codes/onset`); const base = process.env.ONSET_DOWNLOADS; const url = base ? `${base}/${version}/${path.basename(release.url)}` : release.url; process.stderr.write(`Fetching Onset ${version} for ${name}...\n`); let res; try { res = await fetch(url); } catch (e) { fail(`couldn't reach ${new URL(url).host} (${e.cause?.code ?? e.message}); nothing changed`); } if (!res.ok) fail(`${url} answered ${res.status}; nothing changed`); const archive = Buffer.from(await res.arrayBuffer()); const sha = crypto.createHash("sha256").update(archive).digest("hex"); if (sha !== release.sha256) fail(`the download doesn't match Onset ${version}'s checksum; nothing changed`); // Unpacked next to where it goes, then moved in whole, so two first runs at once each end with a whole binary. fs.mkdirSync(path.dirname(dest), { recursive: true }); const tmp = fs.mkdtempSync(path.join(path.dirname(dest), ".fetch-")); try { const file = path.join(tmp, path.basename(release.url)); fs.writeFileSync(file, archive); // tar is on macOS, Linux, and Windows 10 1803 and later (where it opens zips too). execFileSync("tar", [file.endsWith(".zip") ? "-xf" : "-xzf", file, "-C", tmp], { stdio: "ignore" }); fs.chmodSync(path.join(tmp, exe), 0o755); fs.renameSync(path.join(tmp, exe), dest); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } } function cache() { const home = os.homedir(); if (process.platform === "win32") return path.join(process.env.LOCALAPPDATA || home, "onset", "npm"); if (process.platform === "darwin") return path.join(home, "Library", "Caches", "onset", "npm"); return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "onset", "npm"); } function writable(dir) { try { fs.accessSync(dir, fs.constants.W_OK); return true; } catch { return false; } } async function main() { const here = path.join(__dirname, "..", "native", exe); const cached = path.join(cache(), version, exe); let bin = [here, cached].find((p) => fs.existsSync(p)); if (!bin) { bin = writable(path.join(__dirname, "..")) ? here : cached; await fetchBinary(bin); } const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" }); // Ctrl-C already reaches the whole process group; a signal sent to this process alone goes on. process.on("SIGINT", () => {}); for (const s of ["SIGTERM", "SIGHUP"]) process.on(s, () => child.kill(s)); child.on("error", (e) => fail(`couldn't start ${bin} (${e.code ?? e.message})`)); child.on("exit", (code, signal) => { if (signal) { process.removeAllListeners(signal); process.kill(process.pid, signal); } else process.exit(code ?? 1); }); } main();