refactor(core): simplify plugin and config boundaries (#43930)
This commit is contained in:
@@ -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 }]
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])))
|
||||
|
||||
Reference in New Issue
Block a user