|
|
@@ -25,6 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
|
|
|
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
|
|
import { InstanceRef } from "@/effect/instance-ref"
|
|
|
import { zod, ZodOverride } from "@/util/effect-zod"
|
|
|
+import { withStatics } from "@/util/schema"
|
|
|
import { ConfigAgent } from "./agent"
|
|
|
import { ConfigCommand } from "./command"
|
|
|
import { ConfigFormatter } from "./formatter"
|
|
|
@@ -91,7 +92,15 @@ const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
|
|
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
|
|
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
|
|
|
|
|
-export const InfoSchema = Schema.Struct({
|
|
|
+// The Effect Schema is the canonical source of truth. The `.zod` compatibility
|
|
|
+// surface is derived so existing Hono validators keep working without a parallel
|
|
|
+// Zod definition.
|
|
|
+//
|
|
|
+// The walker emits `z.object({...})` which is non-strict by default. Config
|
|
|
+// historically uses `.strict()` (additionalProperties: false in openapi.json),
|
|
|
+// so layer that on after derivation. Re-apply the Config ref afterward
|
|
|
+// since `.strict()` strips the walker's meta annotation.
|
|
|
+export const Info = Schema.Struct({
|
|
|
$schema: Schema.optional(Schema.String).annotate({
|
|
|
description: "JSON schema reference for configuration validation",
|
|
|
}),
|
|
|
@@ -235,6 +244,14 @@ export const InfoSchema = Schema.Struct({
|
|
|
}),
|
|
|
),
|
|
|
})
|
|
|
+ .annotate({ identifier: "Config" })
|
|
|
+ .pipe(
|
|
|
+ withStatics((s) => ({
|
|
|
+ zod: (zod(s) as unknown as z.ZodObject<any>)
|
|
|
+ .strict()
|
|
|
+ .meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof s>>>,
|
|
|
+ })),
|
|
|
+ )
|
|
|
|
|
|
// Schema.Struct produces readonly types by default, but the service code
|
|
|
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
|
|
@@ -256,15 +273,7 @@ type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
|
|
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
|
|
: T
|
|
|
|
|
|
-// The walker emits `z.object({...})` which is non-strict by default. Config
|
|
|
-// historically uses `.strict()` (additionalProperties: false in openapi.json),
|
|
|
-// so layer that on after derivation. Re-apply the Config ref afterward
|
|
|
-// since `.strict()` strips the walker's meta annotation.
|
|
|
-export const Info = (zod(InfoSchema) as unknown as z.ZodObject<any>)
|
|
|
- .strict()
|
|
|
- .meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof InfoSchema>>>
|
|
|
-
|
|
|
-export type Info = z.output<typeof Info> & {
|
|
|
+export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
|
|
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
|
|
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
|
|
plugin_origins?: ConfigPlugin.Origin[]
|
|
|
@@ -361,7 +370,7 @@ export const layer = Layer.effect(
|
|
|
),
|
|
|
)
|
|
|
const parsed = ConfigParse.jsonc(expanded, source)
|
|
|
- const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
|
|
|
+ const data = ConfigParse.schema(Info.zod, normalizeLoadedConfig(parsed, source), source)
|
|
|
if (!("path" in options)) return data
|
|
|
|
|
|
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
|
|
|
@@ -753,13 +762,13 @@ export const layer = Layer.effect(
|
|
|
|
|
|
let next: Info
|
|
|
if (!file.endsWith(".jsonc")) {
|
|
|
- const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
|
|
|
+ const existing = ConfigParse.schema(Info.zod, ConfigParse.jsonc(before, file), file)
|
|
|
const merged = mergeDeep(writable(existing), writable(config))
|
|
|
yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie)
|
|
|
next = merged
|
|
|
} else {
|
|
|
const updated = patchJsonc(before, writable(config))
|
|
|
- next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
|
|
|
+ next = ConfigParse.schema(Info.zod, ConfigParse.jsonc(updated, file), file)
|
|
|
yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
|
|
|
}
|
|
|
|