From 0fe18739501e37afefceca1b8d851633a040fe1e Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Wed, 21 Oct 2026 22:30:03 +0200 Subject: [PATCH] Added cloning and CLI downloading scripts. --- .gitignore | 1 + clone.mjs | 28 ++++++++++++++ config.json | 16 ++++++++ get-clis.mjs | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 .gitignore create mode 100644 clone.mjs create mode 100644 config.json create mode 100755 get-clis.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3de980a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +upstream/** diff --git a/clone.mjs b/clone.mjs new file mode 100644 index 0000000..04f5009 --- /dev/null +++ b/clone.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdir, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const org = "storytold"; +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const config = JSON.parse(await readFile(join(scriptDir, "config.json"), "utf8")); +const upstream = join(scriptDir, "upstream"); + +await mkdir(upstream, { recursive: true }); + +let failed = 0; +for (const app of config.repos) { + const dir = join(upstream, app); + const args = existsSync(dir) + ? ["-C", dir, "fetch", "origin", "--tags"] + : ["clone", `https://github.com/${org}/${app}`, dir]; + try { + execFileSync("git", args, { stdio: "inherit" }); + } catch { + failed++; + console.error(`${app}: git ${args[0] === "-C" ? "fetch" : "clone"} failed`); + } +} +process.exit(failed ? 1 : 0); diff --git a/config.json b/config.json new file mode 100644 index 0000000..98e7888 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "installDir": "__HOME__/.local/bin", + "clis": [ + "pdfcraft", + "wordcraft", + "gridcraft" + ], + "repos": [ + "gridcraft", + "pdfcraft", + "wordcraft", + "lightcraft", + "craft-launcher", + "cadcraft" + ] +} diff --git a/get-clis.mjs b/get-clis.mjs new file mode 100755 index 0000000..9b305c8 --- /dev/null +++ b/get-clis.mjs @@ -0,0 +1,106 @@ +#!/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);