refactor(core): reuse plugin source discovery (#41794)
This commit is contained in:
@@ -17,6 +17,7 @@ import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||
import { ConfigPolicyPlugin } from "../config/plugin/policy"
|
||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||
import { ConfigPluginSource } from "../config/plugin/source"
|
||||
import { ConfigWebSearchPlugin } from "../config/plugin/websearch"
|
||||
import { Bus } from "../bus"
|
||||
import { Environment } from "../environment"
|
||||
@@ -76,6 +77,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
const command = yield* Command.Service
|
||||
const config = yield* Config.Service
|
||||
const credential = yield* Credential.Service
|
||||
const pluginSources = yield* ConfigPluginSource.Service
|
||||
const bus = yield* Bus.Service
|
||||
const environment = yield* Environment.Service
|
||||
const mutation = yield* FileMutation.Service
|
||||
@@ -112,6 +114,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
Context.make(Command.Service, command),
|
||||
Context.make(Config.Service, config),
|
||||
Context.make(Credential.Service, credential),
|
||||
Context.make(ConfigPluginSource.Service, pluginSources),
|
||||
Context.make(Bus.Service, bus),
|
||||
Context.make(Environment.Service, environment),
|
||||
Context.make(FileMutation.Service, mutation),
|
||||
@@ -155,6 +158,7 @@ export const requirements = LayerNode.group([
|
||||
Command.node,
|
||||
Config.node,
|
||||
Credential.node,
|
||||
ConfigPluginSource.node,
|
||||
Bus.node,
|
||||
Environment.node,
|
||||
FileMutation.node,
|
||||
|
||||
@@ -6,12 +6,8 @@ import { define, type Context } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { Skill } from "../skill"
|
||||
import { Config } from "../config"
|
||||
import { Location } from "../location"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { ConfigPluginSource } from "../config/plugin/source"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import opencodeContent from "./skill/opencode.md" with { type: "text" }
|
||||
import reportContent from "./skill/report.md" with { type: "text" }
|
||||
|
||||
@@ -72,32 +68,10 @@ const reportContentWithDiagnostics = Effect.fn("SkillPlugin.reportContentWithDia
|
||||
})
|
||||
|
||||
const configuredPlugins = Effect.fn("SkillPlugin.configuredPlugins")(function* () {
|
||||
const config = yield* Config.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
return yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||
if (entry.type === "document") {
|
||||
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
||||
return Effect.succeed(
|
||||
(entry.info.plugins ?? []).map((item) => {
|
||||
const ref = typeof item === "string" ? { package: item } : item
|
||||
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
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (entry.type !== "directory") return Effect.succeed([])
|
||||
return fs
|
||||
.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: entry.path,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})
|
||||
.pipe(Effect.orElseSucceed(() => []))
|
||||
}).pipe(Effect.map((items) => items.flat().toSorted()))
|
||||
const sources = yield* ConfigPluginSource.Service
|
||||
return (yield* sources.operations())
|
||||
.map((operation) => (operation.type === "remove" ? `-${operation.target}` : operation.target))
|
||||
.toSorted()
|
||||
})
|
||||
|
||||
function terminal() {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { ConfigPluginSource } from "@opencode-ai/core/config/plugin/source"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { SkillPlugin } from "@opencode-ai/core/plugin/skill"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { host } from "./host"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(Skill.node))
|
||||
const sources = (operations: readonly ConfigPluginSource.Operation[] = []) =>
|
||||
Layer.succeed(
|
||||
ConfigPluginSource.Service,
|
||||
ConfigPluginSource.Service.of({ operations: () => Effect.succeed(operations), changes: () => Stream.never }),
|
||||
)
|
||||
|
||||
describe("SkillPlugin.Plugin", () => {
|
||||
it.effect("registers built-in skills", () =>
|
||||
@@ -27,15 +27,7 @@ describe("SkillPlugin.Plugin", () => {
|
||||
reload: skill.reload,
|
||||
},
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provide(Config.testLayer()),
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||
),
|
||||
Effect.provide(AppNodeBuilder.build(FSUtil.node)),
|
||||
Effect.provide(NodeFileSystem.layer),
|
||||
)
|
||||
).pipe(Effect.provide(sources()))
|
||||
const skills = yield* skill.list()
|
||||
const report = skills.find((item) => item.id === "report")
|
||||
|
||||
@@ -58,4 +50,30 @@ describe("SkillPlugin.Plugin", () => {
|
||||
expect(report?.content).toContain("- install/channel: beta")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reports canonical configured plugin sources with existing labels and ordering", () =>
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
yield* SkillPlugin.Plugin.effect(
|
||||
host({
|
||||
skill: {
|
||||
list: () => Effect.die("unused skill.list"),
|
||||
transform: skill.transform,
|
||||
reload: skill.reload,
|
||||
},
|
||||
}),
|
||||
)
|
||||
const report = (yield* skill.list()).find((item) => item.id === "report")
|
||||
expect(report?.content).toContain("- Active plugins: -disabled, local.ts, package-plugin, package-plugin")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
sources([
|
||||
{ type: "add", target: "package-plugin", options: {} },
|
||||
{ type: "remove", target: "disabled" },
|
||||
{ type: "add", target: "local.ts", options: {}, mtime: 1 },
|
||||
{ type: "add", target: "package-plugin", options: { enabled: true } },
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user