import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Skill } from "../../src/skill" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import path from "path" import fs from "fs/promises" const node = CrossSpawnSpawner.defaultLayer const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node)) async function createGlobalSkill(homeDir: string) { const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill") await fs.mkdir(skillDir, { recursive: true }) await Bun.write( path.join(skillDir, "SKILL.md"), `--- name: global-test-skill description: A global skill from ~/.claude/skills for testing. --- # Global Test Skill This skill is loaded from the global home directory. `, ) } const withHome = (home: string, self: Effect.Effect) => 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. --- # Test Skill Instructions here. `, ), ) 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 }, ), ) 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 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 }, ), ) 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 `, ), Bun.write( path.join(dir, ".opencode", "skill", "skill-two", "SKILL.md"), `--- name: skill-two description: Second test skill. --- # Skill Two `, ), ]), ) 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 }, ), ) 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. `, ), ) const skill = yield* Skill.Service expect(yield* skill.all()).toEqual([]) }), { git: true }, ), ) 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 `, ), ) 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 }, ), ) 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]()), ) 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)) }), ) }), ) 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 }, ), ) 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 `, ), ) 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 }, ), ) 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]()), ) 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. --- # Global Agent Skill This skill is loaded from the global home directory. `, ), ) 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)) }), ) }), ) 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 `, ), Bun.write( path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"), `--- name: agent-skill description: A skill in the .agents/skills directory. --- # Agent Skill `, ), ]), ) 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 }, ), ) 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 `, ), Bun.write( path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"), `--- name: agent-skill description: A skill in the .agents/skills directory. --- # Agent Skill `, ), Bun.write( path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"), `--- name: opencode-skill description: A skill in the .opencode/skill directory. --- # OpenCode Skill `, ), Bun.write( path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"), `--- name: opencode-skill description: A skill in the .opencode/skills directory. --- # OpenCode Skill `, ), ]), ) const skill = yield* Skill.Service expect((yield* skill.dirs()).length).toBe(4) }), { git: true }, ), ) })