feat(core): expand v2 config schema
This commit is contained in:
parent
4cd69df7c8
commit
c925ba2b68
13 changed files with 251 additions and 97 deletions
|
|
@ -3,18 +3,74 @@ export * as Config from "./config"
|
|||
import path from "path"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
import { Global } from "../global"
|
||||
import { Location } from "../location"
|
||||
import { Policy } from "../policy"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { ConfigV2 } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { Policy } from "./policy"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigPlugin } from "./config/plugin"
|
||||
import { ConfigProvider } from "./config/provider"
|
||||
import { ConfigReference } from "./config/reference"
|
||||
import { ConfigWatcher } from "./config/watcher"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
model: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default model to use when no session or agent model is selected",
|
||||
}),
|
||||
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]).pipe(Schema.optional).annotate({
|
||||
description: "Automatically update or notify when a new version is available",
|
||||
}),
|
||||
snapshots: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Enable snapshots used for undo and revert behavior",
|
||||
}),
|
||||
watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({
|
||||
description: "Filesystem watcher configuration",
|
||||
}),
|
||||
skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs to discover skills from",
|
||||
}),
|
||||
instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs supplying ambient instructions",
|
||||
}),
|
||||
references: ConfigReference.Info.pipe(Schema.optional).annotate({
|
||||
description: "Named local directories or Git repositories available as external context",
|
||||
}),
|
||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||
description: "Ordered external plugin packages to load",
|
||||
}),
|
||||
experimental: ConfigExperimental.Experimental.pipe(Schema.optional),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class FileSource extends Schema.Class<FileSource>("Config.FileSource")({
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class MemorySource extends Schema.Class<MemorySource>("Config.MemorySource")({
|
||||
type: Schema.Literal("memory"),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
export class Loaded extends Schema.Class<Loaded>("Config.Loaded")({
|
||||
source: Source,
|
||||
info: Info,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
/** Returns supplemental config directories from lowest to highest priority. */
|
||||
readonly directories: () => Effect.Effect<AbsolutePath[]>
|
||||
/** Loads location config files from lowest to highest priority. */
|
||||
readonly get: () => Effect.Effect<ConfigV2.Loaded[]>
|
||||
readonly get: () => Effect.Effect<Loaded[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {}
|
||||
|
|
@ -39,15 +95,15 @@ export const layer = Layer.effect(
|
|||
// Accept legacy fields while v2 is migrated incrementally; recognized
|
||||
// fields still have to satisfy the v2 schema.
|
||||
const info = Option.getOrUndefined(
|
||||
Schema.decodeUnknownOption(ConfigV2.Info)(input, { errors: "all", onExcessProperty: "ignore" }),
|
||||
Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }),
|
||||
)
|
||||
if (!info) return
|
||||
return new ConfigV2.Loaded({ source: new ConfigV2.FileSource({ type: "file", path: filepath }), info })
|
||||
return new Loaded({ source: new FileSource({ type: "file", path: filepath }), info })
|
||||
})
|
||||
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
Effect.map((configs) => configs.filter((config): config is ConfigV2.Loaded => config !== undefined)),
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -74,7 +130,7 @@ export const layer = Layer.effect(
|
|||
.pipe(Effect.orDie)).toReversed()
|
||||
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((configs) => configs.filter((config): config is ConfigV2.Loaded => config !== undefined)),
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
// Apply general settings first and more specific settings last:
|
||||
18
packages/core/src/config/experimental.ts
Normal file
18
packages/core/src/config/experimental.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export * as ConfigExperimental from "./experimental"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("Config.Experimental.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("Config.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
13
packages/core/src/config/plugin.ts
Normal file
13
packages/core/src/config/plugin.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export * as ConfigPlugin from "./plugin"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Entry extends Schema.Class<Entry>("Config.Plugin.Entry")({
|
||||
package: Schema.String,
|
||||
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Plugin = Schema.Union([Schema.String, Entry])
|
||||
export type Plugin = typeof Plugin.Type
|
||||
|
||||
export const Plugins = Plugin.pipe(Schema.Array)
|
||||
|
|
@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider"
|
|||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Config } from "./config"
|
||||
import { Config } from "../config"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { ModelV2 } from "../model"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
|
|
|||
17
packages/core/src/config/reference.ts
Normal file
17
packages/core/src/config/reference.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export * as ConfigReference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Git extends Schema.Class<Git>("Config.Reference.Git")({
|
||||
repository: Schema.String,
|
||||
branch: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Local extends Schema.Class<Local>("Config.Reference.Local")({
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const Entry = Schema.Union([Schema.String, Git, Local])
|
||||
export type Entry = typeof Entry.Type
|
||||
|
||||
export const Info = Schema.Record(Schema.String, Entry)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
export * as ConfigV2 from "./schema"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
import { ConfigProvider } from "./provider"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("ConfigV2.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("ConfigV2.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
experimental: Experimental.pipe(Schema.optional),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class FileSource extends Schema.Class<FileSource>("ConfigV2.FileSource")({
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class MemorySource extends Schema.Class<MemorySource>("ConfigV2.MemorySource")({
|
||||
type: Schema.Literal("memory"),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
export class Loaded extends Schema.Class<Loaded>("ConfigV2.Loaded")({
|
||||
source: Source,
|
||||
info: Info,
|
||||
}) {}
|
||||
7
packages/core/src/config/watcher.ts
Normal file
7
packages/core/src/config/watcher.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * as ConfigWatcher from "./watcher"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Watcher")({
|
||||
ignore: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
|
@ -3,7 +3,7 @@ import { Location } from "./location"
|
|||
import { Catalog } from "./catalog"
|
||||
import { PluginBoot } from "./plugin/boot"
|
||||
import { Policy } from "./policy"
|
||||
import { Config } from "./config/config"
|
||||
import { Config } from "./config"
|
||||
|
||||
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
|
||||
lookup: (ref: Location.Ref) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as PluginBoot from "./boot"
|
|||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { AccountV2 } from "../account"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Config } from "../config/config"
|
||||
import { Config } from "../config"
|
||||
import { ConfigProvider } from "../config/provider"
|
||||
import { EventV2 } from "../event"
|
||||
import { Npm } from "../npm"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue