#!/usr/bin/env node /** * One-shot local bootstrap (any OS): * 1. Copy .env.example → .env if missing * 2. Fill empty TOKEN_SIGNING_SECRET / APP_ENCRYPTION_KEY (local only) * 3. docker compose up -d (Postgres) * 4. wait for healthy DB * 5. goose migrate + sqlc * * Usage: node scripts/setup.mjs * Then: npm install && npm run dev */ import { spawnSync } from "node:child_process"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { dotenvPath, parseEnvFile, readRootEnvFile, repoRoot, writeRootEnvFile, } from "./root-env.mjs"; function requireCmd(bin, hint, versionArgs = ["--version"]) { const r = spawnSync(bin, versionArgs, { encoding: "utf8", shell: process.platform === "win32", }); if (r.error || r.status !== 0) { console.error(`[setup] missing prerequisite: ${bin}`); if (hint) console.error(` ${hint}`); process.exit(1); } } function runNodeScript(rel) { const script = path.join(repoRoot, rel); const r = spawnSync(process.execPath, [script], { cwd: repoRoot, stdio: "inherit", env: process.env, }); if (r.status !== 0) process.exit(r.status ?? 1); } function runDockerCompose(args) { const r = spawnSync("docker", ["compose", ...args], { cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32", }); if (r.status !== 0) process.exit(r.status ?? 1); } function ensureEnvFile() { const dest = dotenvPath(); if (fs.existsSync(dest)) { console.log(`[setup] using existing ${path.relative(repoRoot, dest)}`); return; } const example = path.join(repoRoot, ".env.example"); if (!fs.existsSync(example)) { console.error("[setup] .env.example missing"); process.exit(1); } fs.copyFileSync(example, dest); console.log(`[setup] created ${path.relative(repoRoot, dest)} from .env.example`); } function fillEmptySecrets() { const text = readRootEnvFile(); if (text == null) return; const parsed = parseEnvFile(text); const need = []; for (const key of ["TOKEN_SIGNING_SECRET", "APP_ENCRYPTION_KEY"]) { if (!parsed[key] || !String(parsed[key]).trim()) need.push(key); } if (need.length === 0) return; let next = text; for (const key of need) { const secret = crypto.randomBytes(32).toString("hex"); const lineRe = new RegExp(`^(\\s*${key}\\s*=\\s*).*$`, "m"); if (lineRe.test(next)) { next = next.replace(lineRe, `$1${secret}`); } else { next = `${next.trimEnd()}\n${key}=${secret}\n`; } console.log(`[setup] generated local ${key} (not committed)`); } writeRootEnvFile(next); } console.log("==> Descrybe local setup"); requireCmd("node", "Install Node.js 20+ from https://nodejs.org/"); // Go uses `go version`, not `go --version`. requireCmd("go", "Install Go 1.25+ from https://go.dev/dl/", ["version"]); requireCmd("docker", "Install Docker Desktop / Docker Engine with Compose v2"); ensureEnvFile(); fillEmptySecrets(); console.log("==> docker compose up -d (Postgres on host :5433)"); runDockerCompose(["up", "-d"]); console.log("==> wait for Postgres"); runNodeScript("scripts/wait-postgres.mjs"); console.log("==> migrate"); runNodeScript("scripts/migrate.mjs"); console.log(""); console.log("Setup complete."); console.log("Next:"); console.log(" npm install"); console.log(" npm run dev # API :28471 + web :28472 + worker"); console.log(" npm run seed # optional demo login"); console.log(" npm run health # /healthz + /readyz (needs worker)"); console.log(""); console.log("Web: http://localhost:28472"); console.log("API: http://localhost:28471");