refactor(core): simplify plugin and config boundaries (#43930)

This commit is contained in:
Kit Langton
2026-08-21 13:48:17 -04:00
committed by GitHub
parent 8fec7e0e91
commit b58f29a4ef
7 changed files with 27 additions and 41 deletions
+6 -14
View File
@@ -17,6 +17,7 @@ import {
Event,
} from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
import { isRecord } from "@opencode-ai/ai/utils/record"
import { Credential } from "./credential.js"
import { Bus } from "./bus.js"
import { Watcher } from "./filesystem/watcher.js"
@@ -449,20 +450,11 @@ type Edit = { readonly path: (string | number)[]; readonly value: unknown }
function changes(before: unknown, after: unknown, path: (string | number)[] = []): Edit[] {
if (Object.is(before, after)) return []
if (
before !== null &&
after !== null &&
typeof before === "object" &&
typeof after === "object" &&
!Array.isArray(before) &&
!Array.isArray(after)
) {
const previous = before as Record<string, unknown>
const next = after as Record<string, unknown>
return [...new Set([...Object.keys(previous), ...Object.keys(next)])].flatMap((key) => {
if (!(key in next)) return [{ path: [...path, key], value: undefined }]
if (!(key in previous)) return [{ path: [...path, key], value: next[key] }]
return changes(previous[key], next[key], [...path, key])
if (isRecord(before) && isRecord(after)) {
return [...new Set([...Object.keys(before), ...Object.keys(after)])].flatMap((key) => {
if (!(key in after)) return [{ path: [...path, key], value: undefined }]
if (!(key in before)) return [{ path: [...path, key], value: after[key] }]
return changes(before[key], after[key], [...path, key])
})
}
return [{ path, value: after }]
+5 -5
View File
@@ -54,7 +54,7 @@ export const Plugin = define({
"ConfigSkillPlugin.watchDirectory",
)(function* (directory: string) {
const target = path.resolve(directory)
const resolved = yield* fs.realPath(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
const resolved = yield* fs.realPath(directory).pipe(Effect.orElseSucceed(() => undefined))
if (resolved) {
yield* watch(resolved, "directory")
if (resolved !== target) yield* watch(target, "file")
@@ -65,7 +65,7 @@ export const Plugin = define({
if (
yield* fs.realPath(directory).pipe(
Effect.as(true),
Effect.catch(() => Effect.succeed(false)),
Effect.orElseSucceed(() => false),
)
) {
if (missing) yield* FiberMap.remove(watches, `file:${path.resolve(missing)}`)
@@ -124,11 +124,11 @@ export const Plugin = define({
for (const directory of directories) {
const files = yield* fs
.scan("{*.md,**/SKILL.md}", { cwd: directory, absolute: true, include: "file", symlink: true, dot: true })
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
.pipe(Effect.orElseSucceed(() => [] as string[]))
for (const filepath of files.toSorted()) {
const resolved = yield* fs.realPath(filepath).pipe(Effect.catch(() => Effect.succeed(filepath)))
const resolved = yield* fs.realPath(filepath).pipe(Effect.orElseSucceed(() => filepath))
if (!roots.some((root) => FSUtil.contains(root, resolved))) yield* watch(path.dirname(resolved), "directory")
const content = yield* fs.readFileStringSafe(filepath).pipe(Effect.catch(() => Effect.succeed(undefined)))
const content = yield* fs.readFileStringSafe(filepath).pipe(Effect.orElseSucceed(() => undefined))
if (!content) continue
const parsed = SkillFile.parse(directory, filepath, content)
if (parsed._tag === "Skipped") {
+3 -2
View File
@@ -2,6 +2,7 @@ export * as Plugin from "./plugin.js"
export { Event, ID, Info, Source } from "@opencode-ai/schema/plugin"
import { Plugin } from "@opencode-ai/schema/plugin"
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { App } from "./app.js"
import { Cause, Context, Effect, Exit, Layer, Logger, References, Scope, Semaphore } from "effect"
@@ -31,7 +32,7 @@ export interface Interface {
readonly list: () => Effect.Effect<Plugin.Info[]>
}
export type Versioned = import("@opencode-ai/plugin/effect/plugin").Plugin & {
export type Versioned = PluginDefinition & {
readonly version: string
readonly source?: Plugin.Source
}
@@ -47,7 +48,7 @@ const layer = Layer.effect(
const active = new Map<Plugin.ID, { readonly plugin: Versioned; readonly scope: Scope.Closeable }>()
const lock = Semaphore.makeUnsafe(1)
let inventory: Plugin.Info[] = []
let host: Parameters<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>[0]
let host: Parameters<PluginDefinition["effect"]>[0]
const load = Effect.fnUntraced(function* (plugin: Versioned) {
const child = yield* Scope.fork(scope)
const inherit = yield* State.inherit()
+2 -4
View File
@@ -27,12 +27,10 @@ import { Tool } from "../tool.js"
import { Workspace } from "../workspace.js"
import { WebSearch } from "../websearch.js"
import { PluginHooks } from "./hooks.js"
import type { Interface } from "../plugin.js"
const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (
plugin: import("../plugin.js").Interface,
pluginID: string = "test",
) {
export const make = Effect.fn("PluginHost.make")(function* (plugin: Interface, pluginID: string = "test") {
const app = yield* App.Metadata
const agents = yield* Agent.Service
const aisdk = yield* AISDK.Service
+1 -2
View File
@@ -54,11 +54,10 @@ export interface Cell {
export const makeCell = (): Cell => ({})
const unavailable = <A, E, R>() => Effect.die(new Error("Plugin runtime is unavailable")) as Effect.Effect<A, E, R>
const require = <A, E, R>(cell: Cell, f: (runtime: Interface) => Effect.Effect<A, E, R>) =>
Effect.suspend(() => {
const runtime = cell.runtime
if (runtime === undefined) return unavailable<A, E, R>()
if (runtime === undefined) return Effect.die(new Error("Plugin runtime is unavailable"))
return f(runtime)
})
+1 -1
View File
@@ -157,7 +157,7 @@ const layer = Layer.effect(
const current =
version === undefined
? undefined
: yield* fs.readFileStringSafe(versionFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
: yield* fs.readFileStringSafe(versionFile).pipe(Effect.orElseSucceed(() => undefined))
if (version === undefined || current === version) {
yield* Effect.forEach(files, (file) => download(file.url, file.destination), {
concurrency: fileConcurrency,
+9 -13
View File
@@ -106,6 +106,10 @@ const layer = Layer.effect(
const bus = yield* Bus.Service
const cache = yield* Ref.make(new Map<string, Entry>())
const lock = Semaphore.makeUnsafe(1)
const loadEntry = Effect.fn("WellKnown.loadEntry")(function* (origin: string) {
const manifest = yield* inspect(origin).pipe(Effect.provideService(HttpClient.HttpClient, http))
return { origin, integrationID: Integration.ID.make(origin), manifest }
})
const load = Effect.fn("WellKnown.load")(function* () {
const value = yield* kv.get(sourcesKey)
@@ -114,10 +118,7 @@ const layer = Layer.effect(
const entries = yield* Effect.forEach(origins, (origin) => {
const cached = current.get(origin)
if (cached) return Effect.succeed(cached)
return inspect(origin).pipe(
Effect.provideService(HttpClient.HttpClient, http),
Effect.map((manifest) => ({ origin, integrationID: Integration.ID.make(origin), manifest })),
)
return loadEntry(origin)
})
yield* Ref.set(cache, new Map(entries.map((entry) => [entry.origin, entry])))
return entries
@@ -129,12 +130,7 @@ const layer = Layer.effect(
const value = yield* kv.get(sourcesKey)
const origins = Schema.is(Sources)(value) ? value : []
if (!origins.length) return false
const entries = yield* Effect.forEach(origins, (origin) =>
inspect(origin).pipe(
Effect.provideService(HttpClient.HttpClient, http),
Effect.map((manifest) => ({ origin, integrationID: Integration.ID.make(origin), manifest })),
),
)
const entries = yield* Effect.forEach(origins, loadEntry)
const next = new Map(entries.map((entry) => [entry.origin, entry]))
const changed = !isDeepStrictEqual(Ref.getUnsafe(cache), next)
if (!changed) return false
@@ -153,9 +149,9 @@ const layer = Layer.effect(
return yield* lock.withPermit(
Effect.gen(function* () {
const origin = value.replace(/\/+$/, "")
const manifest = yield* inspect(origin).pipe(Effect.provideService(HttpClient.HttpClient, http))
if (!manifest.auth) return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
const entry = { origin, integrationID: Integration.ID.make(origin), manifest }
const entry = yield* loadEntry(origin)
if (!entry.manifest.auth)
return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
const sources = yield* kv.get(sourcesKey)
const origins = Schema.is(Sources)(sources) ? sources : []
yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin])))