import { Option, Result, Schema } from "effect" import * as Log from "@opencode-ai/core/util/log" const log = Log.create({ service: "config" }) const decodeJsonUnknown = Schema.decodeUnknownResult(Schema.UnknownFromJsonString) export class InvalidConfigError extends Schema.TaggedErrorClass()("InvalidConfigError", { source: Schema.String, value: Schema.String, reason: Schema.String, }) {} // Decode JSON config at a safety boundary. On malformed JSON or schema // mismatch, throws `InvalidConfigError`. On success, returns the typed value. // Use when defaulting silently is dangerous (e.g. permissions). export function requireJsonConfig>(raw: string, schema: S, source: string): S["Type"] { const decoded = decode(raw, schema) if (Result.isFailure(decoded)) { throw new InvalidConfigError({ source, value: raw, reason: decoded.failure }) } return decoded.success } // Decode JSON config at a UX-preference boundary. Returns `None` for absent, // empty, malformed, or schema-mismatched input, logging a warn so the user // can debug. Use when defaulting silently is benign (e.g. theme, route). export function tryJsonConfig>( raw: string | undefined, schema: S, source: string, ): Option.Option { if (!raw) return Option.none() const decoded = decode(raw, schema) if (Result.isFailure(decoded)) { log.warn(`ignoring invalid ${source}`, { value: raw, reason: decoded.failure }) return Option.none() } return Option.some(decoded.success) } function decode>(raw: string, schema: S): Result.Result { const decodeSchema = Schema.decodeUnknownResult(schema) const parsed = decodeJsonUnknown(raw) if (Result.isFailure(parsed)) return Result.fail(parsed.failure.toString()) const decoded = decodeSchema(parsed.success) if (Result.isFailure(decoded)) return Result.fail(decoded.failure.toString()) return Result.succeed(decoded.success) }