The cross-spawn-spawner module has been moved from src/effect/ to src/ to simplify the core package structure. The src/types.d.ts file which contained unused type declarations has also been removed. All imports throughout the codebase have been updated to reflect the new location. This change reduces the package's internal complexity by flattening the module hierarchy and removing dead code, making future maintenance easier.
392 lines
11 KiB
TypeScript
392 lines
11 KiB
TypeScript
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 = <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.
|
|
---
|
|
|
|
# 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 },
|
|
),
|
|
)
|
|
})
|