summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/global/index.ts7
-rw-r--r--packages/opencode/src/skill/skill.ts43
-rw-r--r--packages/opencode/test/preload.ts6
-rw-r--r--packages/opencode/test/skill/skill.test.ts104
4 files changed, 121 insertions, 39 deletions
diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts
index 2504a47dc..7be58634e 100644
--- a/packages/opencode/src/global/index.ts
+++ b/packages/opencode/src/global/index.ts
@@ -12,14 +12,17 @@ const state = path.join(xdgState!, app)
export namespace Global {
export const Path = {
- home: os.homedir(),
+ // Allow override via OPENCODE_TEST_HOME for test isolation
+ get home() {
+ return process.env.OPENCODE_TEST_HOME || os.homedir()
+ },
data,
bin: path.join(data, "bin"),
log: path.join(data, "log"),
cache,
config,
state,
- } as const
+ }
}
await Promise.all([
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index 16fa1d08f..fa6fd7e43 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -4,6 +4,9 @@ import { Instance } from "../project/instance"
import { NamedError } from "@opencode-ai/util/error"
import { ConfigMarkdown } from "../config/markdown"
import { Log } from "../util/log"
+import { Global } from "@/global"
+import { Filesystem } from "@/util/filesystem"
+import { exists } from "fs/promises"
export namespace Skill {
const log = Log.create({ service: "skill" })
@@ -33,10 +36,9 @@ export namespace Skill {
)
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
- const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
+ const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
export const state = Instance.state(async () => {
- const directories = await Config.directories()
const skills: Record<string, Info> = {}
const addSkill = async (match: string) => {
@@ -64,25 +66,42 @@ export namespace Skill {
}
}
- for (const dir of directories) {
- for await (const match of OPENCODE_SKILL_GLOB.scan({
+ // Scan .claude/skills/ directories (project-level)
+ const claudeDirs = await Array.fromAsync(
+ Filesystem.up({
+ targets: [".claude"],
+ start: Instance.directory,
+ stop: Instance.worktree,
+ }),
+ )
+ // Also include global ~/.claude/skills/
+ const globalClaude = `${Global.Path.home}/.claude`
+ if (await exists(globalClaude)) {
+ claudeDirs.push(globalClaude)
+ }
+
+ for (const dir of claudeDirs) {
+ for await (const match of CLAUDE_SKILL_GLOB.scan({
cwd: dir,
absolute: true,
onlyFiles: true,
followSymlinks: true,
+ dot: true,
})) {
await addSkill(match)
}
}
- for await (const match of CLAUDE_SKILL_GLOB.scan({
- cwd: Instance.worktree,
- absolute: true,
- onlyFiles: true,
- followSymlinks: true,
- dot: true,
- })) {
- await addSkill(match)
+ // Scan .opencode/skill/ directories
+ for (const dir of await Config.directories()) {
+ for await (const match of OPENCODE_SKILL_GLOB.scan({
+ cwd: dir,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: true,
+ })) {
+ await addSkill(match)
+ }
}
return skills
diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts
index b6b6a66cf..76d1329f4 100644
--- a/packages/opencode/test/preload.ts
+++ b/packages/opencode/test/preload.ts
@@ -11,6 +11,12 @@ await fs.mkdir(dir, { recursive: true })
afterAll(() => {
fsSync.rmSync(dir, { recursive: true, force: true })
})
+// Set test home directory to isolate tests from user's actual home directory
+// This prevents tests from picking up real user configs/skills from ~/.claude/skills
+const testHome = path.join(dir, "home")
+await fs.mkdir(testHome, { recursive: true })
+process.env["OPENCODE_TEST_HOME"] = testHome
+
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts
index 1da8105bd..72415c141 100644
--- a/packages/opencode/test/skill/skill.test.ts
+++ b/packages/opencode/test/skill/skill.test.ts
@@ -1,9 +1,26 @@
import { test, expect } from "bun:test"
import { Skill } from "../../src/skill"
-import { SystemPrompt } from "../../src/session/system"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import path from "path"
+import fs from "fs/promises"
+
+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.
+`,
+ )
+}
test("discovers skills from .opencode/skill/ directory", async () => {
await using tmp = await tmpdir({
@@ -30,9 +47,10 @@ Instructions here.
fn: async () => {
const skills = await Skill.all()
expect(skills.length).toBe(1)
- expect(skills[0].name).toBe("test-skill")
- expect(skills[0].description).toBe("A test skill for verification.")
- expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
+ 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("skill/test-skill/SKILL.md")
},
})
})
@@ -41,15 +59,26 @@ test("discovers multiple skills from .opencode/skill/ directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
- const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
+ const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
+ const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
await Bun.write(
- path.join(skillDir, "SKILL.md"),
+ path.join(skillDir1, "SKILL.md"),
`---
-name: my-skill
-description: Another test skill.
+name: skill-one
+description: First test skill.
---
-# My Skill
+# Skill One
+`,
+ )
+ await Bun.write(
+ path.join(skillDir2, "SKILL.md"),
+ `---
+name: skill-two
+description: Second test skill.
+---
+
+# Skill Two
`,
)
},
@@ -59,8 +88,9 @@ description: Another test skill.
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
- expect(skills.length).toBe(1)
- expect(skills[0].name).toBe("my-skill")
+ expect(skills.length).toBe(2)
+ expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
+ expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
},
})
})
@@ -89,18 +119,6 @@ Just some content without YAML frontmatter.
})
})
-test("returns empty array when no skills exist", async () => {
- await using tmp = await tmpdir({ git: true })
-
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const skills = await Skill.all()
- expect(skills).toEqual([])
- },
- })
-})
-
test("discovers skills from .claude/skills/ directory", async () => {
await using tmp = await tmpdir({
git: true,
@@ -124,8 +142,44 @@ description: A skill in the .claude/skills directory.
fn: async () => {
const skills = await Skill.all()
expect(skills.length).toBe(1)
- expect(skills[0].name).toBe("claude-skill")
- expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
+ const claudeSkill = skills.find((s) => s.name === "claude-skill")
+ expect(claudeSkill).toBeDefined()
+ expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
+ },
+ })
+})
+
+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(".claude/skills/global-test-skill/SKILL.md")
+ },
+ })
+ } finally {
+ process.env.OPENCODE_TEST_HOME = originalHome
+ }
+})
+
+test("returns empty array when no skills exist", async () => {
+ await using tmp = await tmpdir({ git: true })
+
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const skills = await Skill.all()
+ expect(skills).toEqual([])
},
})
})