Files
vibn-frontend/scripts/sync-db-url-from-coolify.mjs
Mark Henderson 651ddf1e11 Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out:
- Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim)
- Delete app/api/projects/[projectId]/workspace/route.ts and
  app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning)
- Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts
- Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/
  theiaError from app/api/projects/create/route.ts response
- Remove Theia callbackUrl branch in app/auth/page.tsx
- Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx
- Drop theiaWorkspaceUrl from deployment/page.tsx Project type
- Strip Theia IDE line + theia-code-os from advisor + agent-chat
  context strings
- Scrub Theia mention from lib/auth/workspace-auth.ts comment

P5.1 (custom apex domains + DNS):
- lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS
  XML auth, Cloud DNS plumbing
- scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS +
  prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup

In-progress (Justine onboarding/build, MVP setup, agent telemetry):
- New (justine)/stories, project (home) layouts, mvp-setup, run, tasks
  routes + supporting components
- Project shell + sidebar + nav refactor for the Stackless palette
- Agent session API hardening (sessions, events, stream, approve,
  retry, stop) + atlas-chat, advisor, design-surfaces refresh
- New scripts/sync-db-url-from-coolify.mjs +
  scripts/prisma-db-push.mjs + docker-compose.local-db.yml for
  local Prisma workflows
- lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts
- Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel

Made-with: Cursor
2026-04-22 18:05:01 -07:00

146 lines
4.2 KiB
JavaScript

#!/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);
});