feat(plugin): add namespaced hook API (#33416)

This commit is contained in:
Dax 2026-06-22 19:06:57 -04:00 committed by GitHub
commit 909a1a6d78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 3276 additions and 3906 deletions

View file

@ -1,6 +1,6 @@
export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Option, Schema } from "effect"
import { AgentV2 } from "../../agent"

View file

@ -1,6 +1,6 @@
export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect, Option, Schema } from "effect"
import { CommandV2 } from "../../command"

View file

@ -0,0 +1,99 @@
export * as ConfigExternalPlugin from "./external"
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
import { Effect, Schema } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Config } from "../../config"
import { FSUtil } from "../../fs-util"
import { Location } from "../../location"
import { Npm } from "../../npm"
import { define } from "../../plugin/internal"
import { PluginPromise } from "../../plugin/promise"
const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({
id: Schema.String,
effect: Schema.declare<EffectPlugin["effect"]>(
(input): input is EffectPlugin["effect"] => typeof input === "function",
),
}),
Schema.Struct({
id: Schema.String,
setup: Schema.declare<PromisePlugin["setup"]>(
(input): input is PromisePlugin["setup"] => typeof input === "function",
),
}),
]),
})
export const Plugin = define({
id: "config-plugin",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const loaded: EffectPlugin[] = []
yield* ctx.plugin.transform((plugins) => {
for (const plugin of loaded) plugins.add(plugin)
})
yield* Effect.gen(function* () {
const configured: { package: string; options?: Record<string, any> }[] = []
for (const entry of yield* config.entries()) {
if (entry.type === "document") {
const directory = entry.path ? path.dirname(entry.path) : location.directory
for (const item of entry.info.plugins ?? []) {
const ref = typeof item === "string" ? { package: item } : item
const packageName = (() => {
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
return path.resolve(directory, ref.package)
}
return ref.package
})()
configured.push({ package: packageName, options: ref.options })
}
}
if (entry.type === "directory") {
const files = yield* fs
.glob("{plugin,plugins}/*.{ts,js}", {
cwd: entry.path,
absolute: true,
include: "file",
dot: true,
symlink: true,
})
.pipe(Effect.orElseSucceed(() => []))
files.sort()
for (const file of files) configured.push({ package: file })
}
}
for (const ref of configured) {
yield* Effect.gen(function* () {
const entrypoint = path.isAbsolute(ref.package)
? pathToFileURL(ref.package).href
: (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
loaded.push({
id: plugin.id,
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
})
}).pipe(Effect.ignoreCause)
}
yield* ctx.plugin.reload()
}).pipe(Effect.forkScoped({ startImmediately: true }))
}),
})

View file

@ -1,6 +1,6 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect"
import { define } from "../../plugin/internal"
import { Effect } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"

View file

@ -1,24 +1,28 @@
export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/v2/effect"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect } from "effect"
import { Config } from "../../config"
import { ConfigReference } from "../reference"
import { Reference } from "../../reference"
import { AbsolutePath } from "../../schema"
import { Global } from "../../global"
import { Location } from "../../location"
export const Plugin = define({
id: "core/config-reference",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const location = yield* Location.Service
const global = yield* Global.Service
yield* ctx.reference.transform(
Effect.fn(function* (draft) {
const entries = new Map<string, Reference.Source>()
for (const doc of (yield* config.entries()).filter(
(entry): entry is Config.Document => entry.type === "document",
)) {
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
entries.set(
@ -27,7 +31,7 @@ export const Plugin = define({
? new Reference.LocalSource({
type: "local",
path: AbsolutePath.make(
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
),
description: typeof entry === "string" ? undefined : entry.description,
hidden: typeof entry === "string" ? undefined : entry.hidden,

View file

@ -1,16 +1,20 @@
export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect"
import { define } from "../../plugin/internal"
import path from "path"
import { Effect } from "effect"
import { Config } from "../../config"
import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill"
import { Global } from "../../global"
import { Location } from "../../location"
export const Plugin = define({
id: "config-skill",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const global = yield* Global.Service
const location = yield* Location.Service
yield* ctx.skill.transform(
Effect.fn(function* (draft) {
const entries = yield* config.entries()
@ -29,13 +33,11 @@ export const Plugin = define({
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
continue
}
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
new SkillV2.DirectorySource({
type: "directory",
path: AbsolutePath.make(
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
),
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),
)
}