import { existsSync, readFileSync } from "node:fs"; import { isAbsolute } from "node:path"; import { rootPath } from "./config"; export type TransSshBackendPreference = "local-backend-core-broker" | "remote-frontend-websocket"; export interface TransSshBackendConfig { backend: TransSshBackendPreference; raw: string; sourceRef: string; } const defaultTransConfigPath = ".env/trans.ymal"; export function readTransSshBackendConfig(env: NodeJS.ProcessEnv = process.env): TransSshBackendConfig | null { const configPath = resolveTransConfigPath(env); if (!existsSync(configPath.absolutePath)) return null; const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath.absolutePath, "utf8")) as unknown, configPath.sourceRef); const kind = optionalString(parsed, "kind", configPath.sourceRef); if (kind !== null && kind !== "UniDeskTransConfig") { throw new Error(`${configPath.sourceRef}.kind must be UniDeskTransConfig`); } const ssh = optionalRecord(parsed, "ssh", configPath.sourceRef); if (ssh === null) return null; const raw = optionalString(ssh, "backend", `${configPath.sourceRef}.ssh`); if (raw === null) return null; const backend = normalizeTransSshBackend(raw, `${configPath.sourceRef}.ssh.backend`); if (backend === "auto") return null; return { backend, raw, sourceRef: `${configPath.sourceRef}#ssh.backend`, }; } function resolveTransConfigPath(env: NodeJS.ProcessEnv): { absolutePath: string; sourceRef: string } { const configured = (env.UNIDESK_TRANS_CONFIG_PATH ?? env.UNIDESK_TRANS_YAML_PATH)?.trim(); if (configured !== undefined && configured.length > 0) { return { absolutePath: isAbsolute(configured) ? configured : rootPath(configured), sourceRef: configured, }; } return { absolutePath: rootPath(defaultTransConfigPath), sourceRef: defaultTransConfigPath, }; } function normalizeTransSshBackend(raw: string, path: string): TransSshBackendPreference | "auto" { const normalized = raw.trim(); if (normalized === "backend-core" || normalized === "local-backend-core-broker") return "local-backend-core-broker"; if (normalized === "frontend-websocket" || normalized === "remote-frontend-websocket") return "remote-frontend-websocket"; if (normalized === "auto") return "auto"; throw new Error(`${path} must be one of: backend-core, local-backend-core-broker, frontend-websocket, remote-frontend-websocket, auto`); } function asRecord(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function optionalRecord(obj: Record, key: string, path: string): Record | null { const value = obj[key]; if (value === undefined) return null; if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path}.${key} must be an object`); return value as Record; } function optionalString(obj: Record, key: string, path: string): string | null { const value = obj[key]; if (value === undefined) return null; if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; }