import { Config, Context, Effect, Layer } from "effect" type ConfigMap = Record> /** * The service shape inferred from an object of Effect `Config` definitions. */ export type Shape = { readonly [Key in keyof Fields]: Config.Success } /** * A Context service class with generated layers for config-backed services. */ export type ServiceClass = Context.ServiceClass & { /** Provide already-parsed config, useful in tests. */ readonly layer: (input: Service) => Layer.Layer /** Parse config once from the active Effect ConfigProvider and provide the service. */ readonly defaultLayer: Layer.Layer } /** * Create a Context service whose implementation is derived from Effect `Config`. * * This keeps Effect `Config` as the source of truth for env names, defaults, and * validation while generating a typed service plus convenient production/test * layers. * * ```ts * class ServerAuthConfig extends ConfigService.Service()( * "@opencode/ServerAuthConfig", * { * password: Config.string("OPENCODE_SERVER_PASSWORD").pipe(Config.option), * username: Config.string("OPENCODE_SERVER_USERNAME").pipe(Config.withDefault("opencode")), * }, * ) {} * * const live = ServerAuthConfig.defaultLayer * const test = ServerAuthConfig.layer({ password: Option.some("secret"), username: "kit" }) * ``` */ export const Service = () => (id: Id, fields: Fields) => { class ConfigTag extends Context.Service>()(id) { static layer(input: Shape) { return Layer.succeed(this, this.of(input)) } static get defaultLayer() { return Layer.effect( this, Config.all(fields) .asEffect() .pipe( // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Config.all preserves the field shape, but its conditional return type also supports iterable inputs. Effect.map((config) => this.of(config as Shape)), ), ) } } // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- The generated class carries typed static helpers. return ConfigTag as ServiceClass> } export * as ConfigService from "./config-service"