bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

bzl-instance-create.js (5017B)


      1 const fs = require("fs");
      2 const path = require("path");
      3 const crypto = require("crypto");
      4 const { spawnSync } = require("child_process");
      5 
      6 const ROOT = path.join(__dirname, "..");
      7 
      8 function log(msg) {
      9   console.log(`[instance-create] ${msg}`);
     10 }
     11 
     12 function fail(msg) {
     13   console.error(`[instance-create] ERROR: ${msg}`);
     14   process.exit(1);
     15 }
     16 
     17 function parseArgs() {
     18   const out = {};
     19   for (const raw of process.argv.slice(2)) {
     20     const a = String(raw || "");
     21     if (!a.startsWith("--")) continue;
     22     const eq = a.indexOf("=");
     23     if (eq > -1) out[a.slice(2, eq)] = a.slice(eq + 1);
     24     else out[a.slice(2)] = "1";
     25   }
     26   return out;
     27 }
     28 
     29 function expandHome(value) {
     30   const v = String(value || "").trim();
     31   if (!v.startsWith("~")) return v;
     32   const home = process.env.HOME || process.env.USERPROFILE || "";
     33   if (!home) return v;
     34   if (v === "~") return home;
     35   if (v.startsWith("~/") || v.startsWith("~\\")) return path.join(home, v.slice(2));
     36   return v;
     37 }
     38 
     39 function displayPath(filePath) {
     40   const abs = path.resolve(filePath);
     41   const rel = path.relative(process.cwd(), abs);
     42   if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) return rel.replace(/\\/g, "/");
     43   return abs.replace(/\\/g, "/");
     44 }
     45 
     46 function run(cmd, args, cwd, { dryRun = false } = {}) {
     47   const printable = `${cmd} ${args.join(" ")}`;
     48   if (dryRun) {
     49     log(`[dry-run] ${printable} (cwd=${displayPath(cwd)})`);
     50     return 0;
     51   }
     52   const result = spawnSync(cmd, args, { cwd, stdio: "inherit", shell: false });
     53   if (result.error) fail(`${printable} failed to start: ${result.error?.message || result.error}`);
     54   const code = result.status || 0;
     55   if (code !== 0) fail(`${printable} failed with exit code ${code}`);
     56   return code;
     57 }
     58 
     59 function randomCode(bytes = 18) {
     60   return crypto.randomBytes(bytes).toString("base64url");
     61 }
     62 
     63 function parseEnv(body) {
     64   const map = new Map();
     65   const lines = String(body || "").split(/\r?\n/);
     66   for (const line of lines) {
     67     const s = String(line || "");
     68     const m = s.match(/^([A-Z0-9_]+)=(.*)$/);
     69     if (!m) continue;
     70     map.set(m[1], m[2]);
     71   }
     72   return map;
     73 }
     74 
     75 function setEnvKey(body, key, value) {
     76   const lines = String(body || "").split(/\r?\n/);
     77   let replaced = false;
     78   const out = lines.map((line) => {
     79     const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
     80     if (!m) return line;
     81     if (m[1] !== key) return line;
     82     replaced = true;
     83     return `${key}=${value}`;
     84   });
     85   if (!replaced) out.push(`${key}=${value}`);
     86   return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
     87 }
     88 
     89 function main() {
     90   const args = parseArgs();
     91   const repo = String(args.repo || "https://github.com/bzlapp/Bzl.git").trim();
     92   const branch = String(args.branch || "main").trim();
     93   const targetPathRaw = String(args.path || "").trim();
     94   const hostname = String(args.hostname || "").trim().toLowerCase();
     95   const dryRun = args["dry-run"] === "1";
     96   const noStart = args["no-start"] === "1";
     97 
     98   if (!targetPathRaw) fail("Missing --path=/absolute/or/relative/path");
     99   const targetPath = path.resolve(expandHome(targetPathRaw));
    100 
    101   const port = Number(args.port || 0);
    102   if (!Number.isInteger(port) || port < 1024 || port > 65535) fail("--port must be an integer between 1024 and 65535.");
    103 
    104   const registrationCode = String(args["registration-code"] || randomCode()).trim();
    105   if (!registrationCode) fail("registration code resolved empty");
    106 
    107   const exists = fs.existsSync(targetPath);
    108   if (exists) {
    109     const list = fs.readdirSync(targetPath).filter((x) => x !== "." && x !== "..");
    110     if (list.length) fail(`Target path is not empty: ${displayPath(targetPath)}`);
    111   } else if (!dryRun) {
    112     fs.mkdirSync(targetPath, { recursive: true });
    113   }
    114 
    115   run("git", ["clone", "--branch", branch, repo, targetPath], ROOT, { dryRun });
    116 
    117   const envExample = path.join(targetPath, ".env.example");
    118   const envFile = path.join(targetPath, ".env");
    119 
    120   if (dryRun) {
    121     log(`[dry-run] would create/update ${displayPath(envFile)} with PORT=${port}, HOST=0.0.0.0, REGISTRATION_CODE=...`);
    122   } else {
    123     let base = "";
    124     if (fs.existsSync(envFile)) base = fs.readFileSync(envFile, "utf8");
    125     else if (fs.existsSync(envExample)) base = fs.readFileSync(envExample, "utf8");
    126     else base = "";
    127 
    128     let next = setEnvKey(base, "PORT", String(port));
    129     next = setEnvKey(next, "HOST", "0.0.0.0");
    130     next = setEnvKey(next, "REGISTRATION_CODE", registrationCode);
    131     fs.writeFileSync(envFile, next, "utf8");
    132     log(`Wrote ${displayPath(envFile)}`);
    133   }
    134 
    135   if (!noStart) {
    136     run("docker", ["compose", "-f", "compose.yaml", "up", "-d", "--build", "--remove-orphans"], targetPath, { dryRun });
    137   }
    138 
    139   console.log("");
    140   console.log("Instance created.");
    141   console.log(`- Path: ${displayPath(targetPath)}`);
    142   console.log(`- Port: ${port}`);
    143   console.log(`- Registration code: ${registrationCode}`);
    144   if (hostname) {
    145     console.log("");
    146     console.log("Caddy reminder:");
    147     console.log(`${hostname} {`);
    148     console.log(`  reverse_proxy 127.0.0.1:${port}`);
    149     console.log("}");
    150   }
    151 }
    152 
    153 main();