Files
cfw-autumn/scripts/dw/helpers/env-files.ts
2026-05-19 18:01:48 +01:00

173 lines
5.6 KiB
TypeScript

import { existsSync, readFileSync, renameSync, writeFileSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
import { log } from "./shell.ts";
import { forceSslVerifyFull } from "./url.ts";
import { aliasesFor, dragonflyPortFor, elasticMqPortFor } from "./ports.ts";
import { PROJECT_ROOT, ENV_LOCAL_TARGETS, ENV_LOCAL_DISABLED_SUFFIX } from "../constants.ts";
import type { RegistryEntry } from "../types.ts";
// Simple KEY=VALUE parse (no quoting/multiline). Sufficient for .env.local
// files we own end-to-end; preserves blank lines and comments untouched.
export function parseEnvFile(contents: string): { keys: string[]; values: Record<string, string>; raw: string[] } {
const raw = contents.split(/\r?\n/);
const values: Record<string, string> = {};
const keys: string[] = [];
for (const line of raw) {
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (!m) continue;
const [, k, v] = m;
values[k] = v;
keys.push(k);
}
return { keys, values, raw };
}
export function mergeEnvFile(existing: string | null, managed: Record<string, string>): string {
if (!existing) {
return Object.entries(managed)
.map(([k, v]) => `${k}=${v}`)
.join("\n") + "\n";
}
const parsed = parseEnvFile(existing);
const managedKeys = new Set(Object.keys(managed));
const outLines: string[] = [];
const seen = new Set<string>();
for (const line of parsed.raw) {
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
if (m && managedKeys.has(m[1])) {
outLines.push(`${m[1]}=${managed[m[1]]}`);
seen.add(m[1]);
} else {
outLines.push(line);
}
}
for (const [k, v] of Object.entries(managed)) {
if (!seen.has(k)) outLines.push(`${k}=${v}`);
}
// Strip trailing empty lines, then re-add a single newline.
while (outLines.length > 0 && outLines[outLines.length - 1] === "") {
outLines.pop();
}
return outLines.join("\n") + "\n";
}
export function writeEnvLocalFiles(entry: RegistryEntry): void {
const { worktreeNum, databaseUrl } = entry;
if (!databaseUrl) {
log("writeEnvLocalFiles: entry missing databaseUrl, skipping");
return;
}
const aliases = aliasesFor(worktreeNum);
const serverPort = 8080 + (worktreeNum - 1) * 100;
const portlessCa = join(homedir(), ".portless", "ca.pem");
const dbUrl = forceSslVerifyFull(databaseUrl);
const serverEnv: Record<string, string> = {
DATABASE_URL: dbUrl,
DATABASE_CRITICAL_URL: dbUrl,
BETTER_AUTH_URL: aliases.apiUrl,
CLIENT_URL: aliases.viteUrl,
EMULATE_GOOGLE_URL: "https://google.emulate.localhost",
AUTUMN_TEST_BASE_URL: `http://localhost:${serverPort}`,
AUTUMN_TEST_VITE_URL: aliases.viteUrl,
STRIPE_WEBHOOK_SKIP_VERIFY: "true",
};
if (worktreeNum > 1) {
const dragonflyPort = dragonflyPortFor(worktreeNum);
const elasticMqPort = elasticMqPortFor(worktreeNum);
const redisUrl = `redis://localhost:${dragonflyPort}`;
serverEnv.REDIS_URL = redisUrl;
serverEnv.CACHE_URL = redisUrl;
serverEnv.CACHE_V2_DRAGONFLY_URL = redisUrl;
serverEnv.SQS_QUEUE_URL_V2 = `http://localhost:${elasticMqPort}/000000000000/autumn.fifo`;
serverEnv.TRACK_SQS_QUEUE_URL = `http://localhost:${elasticMqPort}/000000000000/autumn-track.fifo`;
}
if (existsSync(portlessCa)) {
serverEnv.NODE_EXTRA_CA_CERTS = portlessCa;
}
const viteEnv: Record<string, string> = {
VITE_BACKEND_URL: aliases.apiUrl,
VITE_FRONTEND_URL: aliases.viteUrl,
};
const checkoutEnv: Record<string, string> = {
VITE_BACKEND_URL: aliases.apiUrl,
};
const writeOne = (relPath: string, managed: Record<string, string>) => {
const abs = join(PROJECT_ROOT, relPath);
const dir = dirname(abs);
if (!existsSync(dir)) {
log(`writeEnvLocalFiles: skipping ${relPath} (dir ${dir} missing)`);
return;
}
const existing = existsSync(abs) ? readFileSync(abs, "utf-8") : null;
const merged = mergeEnvFile(existing, managed);
writeFileSync(abs, merged);
};
writeOne("server/.env.local", serverEnv);
writeOne("vite/.env.local", viteEnv);
writeOne("apps/checkout/.env.local", checkoutEnv);
log(`wrote .env.local for ${ENV_LOCAL_TARGETS.length} workspace(s)`);
}
export function removeEnvLocalFiles(): void {
for (const rel of ENV_LOCAL_TARGETS) {
const abs = join(PROJECT_ROOT, rel);
if (existsSync(abs)) {
rmSync(abs, { force: true });
log(`removed ${rel}`);
}
const disabled = `${abs}${ENV_LOCAL_DISABLED_SUFFIX}`;
if (existsSync(disabled)) {
rmSync(disabled, { force: true });
log(`removed ${rel}${ENV_LOCAL_DISABLED_SUFFIX}`);
}
}
}
export function disableEnvLocalFiles(): { moved: number; missing: number; alreadyDisabled: number } {
let moved = 0;
let missing = 0;
let alreadyDisabled = 0;
for (const rel of ENV_LOCAL_TARGETS) {
const abs = join(PROJECT_ROOT, rel);
const disabled = `${abs}${ENV_LOCAL_DISABLED_SUFFIX}`;
if (existsSync(abs)) {
if (existsSync(disabled)) rmSync(disabled, { force: true });
renameSync(abs, disabled);
log(`disabled ${rel} -> ${rel}${ENV_LOCAL_DISABLED_SUFFIX}`);
moved++;
} else if (existsSync(disabled)) {
alreadyDisabled++;
} else {
missing++;
}
}
return { moved, missing, alreadyDisabled };
}
export function enableEnvLocalFiles(): { moved: number; missing: number; alreadyEnabled: number } {
let moved = 0;
let missing = 0;
let alreadyEnabled = 0;
for (const rel of ENV_LOCAL_TARGETS) {
const abs = join(PROJECT_ROOT, rel);
const disabled = `${abs}${ENV_LOCAL_DISABLED_SUFFIX}`;
if (existsSync(disabled)) {
if (existsSync(abs)) rmSync(abs, { force: true });
renameSync(disabled, abs);
log(`enabled ${rel}`);
moved++;
} else if (existsSync(abs)) {
alreadyEnabled++;
} else {
missing++;
}
}
return { moved, missing, alreadyEnabled };
}