import * as fs from 'node:fs'; import * as path from 'node:path'; type JsonRecord = Record; export interface IMatrixPackageEnvironment { readonly name: string; readonly nats?: { readonly mode?: string; readonly port?: number; readonly wsPort?: number; readonly url?: string; readonly wsUrl?: string; readonly credentialsRef?: string; readonly dataDir?: string; readonly pidFile?: string; readonly binaryPath?: string; }; readonly runtime: { readonly root: string; readonly runtimeId?: string; readonly runtimeMount?: string; readonly controlMount?: string; }; readonly package?: { readonly publicRoot?: string; }; readonly http?: { readonly enabled?: boolean; readonly port?: number; }; readonly host?: { readonly matrixDir?: string; }; readonly topology?: Record; readonly authorityLease?: Record | null; } export interface ILoadedMatrixPackageEnvironment { readonly packageDir: string; readonly envName: string; readonly environmentPath: string; readonly environment: IMatrixPackageEnvironment; } export function loadPackageEnvironment(packageDir: string, envName: string): ILoadedMatrixPackageEnvironment { const resolvedPackageDir = path.resolve(packageDir); const cleanEnvName = envName.trim(); if (cleanEnvName.length === 0) { throw new Error('Package environment name must be a non-empty string'); } const environmentPath = path.join(resolvedPackageDir, '.matrix', `${cleanEnvName}.environment.json`); if (!fs.existsSync(environmentPath)) { throw new Error(`Package environment "${cleanEnvName}" not found at ${environmentPath}`); } return loadPackageEnvironmentFromPath(resolvedPackageDir, environmentPath, cleanEnvName); } export function loadPackageEnvironmentFromPath( packageDir: string, environmentPath: string, envName?: string, ): ILoadedMatrixPackageEnvironment { const resolvedPackageDir = path.resolve(packageDir); const resolvedEnvironmentPath = path.resolve(environmentPath); if (!fs.existsSync(resolvedEnvironmentPath)) { throw new Error(`Package environment file not found at ${resolvedEnvironmentPath}`); } const label = path.basename(resolvedEnvironmentPath); const parsed = readJsonObject(resolvedEnvironmentPath, label); const name = readRequiredString(parsed.name, `Package environment "${envName ?? label}" requires name`); const cleanEnvName = readOptionalString(envName) ?? name; const runtime = asRecord(parsed.runtime); const root = readRequiredString(runtime?.root, `Package environment "${cleanEnvName}" requires runtime.root`); const environment: IMatrixPackageEnvironment = { name, runtime: { root, ...(readOptionalString(runtime?.runtimeId) ? { runtimeId: readOptionalString(runtime?.runtimeId) } : {}), ...(readOptionalString(runtime?.runtimeMount) ? { runtimeMount: readOptionalString(runtime?.runtimeMount) } : {}), ...(readOptionalString(runtime?.controlMount) ? { controlMount: readOptionalString(runtime?.controlMount) } : {}), }, ...(asRecord(parsed.nats) ? { nats: asRecord(parsed.nats) as IMatrixPackageEnvironment['nats'] } : {}), ...(asRecord(parsed.package) ? { package: asRecord(parsed.package) as IMatrixPackageEnvironment['package'] } : {}), ...(asRecord(parsed.http) ? { http: asRecord(parsed.http) as IMatrixPackageEnvironment['http'] } : {}), ...(asRecord(parsed.host) ? { host: asRecord(parsed.host) as IMatrixPackageEnvironment['host'] } : {}), ...(asRecord(parsed.topology) ? { topology: asRecord(parsed.topology) } : {}), ...(parsed.authorityLease === null || asRecord(parsed.authorityLease) ? { authorityLease: parsed.authorityLease === null ? null : asRecord(parsed.authorityLease) } : {}), }; return { packageDir: resolvedPackageDir, envName: cleanEnvName, environmentPath: resolvedEnvironmentPath, environment, }; } function readJsonObject(filePath: string, label: string): JsonRecord { try { const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); if (!isRecord(parsed)) { throw new Error(`${label} must contain a JSON object`); } return parsed; } catch (err) { throw new Error(`Failed to parse ${label}: ${err instanceof Error ? err.message : String(err)}`); } } function readRequiredString(value: unknown, message: string): string { const resolved = readOptionalString(value); if (!resolved) { throw new Error(message); } return resolved; } function readOptionalString(value: unknown): string | undefined { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function isRecord(value: unknown): value is JsonRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); } function asRecord(value: unknown): JsonRecord | undefined { return isRecord(value) ? value : undefined; }