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

This commit is contained in:
Kit Langton
2026-04-13 11:18:10 -04:00
committed by GitHub
parent 9ae8dc2d01
commit 7239b38b7f
4 changed files with 281 additions and 286 deletions
+8 -1
View File
@@ -1,4 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { Effect } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { Skill } from "../../../skill" import { Skill } from "../../../skill"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@@ -9,7 +11,12 @@ export const SkillCommand = cmd({
builder: (yargs) => yargs, builder: (yargs) => yargs,
async handler() { async handler() {
await bootstrap(process.cwd(), async () => { 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) process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
}) })
}, },
@@ -24,6 +24,7 @@ import { ProviderRoutes } from "./provider"
import { EventRoutes } from "./event" import { EventRoutes } from "./event"
import { WorkspaceRouterMiddleware } from "./middleware" import { WorkspaceRouterMiddleware } from "./middleware"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Effect } from "effect"
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
new Hono() new Hono()
@@ -215,7 +216,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
}, },
}), }),
async (c) => { 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) return c.json(skills)
}, },
) )
-19
View File
@@ -7,7 +7,6 @@ import { NamedError } from "@opencode-ai/util/error"
import type { Agent } from "@/agent/agent" import type { Agent } from "@/agent/agent"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Global } from "@/global" import { Global } from "@/global"
import { Permission } from "@/permission" import { Permission } from "@/permission"
@@ -262,22 +261,4 @@ export namespace Skill {
.map((skill) => `- **${skill.name}**: ${skill.description}`), .map((skill) => `- **${skill.name}**: ${skill.description}`),
].join("\n") ].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))
}
} }
+266 -265
View File
@@ -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 { Skill } from "../../src/skill"
import { Instance } from "../../src/project/instance" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
afterEach(async () => { const node = NodeChildProcessSpawner.layer.pipe(
await Instance.disposeAll() Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
}) )
const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node))
async function createGlobalSkill(homeDir: string) { async function createGlobalSkill(homeDir: string) {
const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill") 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 () => { const withHome = <A, E, R>(home: string, self: Effect.Effect<A, E, R>) =>
await using tmp = await tmpdir({ Effect.acquireUseRelease(
git: true, Effect.sync(() => {
init: async (dir) => { const prev = process.env.OPENCODE_TEST_HOME
const skillDir = path.join(dir, ".opencode", "skill", "test-skill") process.env.OPENCODE_TEST_HOME = home
await Bun.write( return prev
path.join(skillDir, "SKILL.md"), }),
`--- () => 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 name: test-skill
description: A test skill for verification. description: A test skill for verification.
--- ---
@@ -42,230 +61,217 @@ description: A test skill for verification.
Instructions here. Instructions here.
`, `,
) ),
}, )
})
await Instance.provide({ const skill = yield* Skill.Service
directory: tmp.path, const list = yield* skill.all()
fn: async () => { expect(list.length).toBe(1)
const skills = await Skill.all() const item = list.find((x) => x.name === "test-skill")
expect(skills.length).toBe(1) expect(item).toBeDefined()
const testSkill = skills.find((s) => s.name === "test-skill") expect(item!.description).toBe("A test skill for verification.")
expect(testSkill).toBeDefined() expect(item!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
expect(testSkill!.description).toBe("A test skill for verification.") }),
expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md")) { git: true },
}, ),
}) )
})
test("returns skill directories from Skill.dirs", async () => { it.live("returns skill directories from Skill.dirs", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { withHome(
const skillDir = path.join(dir, ".opencode", "skill", "dir-skill") dir,
await Bun.write( Effect.gen(function* () {
path.join(skillDir, "SKILL.md"), yield* Effect.promise(() =>
`--- Bun.write(
path.join(dir, ".opencode", "skill", "dir-skill", "SKILL.md"),
`---
name: dir-skill name: dir-skill
description: Skill for dirs test. description: Skill for dirs test.
--- ---
# Dir Skill # Dir Skill
`, `,
) ),
}, )
})
const home = process.env.OPENCODE_TEST_HOME const skill = yield* Skill.Service
process.env.OPENCODE_TEST_HOME = tmp.path const dirs = yield* skill.dirs()
expect(dirs).toContain(path.join(dir, ".opencode", "skill", "dir-skill"))
expect(dirs.length).toBe(1)
}),
),
{ git: true },
),
)
try { it.live("discovers multiple skills from .opencode/skill/ directory", () =>
await Instance.provide({ provideTmpdirInstance(
directory: tmp.path, (dir) =>
fn: async () => { Effect.gen(function* () {
const dirs = await Skill.dirs() yield* Effect.promise(() =>
const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill") Promise.all([
expect(dirs).toContain(skillDir) Bun.write(
expect(dirs.length).toBe(1) path.join(dir, ".opencode", "skill", "skill-one", "SKILL.md"),
}, `---
})
} finally {
process.env.OPENCODE_TEST_HOME = home
}
})
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"),
`---
name: skill-one name: skill-one
description: First test skill. description: First test skill.
--- ---
# Skill One # Skill One
`, `,
) ),
await Bun.write( Bun.write(
path.join(skillDir2, "SKILL.md"), path.join(dir, ".opencode", "skill", "skill-two", "SKILL.md"),
`--- `---
name: skill-two name: skill-two
description: Second test skill. description: Second test skill.
--- ---
# Skill Two # Skill Two
`, `,
) ),
}, ]),
}) )
await Instance.provide({ const skill = yield* Skill.Service
directory: tmp.path, const list = yield* skill.all()
fn: async () => { expect(list.length).toBe(2)
const skills = await Skill.all() expect(list.find((x) => x.name === "skill-one")).toBeDefined()
expect(skills.length).toBe(2) expect(list.find((x) => x.name === "skill-two")).toBeDefined()
expect(skills.find((s) => s.name === "skill-one")).toBeDefined() }),
expect(skills.find((s) => s.name === "skill-two")).toBeDefined() { git: true },
}, ),
}) )
})
test("skips skills with missing frontmatter", async () => { it.live("skips skills with missing frontmatter", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { Effect.gen(function* () {
const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter") yield* Effect.promise(() =>
await Bun.write( Bun.write(
path.join(skillDir, "SKILL.md"), path.join(dir, ".opencode", "skill", "no-frontmatter", "SKILL.md"),
`# No Frontmatter `# No Frontmatter
Just some content without YAML frontmatter. Just some content without YAML frontmatter.
`, `,
) ),
}, )
})
await Instance.provide({ const skill = yield* Skill.Service
directory: tmp.path, expect(yield* skill.all()).toEqual([])
fn: async () => { }),
const skills = await Skill.all() { git: true },
expect(skills).toEqual([]) ),
}, )
})
})
test("discovers skills from .claude/skills/ directory", async () => { it.live("discovers skills from .claude/skills/ directory", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { Effect.gen(function* () {
const skillDir = path.join(dir, ".claude", "skills", "claude-skill") yield* Effect.promise(() =>
await Bun.write( Bun.write(
path.join(skillDir, "SKILL.md"), path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
`--- `---
name: claude-skill name: claude-skill
description: A skill in the .claude/skills directory. description: A skill in the .claude/skills directory.
--- ---
# Claude Skill # 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]()),
) )
},
})
await Instance.provide({ yield* withHome(
directory: tmp.path, tmp.path,
fn: async () => { Effect.gen(function* () {
const skills = await Skill.all() yield* Effect.promise(() => createGlobalSkill(tmp.path))
expect(skills.length).toBe(1) yield* Effect.gen(function* () {
const claudeSkill = skills.find((s) => s.name === "claude-skill") const skill = yield* Skill.Service
expect(claudeSkill).toBeDefined() const list = yield* skill.all()
expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md")) 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))
}),
)
}),
)
test("discovers global skills from ~/.claude/skills/ directory", async () => { it.live("returns empty array when no skills exist", () =>
await using tmp = await tmpdir({ git: true }) provideTmpdirInstance(
() =>
Effect.gen(function* () {
const skill = yield* Skill.Service
expect(yield* skill.all()).toEqual([])
}),
{ git: true },
),
)
const originalHome = process.env.OPENCODE_TEST_HOME it.live("discovers skills from .agents/skills/ directory", () =>
process.env.OPENCODE_TEST_HOME = tmp.path provideTmpdirInstance(
(dir) =>
try { Effect.gen(function* () {
await createGlobalSkill(tmp.path) yield* Effect.promise(() =>
await Instance.provide({ Bun.write(
directory: tmp.path, path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
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
}
})
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 .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"),
`---
name: agent-skill name: agent-skill
description: A skill in the .agents/skills directory. description: A skill in the .agents/skills directory.
--- ---
# Agent Skill # 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]()),
) )
},
})
await Instance.provide({ yield* withHome(
directory: tmp.path, tmp.path,
fn: async () => { Effect.gen(function* () {
const skills = await Skill.all() const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
expect(skills.length).toBe(1) yield* Effect.promise(() => fs.mkdir(skillDir, { recursive: true }))
const agentSkill = skills.find((s) => s.name === "agent-skill") yield* Effect.promise(() =>
expect(agentSkill).toBeDefined() Bun.write(
expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md")) path.join(skillDir, "SKILL.md"),
}, `---
})
})
test("discovers global skills from ~/.agents/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 {
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"),
`---
name: global-agent-skill name: global-agent-skill
description: A global skill from ~/.agents/skills for testing. 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. This skill is loaded from the global home directory.
`, `,
) ),
)
await Instance.provide({ yield* Effect.gen(function* () {
directory: tmp.path, const skill = yield* Skill.Service
fn: async () => { const list = yield* skill.all()
const skills = await Skill.all() expect(list.length).toBe(1)
expect(skills.length).toBe(1) expect(list[0].name).toBe("global-agent-skill")
expect(skills[0].name).toBe("global-agent-skill") expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
expect(skills[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"))
expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md")) }).pipe(provideInstance(tmp.path))
}, }),
}) )
} finally { }),
process.env.OPENCODE_TEST_HOME = originalHome )
}
})
test("discovers skills from both .claude/skills/ and .agents/skills/", async () => { it.live("discovers skills from both .claude/skills/ and .agents/skills/", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { Effect.gen(function* () {
const claudeDir = path.join(dir, ".claude", "skills", "claude-skill") yield* Effect.promise(() =>
const agentDir = path.join(dir, ".agents", "skills", "agent-skill") Promise.all([
await Bun.write( Bun.write(
path.join(claudeDir, "SKILL.md"), path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
`--- `---
name: claude-skill name: claude-skill
description: A skill in the .claude/skills directory. description: A skill in the .claude/skills directory.
--- ---
# Claude Skill # Claude Skill
`, `,
) ),
await Bun.write( Bun.write(
path.join(agentDir, "SKILL.md"), path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
`--- `---
name: agent-skill name: agent-skill
description: A skill in the .agents/skills directory. description: A skill in the .agents/skills directory.
--- ---
# Agent Skill # Agent Skill
`, `,
) ),
}, ]),
}) )
await Instance.provide({ const skill = yield* Skill.Service
directory: tmp.path, const list = yield* skill.all()
fn: async () => { expect(list.length).toBe(2)
const skills = await Skill.all() expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
expect(skills.length).toBe(2) expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
expect(skills.find((s) => s.name === "claude-skill")).toBeDefined() }),
expect(skills.find((s) => s.name === "agent-skill")).toBeDefined() { git: true },
}, ),
}) )
})
test("properly resolves directories that skills live in", async () => { it.live("properly resolves directories that skills live in", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { Effect.gen(function* () {
const opencodeSkillDir = path.join(dir, ".opencode", "skill", "agent-skill") yield* Effect.promise(() =>
const opencodeSkillsDir = path.join(dir, ".opencode", "skills", "agent-skill") Promise.all([
const claudeDir = path.join(dir, ".claude", "skills", "claude-skill") Bun.write(
const agentDir = path.join(dir, ".agents", "skills", "agent-skill") path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
await Bun.write( `---
path.join(claudeDir, "SKILL.md"),
`---
name: claude-skill name: claude-skill
description: A skill in the .claude/skills directory. description: A skill in the .claude/skills directory.
--- ---
# Claude Skill # Claude Skill
`, `,
) ),
await Bun.write( Bun.write(
path.join(agentDir, "SKILL.md"), path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
`--- `---
name: agent-skill name: agent-skill
description: A skill in the .agents/skills directory. description: A skill in the .agents/skills directory.
--- ---
# Agent Skill # Agent Skill
`, `,
) ),
await Bun.write( Bun.write(
path.join(opencodeSkillDir, "SKILL.md"), path.join(dir, ".opencode", "skill", "agent-skill", "SKILL.md"),
`--- `---
name: opencode-skill name: opencode-skill
description: A skill in the .opencode/skill directory. description: A skill in the .opencode/skill directory.
--- ---
# OpenCode Skill # OpenCode Skill
`, `,
) ),
await Bun.write( Bun.write(
path.join(opencodeSkillsDir, "SKILL.md"), path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"),
`--- `---
name: opencode-skill name: opencode-skill
description: A skill in the .opencode/skills directory. description: A skill in the .opencode/skills directory.
--- ---
# OpenCode Skill # OpenCode Skill
`, `,
) ),
}, ]),
}) )
await Instance.provide({ const skill = yield* Skill.Service
directory: tmp.path, expect((yield* skill.dirs()).length).toBe(4)
fn: async () => { }),
const dirs = await Skill.dirs() { git: true },
expect(dirs.length).toBe(4) ),
}, )
})
}) })