Przeglądaj źródła

refactor(skill): remove async facade exports (#22308)

Kit Langton 1 tydzień temu
rodzic
commit
7239b38b7f

+ 8 - 1
packages/opencode/src/cli/cmd/debug/skill.ts

@@ -1,4 +1,6 @@
 import { EOL } from "os"
+import { Effect } from "effect"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Skill } from "../../../skill"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
@@ -9,7 +11,12 @@ export const SkillCommand = cmd({
   builder: (yargs) => yargs,
   async handler() {
     await bootstrap(process.cwd(), async () => {
-      const skills = await Skill.all()
+      const skills = await AppRuntime.runPromise(
+        Effect.gen(function* () {
+          const skill = yield* Skill.Service
+          return yield* skill.all()
+        }),
+      )
       process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
     })
   },

+ 7 - 1
packages/opencode/src/server/instance/index.ts

@@ -24,6 +24,7 @@ import { ProviderRoutes } from "./provider"
 import { EventRoutes } from "./event"
 import { WorkspaceRouterMiddleware } from "./middleware"
 import { AppRuntime } from "@/effect/app-runtime"
+import { Effect } from "effect"
 
 export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
   new Hono()
@@ -215,7 +216,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         },
       }),
       async (c) => {
-        const skills = await Skill.all()
+        const skills = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            return yield* skill.all()
+          }),
+        )
         return c.json(skills)
       },
     )

+ 0 - 19
packages/opencode/src/skill/index.ts

@@ -7,7 +7,6 @@ import { NamedError } from "@opencode-ai/util/error"
 import type { Agent } from "@/agent/agent"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Flag } from "@/flag/flag"
 import { Global } from "@/global"
 import { Permission } from "@/permission"
@@ -262,22 +261,4 @@ export namespace Skill {
         .map((skill) => `- **${skill.name}**: ${skill.description}`),
     ].join("\n")
   }
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function get(name: string) {
-    return runPromise((skill) => skill.get(name))
-  }
-
-  export async function all() {
-    return runPromise((skill) => skill.all())
-  }
-
-  export async function dirs() {
-    return runPromise((skill) => skill.dirs())
-  }
-
-  export async function available(agent?: Agent.Info) {
-    return runPromise((skill) => skill.available(agent))
-  }
 }

+ 273 - 272
packages/opencode/test/skill/skill.test.ts

@@ -1,13 +1,17 @@
-import { afterEach, test, expect } from "bun:test"
+import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
 import { Skill } from "../../src/skill"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
+import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 import path from "path"
 import fs from "fs/promises"
 
-afterEach(async () => {
-  await Instance.disposeAll()
-})
+const node = NodeChildProcessSpawner.layer.pipe(
+  Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
+)
+
+const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node))
 
 async function createGlobalSkill(homeDir: string) {
   const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
@@ -26,14 +30,29 @@ This skill is loaded from the global home directory.
   )
 }
 
