Files
storytold/get-clis.mjs
T

107 lines
3.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
const org = "storytold";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(await readFile(join(scriptDir, "config.json"), "utf8"));
const installDir = resolve(scriptDir, (config.installDir ?? "bin").replaceAll("__HOME__", homedir()));
// Use the token of the logged in gh, if any.
let token = null;
try {
token = execFileSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
} catch {}
console.log(token ? "Using gh token" : "No gh token available, using anonymous access");
const headers = (accept) => ({
Accept: accept,
"X-GitHub-Api-Version": "2022-11-28",
...(token && { Authorization: `Bearer ${token}` }),
});
const platforms = {
macos: /-macos-universal\.zip$/,
linux: /-linux-x86_64\.tar\.gz$/,
windows: /-windows-x64-portable\.zip$/,
};
const hostPlatform = { darwin: "macos", linux: "linux", win32: "windows" }[process.platform];
const { values: { os: targetOs = hostPlatform, "dry-run": dryRun } } = parseArgs({
options: { os: { type: "string" }, "dry-run": { type: "boolean" } },
});
if (!platforms[targetOs]) {
console.error(`Usage: get-clis.mjs [--os ${Object.keys(platforms).join("|")}] [--dry-run]`);
process.exit(2);
}
const matchesPlatform = platforms[targetOs];
const archiveRe = /\.(tar\.gz|tgz|tar|zip)$/i;
async function findFiles(dir) {
const out = [];
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...(await findFiles(p)));
else if (e.isFile()) out.push(p);
}
return out;
}
async function install(name) {
const res = await fetch(`https://api.github.com/repos/${org}/${name}/releases/latest`, {
headers: headers("application/vnd.github+json"),
});
if (!res.ok) throw new Error(`release lookup failed: ${res.status} ${res.statusText}`);
const release = await res.json();
const matching = release.assets.filter((a) => a.name.startsWith(`${name}-`) && matchesPlatform.test(a.name));
const asset = matching.find((a) => a.name.startsWith(`${name}-cli-`)) ?? matching[0];
if (!asset) throw new Error(`no ${targetOs} asset in release ${release.tag_name}`);
console.log(`${name}: ${release.tag_name} -> ${asset.name}`);
if (dryRun) {
console.log(`${name}: dry run, would download ${asset.size} bytes into ${installDir}`);
return;
}
const dl = await fetch(asset.url, { headers: headers("application/octet-stream") });
if (!dl.ok) throw new Error(`download failed: ${dl.status} ${dl.statusText}`);
const work = await mkdtemp(join(tmpdir(), `${name}-`));
try {
const file = join(work, asset.name);
await writeFile(file, Buffer.from(await dl.arrayBuffer()));
let binary = file;
if (archiveRe.test(asset.name)) {
const out = join(work, "extracted");
await mkdir(out);
execFileSync("tar", ["-xf", file, "-C", out]);
const files = await findFiles(out);
binary = files.find((f) => [`${name}-cli`, `${name}-cli.exe`].includes(basename(f)));
if (!binary) throw new Error(`no ${name}-cli executable in ${asset.name}`);
}
await mkdir(installDir, { recursive: true });
const target = join(installDir, basename(binary) === asset.name ? name : basename(binary));
await copyFile(binary, target);
await chmod(target, 0o755);
console.log(`${name}: installed ${target}`);
} finally {
await rm(work, { recursive: true, force: true });
}
}
let failed = 0;
for (const name of config.clis) {
try {
await install(name);
} catch (err) {
failed++;
console.error(`${name}: ${err.message}`);
}
}
process.exit(failed ? 1 : 0);