export * as PluginV2 from "./plugin" import { createDraft, finishDraft, type Draft } from "immer" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" import type { ModelV2 } from "./model" import type { Catalog } from "./catalog" import { EventV2 } from "./event" import { KeyedMutex } from "./effect/keyed-mutex" export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export type ID = typeof ID.Type export const Event = { Added: EventV2.define({ type: "plugin.added", schema: { id: ID, }, }), } type HookSpec = { "catalog.transform": { input: Catalog.Editor output: {} } "account.switched": { input: { serviceID: import("./auth").Auth.ServiceID from?: import("./auth").Auth.ID to?: import("./auth").Auth.ID } output: {} } "aisdk.language": { input: { model: ModelV2.Info sdk: any options: Record } output: { language?: LanguageModelV3 } } "aisdk.sdk": { input: { model: ModelV2.Info package: string options: Record } output: { sdk?: any } } } export type Hooks = { [Name in keyof HookSpec]: Readonly & { -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object ? Draft : HookSpec[Name]["output"][Field] } } export type HookFunctions = { [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect } export type HookInput = HookSpec[Name]["input"] export type HookOutput = HookSpec[Name]["output"] export type Effect = Effect.Effect export function define(input: { id: ID; effect: Effect.Effect }) { return input } export interface Interface { readonly add: (input: { id: ID effect: Effect.Effect }) => Effect.Effect readonly remove: (id: ID) => Effect.Effect readonly triggerFor: ( id: ID, name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> readonly trigger: ( name: Name, input: HookInput, output: HookOutput, ) => Effect.Effect & HookOutput> } export class Service extends Context.Service()("@opencode/v2/Plugin") {} export const layer = Layer.effect( Service, Effect.gen(function* () { let hooks: { id: ID hooks: HookFunctions scope: Scope.Closeable }[] = [] const events = yield* EventV2.Service const scope = yield* Scope.Scope const locks = KeyedMutex.makeUnsafe() const svc = Service.of({ add: Effect.fn("Plugin.add")(function* (input) { yield* locks.withLock(input.id)( Effect.gen(function* () { const existing = hooks.find((item) => item.id === input.id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) const childScope = yield* Scope.fork(scope) const result = yield* input.effect.pipe( Scope.provide(childScope), Effect.withSpan("Plugin.load", { attributes: { "plugin.id": input.id, }, }), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)), ) hooks = [ ...hooks.filter((item) => item.id !== input.id), { id: input.id, hooks: result ?? {}, scope: childScope, }, ] yield* events.publish(Event.Added, { id: input.id }) }), ) }), trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { return yield* svc.triggerFor(ID.make("*"), name, input, output) }), triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) { const draftEntries = new Map>() const event = { ...input, ...output, } as Record for (const [field, value] of Object.entries(output)) { if (value && typeof value === "object") { draftEntries.set(field, createDraft(value)) event[field] = draftEntries.get(field) } } for (const item of hooks) { if (id !== ID.make("*") && item.id !== id) continue const match = item.hooks[name] if (!match) continue yield* match(event as any).pipe( Effect.withSpan(`Plugin.hook.${name}`, { attributes: { plugin: item.id, hook: name, }, }), ) } for (const [field, draft] of draftEntries) { event[field] = finishDraft(draft) } return event as any }), remove: Effect.fn("Plugin.remove")(function* (id) { yield* locks.withLock(id)( Effect.gen(function* () { const existing = hooks.find((item) => item.id === id) hooks = hooks.filter((item) => item.id !== id) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) }), ) }), }) return svc }), ) export const locationLayer = layer // opencode // sdcok