-test("discovers skills from .opencode/skill/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+const withHome = <A, E, R>(home: string, self: Effect.Effect<A, E, R>) =>
+  Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const prev = process.env.OPENCODE_TEST_HOME
+      process.env.OPENCODE_TEST_HOME = home
+      return prev
+    }),
+    () => self,
+    (prev) =>
+      Effect.sync(() => {
+        process.env.OPENCODE_TEST_HOME = prev
+      }),
+  )
+
+describe("skill", () => {
+  it.live("discovers skills from .opencode/skill/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".opencode", "skill", "test-skill", "SKILL.md"),
+              `---
 name: test-skill
 description: A test skill for verification.
 ---
@@ -42,230 +61,217 @@ description: A test skill for verification.
 
 Instructions here.
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const testSkill = skills.find((s) => s.name === "test-skill")
-      expect(testSkill).toBeDefined()
-      expect(testSkill!.description).toBe("A test skill for verification.")
-      expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "test-skill")
+          expect(item).toBeDefined()
+          expect(item!.description).toBe("A test skill for verification.")
+          expect(item!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-test("returns skill directories from Skill.dirs", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "dir-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("returns skill directories from Skill.dirs", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        withHome(
+          dir,
+          Effect.gen(function* () {
+            yield* Effect.promise(() =>
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "dir-skill", "SKILL.md"),
+                `---
 name: dir-skill
 description: Skill for dirs test.
 ---
 
 # Dir Skill
 `,
-      )
-    },
-  })
-
-  const home = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
-
-  try {
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const dirs = await Skill.dirs()
-        const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill")
-        expect(dirs).toContain(skillDir)
-        expect(dirs.length).toBe(1)
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = home
-  }
-})
+              ),
+            )
+
+            const skill = yield* Skill.Service
+            const dirs = yield* skill.dirs()
+            expect(dirs).toContain(path.join(dir, ".opencode", "skill", "dir-skill"))
+            expect(dirs.length).toBe(1)
+          }),
+        ),
+      { git: true },
+    ),
+  )
 
-test("discovers multiple skills from .opencode/skill/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
-      const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
-      await Bun.write(
-        path.join(skillDir1, "SKILL.md"),
-        `---
+  it.live("discovers multiple skills from .opencode/skill/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "skill-one", "SKILL.md"),
+                `---
 name: skill-one
 description: First test skill.
 ---
 
 # Skill One
 `,
-      )
-      await Bun.write(
-        path.join(skillDir2, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "skill-two", "SKILL.md"),
+                `---
 name: skill-two
 description: Second test skill.
 ---
 
 # Skill Two
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(2)
-      expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
-      expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
-    },
-  })
-})
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(2)
+          expect(list.find((x) => x.name === "skill-one")).toBeDefined()
+          expect(list.find((x) => x.name === "skill-two")).toBeDefined()
+        }),
+      { git: true },
+    ),
+  )
 
-test("skips skills with missing frontmatter", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `# No Frontmatter
+  it.live("skips skills with missing frontmatter", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".opencode", "skill", "no-frontmatter", "SKILL.md"),
+              `# No Frontmatter
 
 Just some content without YAML frontmatter.
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills).toEqual([])
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          expect(yield* skill.all()).toEqual([])
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers skills from .claude/skills/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from .claude/skills/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+              `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const claudeSkill = skills.find((s) => s.name === "claude-skill")
-      expect(claudeSkill).toBeDefined()
-      expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
-    },
-  })
-})
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "claude-skill")
+          expect(item).toBeDefined()
+          expect(item!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers global skills from ~/.claude/skills/ directory", async () => {
-  await using tmp = await tmpdir({ git: true })
-
-  const originalHome = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
-
-  try {
-    await createGlobalSkill(tmp.path)
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const skills = await Skill.all()
-        expect(skills.length).toBe(1)
-        expect(skills[0].name).toBe("global-test-skill")
-        expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
-        expect(skills[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = originalHome
-  }
-})
+  it.live("discovers global skills from ~/.claude/skills/ directory", () =>
+    Effect.gen(function* () {
+      const tmp = yield* Effect.acquireRelease(
+        Effect.promise(() => tmpdir({ git: true })),
+        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+      )
 
-test("returns empty array when no skills exist", async () => {
-  await using tmp = await tmpdir({ git: true })
+      yield* withHome(
+        tmp.path,
+        Effect.gen(function* () {
+          yield* Effect.promise(() => createGlobalSkill(tmp.path))
+          yield* Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            const list = yield* skill.all()
+            expect(list.length).toBe(1)
+            expect(list[0].name).toBe("global-test-skill")
+            expect(list[0].description).toBe("A global skill from ~/.claude/skills for testing.")
+            expect(list[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
+          }).pipe(provideInstance(tmp.path))
+        }),
+      )
+    }),
+  )
 
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills).toEqual([])
-    },
-  })
-})
+  it.live("returns empty array when no skills exist", () =>
+    provideTmpdirInstance(
+      () =>
+        Effect.gen(function* () {
+          const skill = yield* Skill.Service
+          expect(yield* skill.all()).toEqual([])
+        }),
+      { git: true },
+    ),
+  )
 
-test("discovers skills from .agents/skills/ directory", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const skillDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(skillDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from .agents/skills/ directory", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+              `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(1)
-      const agentSkill = skills.find((s) => s.name === "agent-skill")
-      expect(agentSkill).toBeDefined()
-      expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
-    },
-  })
-})
-
-test("discovers global skills from ~/.agents/skills/ directory", async () => {
-  await using tmp = await tmpdir({ git: true })
+            ),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(1)
+          const item = list.find((x) => x.name === "agent-skill")
+          expect(item).toBeDefined()
+          expect(item!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
+        }),
+      { git: true },
+    ),
+  )
 
