#!/usr/bin/env node /** * Pull postgres external URL from Coolify API and write DATABASE_URL + POSTGRES_URL in .env.local. * * Loads (in order): ../.coolify.env, .env, .env.local (Coolify wins for COOLIFY_* from parent file). * * Identify DB by (first match): * COOLIFY_DATABASE_UUID — exact * COOLIFY_DATABASE_NAME — default: vibn-postgres * argv[2] — uuid or name substring * * Requires: COOLIFY_URL, COOLIFY_API_TOKEN */ import { config } from "dotenv"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = path.join(root, ".."); config({ path: path.join(repoRoot, ".coolify.env") }); config({ path: path.join(root, ".env") }); config({ path: path.join(root, ".env.local"), override: true }); const COOLIFY_URL = (process.env.COOLIFY_URL ?? "").replace(/\/$/, ""); const token = process.env.COOLIFY_API_TOKEN ?? ""; const arg = process.argv[2]; function fail(msg) { console.error(msg); process.exit(1); } if (!COOLIFY_URL || !token) { fail("Missing COOLIFY_URL or COOLIFY_API_TOKEN (e.g. set in master-ai/.coolify.env)."); } function normalizeDatabaseUrl(url) { if (!url || typeof url !== "string") return ""; return url.replace(/^postgres:\/\//i, "postgresql://"); } async function coolifyFetch(path) { const res = await fetch(`${COOLIFY_URL}/api/v1${path}`, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!res.ok) { const text = await res.text(); fail(`Coolify API ${res.status} ${path}: ${text.slice(0, 500)}`); } return res.json(); } function pickDatabase(list) { const uuid = process.env.COOLIFY_DATABASE_UUID?.trim(); const nameDefault = process.env.COOLIFY_DATABASE_NAME || "vibn-postgres"; if (uuid) { const d = list.find((x) => x.uuid === uuid); if (d) return d; fail(`No database with COOLIFY_DATABASE_UUID=${uuid}.`); } if (arg?.trim()) { const a = arg.trim().toLowerCase(); const byUuid = list.find((x) => x.uuid === arg.trim()); if (byUuid) return byUuid; const byName = list.find( (x) => String(x.name ?? "") .toLowerCase() .includes(a) || String(x.uuid ?? "") .toLowerCase() .includes(a) ); if (byName) return byName; fail(`No database matching "${arg}".`); } const byName = list.find( (x) => String(x.name ?? "").toLowerCase() === nameDefault.toLowerCase() ); if (byName) return byName; fail( `No database named "${nameDefault}". Set COOLIFY_DATABASE_UUID or pass uuid/name as argument.` ); } async function main() { const list = await coolifyFetch("/databases"); if (!Array.isArray(list) || list.length === 0) { fail("Coolify returned no databases."); } const picked = pickDatabase(list); const detail = await coolifyFetch(`/databases/${picked.uuid}`); const raw = detail.external_db_url; const databaseUrl = normalizeDatabaseUrl(raw); if (!databaseUrl) { fail( "No external_db_url from Coolify. Enable public exposure and set a host port on the Postgres service, then restart." ); } const envLocalPath = path.join(root, ".env.local"); if (!fs.existsSync(envLocalPath)) { fail(`Missing ${envLocalPath}; create it first (copy from .env.example).`); } let body = fs.readFileSync(envLocalPath, "utf8"); const lines = body.split(/\r?\n/); const setLine = (key, value) => { const next = `${key}=${value}`; let seen = false; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith(`${key}=`)) { lines[i] = next; seen = true; break; } } if (!seen) lines.push(next); }; setLine("DATABASE_URL", databaseUrl); setLine("POSTGRES_URL", databaseUrl); fs.writeFileSync(envLocalPath, lines.join("\n") + (body.endsWith("\n") ? "\n" : ""), "utf8"); console.log( `Updated DATABASE_URL and POSTGRES_URL in .env.local from Coolify (${picked.name}, ${picked.uuid}).` ); console.log(`Public port from API: ${detail.public_port ?? "(n/a)"}; is_public: ${detail.is_public ?? "(n/a)"}`); } main().catch((e) => { console.error(e); process.exit(1); });