|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) |
| 3 | +// |
| 4 | +// run.js — Homoiconic, fault-tolerant, platform-independent run script |
| 5 | +// for Game Server Admin (GSA) |
| 6 | +// |
| 7 | +// Usage: |
| 8 | +// deno run --allow-all run.js # auto-detect and launch |
| 9 | +// deno run --allow-all run.js --help # show usage |
| 10 | +// deno run --allow-all run.js --reflect |
| 11 | + |
| 12 | +// ───────────────────────────────────────────────────────────────────────────── |
| 13 | +// REGISTRY — homoiconic data; the script reads this at runtime via reflect() |
| 14 | +// ───────────────────────────────────────────────────────────────────────────── |
| 15 | +const REGISTRY = { |
| 16 | + identity: { |
| 17 | + name: "game-server-admin", |
| 18 | + display: "Game Server Admin (GSA)", |
| 19 | + version: "0.3.0", |
| 20 | + license: "PMPL-1.0-or-later", |
| 21 | + repo: "https://github.com/hyperpolymath/game-server-admin", |
| 22 | + }, |
| 23 | + launchers: { |
| 24 | + bash: "play.sh", // existing homoiconic bash launcher |
| 25 | + just: "Justfile", |
| 26 | + }, |
| 27 | + binary: { |
| 28 | + zig: "src/interface/ffi/zig-out/bin/gsa", |
| 29 | + }, |
| 30 | + git: { |
| 31 | + remote: "origin", |
| 32 | + branch: "main", |
| 33 | + mirrors: [], |
| 34 | + }, |
| 35 | + capabilities: [ |
| 36 | + "reflect", // reads own source (homoiconic) |
| 37 | + "detectPlatform", // OS, arch, display server |
| 38 | + "checkGitSync", // fetch + ahead/behind + dirty check |
| 39 | + "launchBash", // delegates to play.sh (existing homoiconic launcher) |
| 40 | + "launchJust", // delegates to just run |
| 41 | + "launchBinary", // runs zig-out/bin/gsa directly |
| 42 | + "gitCycle", // add, commit, push, mirror |
| 43 | + ], |
| 44 | +}; |
| 45 | + |
| 46 | +// ───────────────────────────────────────────────────────────────────────────── |
| 47 | +// REFLECTION |
| 48 | +// ───────────────────────────────────────────────────────────────────────────── |
| 49 | +async function reflect() { |
| 50 | + const path = new URL(import.meta.url).pathname; |
| 51 | + const src = await Deno.readTextFile(path); |
| 52 | + return { path, lines: src.split("\n").length, capabilities: REGISTRY.capabilities }; |
| 53 | +} |
| 54 | + |
| 55 | +// ───────────────────────────────────────────────────────────────────────────── |
| 56 | +// PLATFORM DETECTION |
| 57 | +// ───────────────────────────────────────────────────────────────────────────── |
| 58 | +async function detectPlatform() { |
| 59 | + const os = Deno.build.os; |
| 60 | + const arch = Deno.build.arch; |
| 61 | + |
| 62 | + let display = "unknown"; |
| 63 | + if (os === "linux") { |
| 64 | + if (Deno.env.get("WAYLAND_DISPLAY")) display = "wayland"; |
| 65 | + else if (Deno.env.get("DISPLAY")) display = "x11"; |
| 66 | + else display = "headless"; |
| 67 | + } else if (os === "darwin") display = "quartz"; |
| 68 | + else if (os === "windows") display = "win32"; |
| 69 | + |
| 70 | + const has = async (cmd) => { |
| 71 | + try { |
| 72 | + const p = new Deno.Command("which", { args: [cmd], stdout: "null", stderr: "null" }); |
| 73 | + return (await p.output()).success; |
| 74 | + } catch { return false; } |
| 75 | + }; |
| 76 | + |
| 77 | + return { |
| 78 | + os, arch, display, |
| 79 | + hasBash: os !== "windows", |
| 80 | + hasJust: await has("just"), |
| 81 | + hasDeno: await has("deno"), |
| 82 | + }; |
| 83 | +} |
| 84 | + |
| 85 | +// ───────────────────────────────────────────────────────────────────────────── |
| 86 | +// SHARED HELPERS |
| 87 | +// ───────────────────────────────────────────────────────────────────────────── |
| 88 | +async function run(cmd, args) { |
| 89 | + try { |
| 90 | + const p = new Deno.Command(cmd, { args, stdout: "piped", stderr: "piped" }); |
| 91 | + const { code, stdout, stderr } = await p.output(); |
| 92 | + return { |
| 93 | + ok: code === 0, |
| 94 | + out: new TextDecoder().decode(stdout).trim(), |
| 95 | + err: new TextDecoder().decode(stderr).trim(), |
| 96 | + }; |
| 97 | + } catch (e) { |
| 98 | + return { ok: false, out: "", err: e.message }; |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +const c = { reset:"\x1b[0m", bold:"\x1b[1m", green:"\x1b[32m", yellow:"\x1b[33m", cyan:"\x1b[36m" }; |
| 103 | +const log = (m) => console.log(`${c.green}▶${c.reset} ${m}`); |
| 104 | +const warn = (m) => console.warn(`${c.yellow}⚠${c.reset} ${m}`); |
| 105 | +const head = (m) => console.log(`\n${c.bold}${c.cyan}${m}${c.reset}`); |
| 106 | + |
| 107 | +// ───────────────────────────────────────────────────────────────────────────── |
| 108 | +// GIT SYNC |
| 109 | +// ───────────────────────────────────────────────────────────────────────────── |
| 110 | +async function checkGitSync() { |
| 111 | + const sync = { dirty: false, ahead: 0, behind: 0, branch: "main", fetchError: false }; |
| 112 | + |
| 113 | + const branch = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"]); |
| 114 | + if (branch.ok) sync.branch = branch.out; |
| 115 | + |
| 116 | + const fetch = await run("git", ["fetch", "--quiet", REGISTRY.git.remote]); |
| 117 | + if (!fetch.ok) { sync.fetchError = true; warn("git fetch failed — offline"); } |
| 118 | + |
| 119 | + const rl = await run("git", ["rev-list", "--left-right", "--count", |
| 120 | + `${REGISTRY.git.remote}/${sync.branch}...HEAD`]); |
| 121 | + if (rl.ok) { |
| 122 | + const [b, a] = rl.out.split(/\s+/); |
| 123 | + sync.behind = parseInt(b, 10) || 0; |
| 124 | + sync.ahead = parseInt(a, 10) || 0; |
| 125 | + } |
| 126 | + |
| 127 | + const diff = await run("git", ["status", "--porcelain"]); |
| 128 | + if (diff.ok && diff.out) sync.dirty = true; |
| 129 | + |
| 130 | + const remotes = await run("git", ["remote", "-v"]); |
| 131 | + if (remotes.ok) { |
| 132 | + REGISTRY.git.mirrors = [...new Set( |
| 133 | + remotes.out.split("\n") |
| 134 | + .filter(l => l.includes("(push)") && !l.startsWith(REGISTRY.git.remote + "\t")) |
| 135 | + .map(l => l.split("\t")[0]) |
| 136 | + )]; |
| 137 | + } |
| 138 | + |
| 139 | + return sync; |
| 140 | +} |
| 141 | + |
| 142 | +// ───────────────────────────────────────────────────────────────────────────── |
| 143 | +// LAUNCH |
| 144 | +// ───────────────────────────────────────────────────────────────────────────── |
| 145 | +async function launchBash() { |
| 146 | + try { |
| 147 | + await Deno.stat(REGISTRY.launchers.bash); |
| 148 | + log(`Delegating to ${REGISTRY.launchers.bash} (homoiconic bash launcher)...`); |
| 149 | + const p = new Deno.Command("bash", { |
| 150 | + args: [REGISTRY.launchers.bash, "run"], |
| 151 | + stdin: "inherit", stdout: "inherit", stderr: "inherit", |
| 152 | + }); |
| 153 | + const child = p.spawn(); |
| 154 | + await child.status; |
| 155 | + return true; |
| 156 | + } catch { return false; } |
| 157 | +} |
| 158 | + |
| 159 | +async function launchJust(platform) { |
| 160 | + if (!platform.hasJust) return false; |
| 161 | + try { |
| 162 | + await Deno.stat(REGISTRY.launchers.just); |
| 163 | + log("Launching via: just run"); |
| 164 | + const p = new Deno.Command("just", { |
| 165 | + args: ["run"], stdin: "inherit", stdout: "inherit", stderr: "inherit", |
| 166 | + }); |
| 167 | + const child = p.spawn(); |
| 168 | + await child.status; |
| 169 | + return true; |
| 170 | + } catch { return false; } |
| 171 | +} |
| 172 | + |
| 173 | +async function launchBinary() { |
| 174 | + try { |
| 175 | + await Deno.stat(REGISTRY.binary.zig); |
| 176 | + log(`Running binary: ${REGISTRY.binary.zig}`); |
| 177 | + const p = new Deno.Command(REGISTRY.binary.zig, { |
| 178 | + args: ["status"], stdin: "inherit", stdout: "inherit", stderr: "inherit", |
| 179 | + }); |
| 180 | + const child = p.spawn(); |
| 181 | + await child.status; |
| 182 | + return true; |
| 183 | + } catch { return false; } |
| 184 | +} |
| 185 | + |
| 186 | +// ───────────────────────────────────────────────────────────────────────────── |
| 187 | +// GIT CYCLE |
| 188 | +// ───────────────────────────────────────────────────────────────────────────── |
| 189 | +async function gitCycle(sync) { |
| 190 | + head("── Git cycle ──"); |
| 191 | + |
| 192 | + const add = await run("git", ["add", "-A"]); |
| 193 | + if (!add.ok) { warn("git add failed: " + add.err); return; } |
| 194 | + |
| 195 | + const staged = await run("git", ["diff", "--cached", "--stat"]); |
| 196 | + if (staged.out) { |
| 197 | + const commit = await run("git", ["commit", "-m", |
| 198 | + "chore: run.js launch cycle — auto-detected platform, git cycle"]); |
| 199 | + if (commit.ok) log("Committed outstanding changes"); |
| 200 | + else warn("Commit failed: " + commit.err); |
| 201 | + } else { |
| 202 | + log("Nothing to commit"); |
| 203 | + } |
| 204 | + |
| 205 | + if (sync.ahead > 0 || staged.out) { |
| 206 | + const push = await run("git", ["push", REGISTRY.git.remote, sync.branch]); |
| 207 | + if (push.ok) log(`Pushed to ${REGISTRY.git.remote}/${sync.branch}`); |
| 208 | + else warn("Push failed: " + push.err); |
| 209 | + } |
| 210 | + |
| 211 | + for (const mirror of REGISTRY.git.mirrors) { |
| 212 | + const mp = await run("git", ["push", mirror, sync.branch]); |
| 213 | + if (mp.ok) log(`Pushed to mirror: ${mirror}`); |
| 214 | + else warn(`Mirror push failed (${mirror}): ${mp.err}`); |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +// ───────────────────────────────────────────────────────────────────────────── |
| 219 | +// MAIN |
| 220 | +// ───────────────────────────────────────────────────────────────────────────── |
| 221 | +if (import.meta.main) { |
| 222 | + const args = Deno.args; |
| 223 | + |
| 224 | + if (args.includes("--help") || args.includes("-h")) { |
| 225 | + console.log(` |
| 226 | +${c.bold}${REGISTRY.identity.display} — run.js${c.reset} |
| 227 | +${REGISTRY.identity.license} | ${REGISTRY.identity.repo} |
| 228 | +
|
| 229 | +Usage: deno run --allow-all run.js [OPTIONS] |
| 230 | +
|
| 231 | +Options: |
| 232 | + --help, -h Show this help |
| 233 | + --no-git Skip git sync check and post-launch git cycle |
| 234 | + --no-launch Git cycle only |
| 235 | + --reflect Print reflection data and exit |
| 236 | +`); |
| 237 | + Deno.exit(0); |
| 238 | + } |
| 239 | + |
| 240 | + if (args.includes("--reflect")) { |
| 241 | + const r = await reflect(); |
| 242 | + console.log(JSON.stringify({ registry: REGISTRY, reflection: r }, null, 2)); |
| 243 | + Deno.exit(0); |
| 244 | + } |
| 245 | + |
| 246 | + const skipGit = args.includes("--no-git"); |
| 247 | + const doLaunch = !args.includes("--no-launch"); |
| 248 | + |
| 249 | + head(`${REGISTRY.identity.display} v${REGISTRY.identity.version}`); |
| 250 | + |
| 251 | + const r = await reflect(); |
| 252 | + log(`Reflected: ${r.lines} lines, ${r.capabilities.length} capabilities`); |
| 253 | + |
| 254 | + head("Platform"); |
| 255 | + const platform = await detectPlatform(); |
| 256 | + log(`OS: ${platform.os}/${platform.arch} / display: ${platform.display}`); |
| 257 | + log(`just: ${platform.hasJust ? "available" : "not found"} | bash: ${platform.hasBash ? "yes" : "no"}`); |
| 258 | + |
| 259 | + let sync = { dirty: false, ahead: 0, behind: 0, branch: "main", fetchError: false }; |
| 260 | + if (!skipGit) { |
| 261 | + head("Git sync"); |
| 262 | + sync = await checkGitSync(); |
| 263 | + log(`Branch: ${sync.branch} | ahead: ${sync.ahead} | behind: ${sync.behind}`); |
| 264 | + if (sync.dirty) warn("Working tree has uncommitted changes"); |
| 265 | + if (sync.behind > 0) warn(`${sync.behind} commit(s) behind remote`); |
| 266 | + } |
| 267 | + |
| 268 | + if (doLaunch) { |
| 269 | + head("Launch"); |
| 270 | + let launched = false; |
| 271 | + // play.sh is the preferred launcher — it's already homoiconic bash |
| 272 | + launched = launched || await launchBash(); |
| 273 | + launched = launched || await launchJust(platform); |
| 274 | + launched = launched || await launchBinary(); |
| 275 | + if (!launched) warn("No launch method available — check build / install tools"); |
| 276 | + } |
| 277 | + |
| 278 | + if (!skipGit) await gitCycle(sync); |
| 279 | + |
| 280 | + head("Done"); |
| 281 | +} |
0 commit comments