import { readdir, access } from "node:fs/promises"; import path from "node:path"; import { composeStatus, type StackRunState } from "./docker"; import { readEnvFile, readEnvValue } from "./env-file"; import { RESERVED_SLUGS } from "./slug"; export { isValidSlug, suggestSlug } from "./slug"; export interface BusinessStackSummary { slug: string; /** BUSINESS_NAME from the business's own .env — null for a business provisioned before this field existed, until it's next edited. */ name: string | null; domain: string | null; databaseName: string | null; containerName: string; status: StackRunState; } export function stacksRoot(): string { const dir = process.env.STACKS_DIR; if (!dir) throw new Error("STACKS_DIR must be set."); return dir; } export function stackDir(slug: string): string { return path.join(stacksRoot(), slug); } export async function listBusinessStacks(): Promise { const root = stacksRoot(); let entries: string[]; try { entries = await readdir(root); } catch { return []; } const stacks: BusinessStackSummary[] = []; for (const slug of entries) { if (RESERVED_SLUGS.has(slug)) continue; const dir = path.join(root, slug); const composePath = path.join(dir, "compose.yaml"); try { await access(composePath); } catch { continue; } // Only ever reads specific known-safe fields — never the rest of a // business's .env (database password, AUTH_SECRET, etc.) — see // env-file.ts's readEnvValue. const envContent = await readEnvFile(path.join(dir, ".env")); const name = envContent ? readEnvValue(envContent, "BUSINESS_NAME") : null; const domain = envContent ? readEnvValue(envContent, "DOMAIN") : null; const databaseName = envContent ? readEnvValue(envContent, "DATABASE_NAME") : null; const status = await composeStatus(dir); stacks.push({ slug, name, domain, databaseName, containerName: `next-chair-${slug}`, status }); } return stacks.sort((a, b) => a.slug.localeCompare(b.slug)); } export async function stackExists(slug: string): Promise { try { await access(path.join(stackDir(slug), "compose.yaml")); return true; } catch { return false; } }