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();