import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; export interface UniDeskConfig { project: { name: string; timezone: string }; runtime: { typescript: "bun"; bunVersion: string }; network: { host: string; publicHost: string; core: { port: number; containerPort: number }; frontend: { port: number; containerPort: number }; database: { port: number; containerPort: number }; providerIngress: { port: number; containerPort: number }; }; auth: { username: string; password: string; sessionSecret: string; sessionTtlSeconds: number }; database: { user: string; password: string; name: string; volume: string; volumeSize: string }; providerGateway: { id: string; name: string; token: string; labels: Record; heartbeatIntervalMs: number; reconnectBaseMs: number; reconnectMaxMs: number; metrics: { diskPath: string }; upgrade: { hostProjectRoot: string; workspacePath: string; composeFile: string; composeEnvFile: string; composeProject: string; service: string; runnerImage: string; }; }; docker: { composeFile: string; projectName: string }; paths: { stateDir: string; logsDir: string; docsReferenceDir: string }; sshForwarding: { mode: string; keyDir: string; host: string; port: number; user: string }; } const moduleDir = dirname(fileURLToPath(import.meta.url)); export const repoRoot = join(moduleDir, "..", ".."); export function rootPath(...parts: string[]): string { return join(repoRoot, ...parts); } function asRecord(value: unknown, name: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error(`${name} must be an object`); } return value as Record; } function stringField(obj: Record, key: string, path: string): string { const value = obj[key]; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function numberField(obj: Record, key: string, path: string): number { const value = obj[key]; if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error(`${path}.${key} must be a positive number`); return value; } function portPair(obj: Record, key: string): { port: number; containerPort: number } { const value = asRecord(obj[key], `network.${key}`); return { port: numberField(value, "port", `network.${key}`), containerPort: numberField(value, "containerPort", `network.${key}`) }; } export function readConfig(): UniDeskConfig { const configPath = rootPath("config.json"); if (!existsSync(configPath)) throw new Error(`config.json not found at ${configPath}`); const raw = readFileSync(configPath, "utf8"); const parsed = asRecord(JSON.parse(raw) as unknown, "config.json"); const project = asRecord(parsed.project, "project"); const runtime = asRecord(parsed.runtime, "runtime"); const network = asRecord(parsed.network, "network"); const database = asRecord(parsed.database, "database"); const auth = asRecord(parsed.auth, "auth"); const providerGateway = asRecord(parsed.providerGateway, "providerGateway"); const docker = asRecord(parsed.docker, "docker"); const paths = asRecord(parsed.paths, "paths"); const sshForwarding = asRecord(parsed.sshForwarding, "sshForwarding"); const labels = asRecord(providerGateway.labels, "providerGateway.labels"); const providerMetrics = asRecord(providerGateway.metrics, "providerGateway.metrics"); const providerUpgrade = asRecord(providerGateway.upgrade, "providerGateway.upgrade"); const typescript = stringField(runtime, "typescript", "runtime"); if (typescript !== "bun") throw new Error("runtime.typescript must be bun"); return { project: { name: stringField(project, "name", "project"), timezone: stringField(project, "timezone", "project") }, runtime: { typescript, bunVersion: stringField(runtime, "bunVersion", "runtime") }, network: { host: stringField(network, "host", "network"), publicHost: stringField(network, "publicHost", "network"), core: portPair(network, "core"), frontend: portPair(network, "frontend"), database: portPair(network, "database"), providerIngress: portPair(network, "providerIngress"), }, database: { user: stringField(database, "user", "database"), password: stringField(database, "password", "database"), name: stringField(database, "name", "database"), volume: stringField(database, "volume", "database"), volumeSize: stringField(database, "volumeSize", "database"), }, providerGateway: { id: stringField(providerGateway, "id", "providerGateway"), name: stringField(providerGateway, "name", "providerGateway"), token: stringField(providerGateway, "token", "providerGateway"), labels, heartbeatIntervalMs: numberField(providerGateway, "heartbeatIntervalMs", "providerGateway"), reconnectBaseMs: numberField(providerGateway, "reconnectBaseMs", "providerGateway"), reconnectMaxMs: numberField(providerGateway, "reconnectMaxMs", "providerGateway"), metrics: { diskPath: stringField(providerMetrics, "diskPath", "providerGateway.metrics"), }, upgrade: { hostProjectRoot: stringField(providerUpgrade, "hostProjectRoot", "providerGateway.upgrade"), workspacePath: stringField(providerUpgrade, "workspacePath", "providerGateway.upgrade"), composeFile: stringField(providerUpgrade, "composeFile", "providerGateway.upgrade"), composeEnvFile: stringField(providerUpgrade, "composeEnvFile", "providerGateway.upgrade"), composeProject: stringField(providerUpgrade, "composeProject", "providerGateway.upgrade"), service: stringField(providerUpgrade, "service", "providerGateway.upgrade"), runnerImage: stringField(providerUpgrade, "runnerImage", "providerGateway.upgrade"), }, }, docker: { composeFile: stringField(docker, "composeFile", "docker"), projectName: stringField(docker, "projectName", "docker") }, paths: { stateDir: stringField(paths, "stateDir", "paths"), logsDir: stringField(paths, "logsDir", "paths"), docsReferenceDir: stringField(paths, "docsReferenceDir", "paths"), }, sshForwarding: { mode: stringField(sshForwarding, "mode", "sshForwarding"), keyDir: stringField(sshForwarding, "keyDir", "sshForwarding"), host: stringField(sshForwarding, "host", "sshForwarding"), port: numberField(sshForwarding, "port", "sshForwarding"), user: stringField(sshForwarding, "user", "sshForwarding"), }, auth: { username: stringField(auth, "username", "auth"), password: stringField(auth, "password", "auth"), sessionSecret: stringField(auth, "sessionSecret", "auth"), sessionTtlSeconds: numberField(auth, "sessionTtlSeconds", "auth"), }, }; }