-  const originalHome = process.env.OPENCODE_TEST_HOME
-  process.env.OPENCODE_TEST_HOME = tmp.path
+  it.live("discovers global skills from ~/.agents/skills/ directory", () =>
+    Effect.gen(function* () {
+      const tmp = yield* Effect.acquireRelease(
+        Effect.promise(() => tmpdir({ git: true })),
+        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+      )
 
-  try {
-    const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
-    await fs.mkdir(skillDir, { recursive: true })
-    await Bun.write(
-      path.join(skillDir, "SKILL.md"),
-      `---
+      yield* withHome(
+        tmp.path,
+        Effect.gen(function* () {
+          const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
+          yield* Effect.promise(() => fs.mkdir(skillDir, { recursive: true }))
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(skillDir, "SKILL.md"),
+              `---
 name: global-agent-skill
 description: A global skill from ~/.agents/skills for testing.
 ---
@@ -274,119 +280,114 @@ description: A global skill from ~/.agents/skills for testing.
 
 This skill is loaded from the global home directory.
 `,
-    )
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const skills = await Skill.all()
-        expect(skills.length).toBe(1)
-        expect(skills[0].name).toBe("global-agent-skill")
-        expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
-        expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
-      },
-    })
-  } finally {
-    process.env.OPENCODE_TEST_HOME = originalHome
-  }
-})
+            ),
+          )
+
+          yield* Effect.gen(function* () {
+            const skill = yield* Skill.Service
+            const list = yield* skill.all()
+            expect(list.length).toBe(1)
+            expect(list[0].name).toBe("global-agent-skill")
+            expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
+            expect(list[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
+          }).pipe(provideInstance(tmp.path))
+        }),
+      )
+    }),
+  )
 
-test("discovers skills from both .claude/skills/ and .agents/skills/", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
-      const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(claudeDir, "SKILL.md"),
-        `---
+  it.live("discovers skills from both .claude/skills/ and .agents/skills/", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+                `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-      await Bun.write(
-        path.join(agentDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const skills = await Skill.all()
-      expect(skills.length).toBe(2)
-      expect(skills.find((s) => s.name === "claude-skill")).toBeDefined()
-      expect(skills.find((s) => s.name === "agent-skill")).toBeDefined()
-    },
-  })
-})
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          const list = yield* skill.all()
+          expect(list.length).toBe(2)
+          expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
+          expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
+        }),
+      { git: true },
+    ),
+  )
 
-test("properly resolves directories that skills live in", async () => {
-  await using tmp = await tmpdir({
-    git: true,
-    init: async (dir) => {
-      const opencodeSkillDir = path.join(dir, ".opencode", "skill", "agent-skill")
-      const opencodeSkillsDir = path.join(dir, ".opencode", "skills", "agent-skill")
-      const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
-      const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
-      await Bun.write(
-        path.join(claudeDir, "SKILL.md"),
-        `---
+  it.live("properly resolves directories that skills live in", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Promise.all([
+              Bun.write(
+                path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
+                `---
 name: claude-skill
 description: A skill in the .claude/skills directory.
 ---
 
 # Claude Skill
 `,
-      )
-      await Bun.write(
-        path.join(agentDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: agent-skill
 description: A skill in the .agents/skills directory.
 ---
 
 # Agent Skill
 `,
-      )
-      await Bun.write(
-        path.join(opencodeSkillDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"),
+                `---
 name: opencode-skill
 description: A skill in the .opencode/skill directory.
 ---
 
 # OpenCode Skill
 `,
-      )
-      await Bun.write(
-        path.join(opencodeSkillsDir, "SKILL.md"),
-        `---
+              ),
+              Bun.write(
+                path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"),
+                `---
 name: opencode-skill
 description: A skill in the .opencode/skills directory.
 ---
 
 # OpenCode Skill
 `,
-      )
-    },
-  })
-
-  await Instance.provide({
-    directory: tmp.path,
-    fn: async () => {
-      const dirs = await Skill.dirs()
-      expect(dirs.length).toBe(4)
-    },
-  })
+              ),
+            ]),
+          )
+
+          const skill = yield* Skill.Service
+          expect((yield* skill.dirs()).length).toBe(4)
+        }),
+      { git: true },
+    ),
+  )
 })