import * as os from "node:os" import { Context, Effect, Layer, Result } from "effect" import { globalConfigPath, projectConfigPath } from "../domain/paths.ts" import { ConfigError } from "../errors.ts" import { ROLE_MODE, type ApneaConfig, type Role, type RoleMode, } from "../domain/types.ts" import { applyProjectConfig, decodeGlobalConfig, decodeProjectConfig, resolveRoleCmdResult, validateRoleBindings, } from "../schema/config.ts" import { FileSystem } from "./file-system.ts" export interface ConfigService { readonly load: (root: string) => Effect.Effect readonly resolveRoleCmd: ( cfg: ApneaConfig, role: Role, mode?: RoleMode, ) => Effect.Effect } export class Config extends Context.Service()( "apnea/Config", ) {} function parseJson( text: string, filePath: string, ): Effect.Effect { return Effect.try({ try: () => JSON.parse(text) as unknown, catch: (e) => new ConfigError({ message: `invalid JSON at ${filePath}: ${e instanceof Error ? e.message : String(e)}`, path: filePath, }), }) } export const ConfigLive = Layer.effect( Config, Effect.gen(function* () { const fs = yield* FileSystem const load = (root: string): Effect.Effect => Effect.gen(function* () { const trustedHome = os.homedir() const gPath = globalConfigPath(trustedHome) const gPresent = yield* fs.exists(gPath) if (!gPresent) { return yield* new ConfigError({ message: `missing global config at ${gPath}. Run apnea-setup / create profiles there.`, path: gPath, }) } const gText = yield* fs.readTrustedGlobalFile(trustedHome, gPath) const gRaw = yield* parseJson(gText, gPath) const gDecoded = decodeGlobalConfig(gRaw) if (Result.isFailure(gDecoded)) { return yield* gDecoded.failure } let cfg = gDecoded.success const pPath = projectConfigPath(root) const pPresent = yield* fs.projectPathExists(root, pPath) if (pPresent) { const pText = yield* fs.readProjectFile(root, pPath) const pRaw = yield* parseJson(pText, pPath) const pDecoded = decodeProjectConfig(pRaw) if (Result.isFailure(pDecoded)) { return yield* pDecoded.failure } cfg = applyProjectConfig(cfg, pDecoded.success) } const validated = validateRoleBindings(cfg) if (Result.isFailure(validated)) { return yield* validated.failure } return validated.success }) const resolveRoleCmd = ( cfg: ApneaConfig, role: Role, mode: RoleMode = ROLE_MODE[role], ): Effect.Effect => Effect.gen(function* () { const resolved = resolveRoleCmdResult(cfg, role, mode) if (Result.isFailure(resolved)) { return yield* resolved.failure } return resolved.success }) return Config.of({ load, resolveRoleCmd }) }), )