diff --git a/src/features/builtin-skills/skill-file-loader.test.ts b/src/features/builtin-skills/skill-file-loader.test.ts index 6ad2ae87f..eb77810db 100644 --- a/src/features/builtin-skills/skill-file-loader.test.ts +++ b/src/features/builtin-skills/skill-file-loader.test.ts @@ -40,6 +40,30 @@ describe("shared builtin skill file loader", () => { expect(reads).toHaveLength(1) }) + test("#given source and bundled layouts #when loading shared skill templates #then both package-relative paths resolve", () => { + // given + const expectedContent = "---\nname: layout\n---\nLayout body" + const createMissingFileError = (): Error => { + const error = new Error("ENOENT missing SKILL.md") + Object.defineProperty(error, "code", { value: "ENOENT" }) + return error + } + const readFile = (path: string): string => { + if (path.endsWith("/packages/shared-skills/skills/layout/SKILL.md")) { + return expectedContent + } + throw createMissingFileError() + } + + // when + const bundledLoader = createSharedSkillTemplateLoader(readFile, "/workspace/dist") + const sourceLoader = createSharedSkillTemplateLoader(readFile, "/workspace/src/features/builtin-skills") + + // then + expect(bundledLoader("layout")).toBe("Layout body") + expect(sourceLoader("layout")).toBe("Layout body") + }) + test("#given a missing shared skill file #when loading the template #then the loader fails fast", () => { // given const loader = createSharedSkillTemplateLoader(() => { diff --git a/src/features/builtin-skills/skill-file-loader.ts b/src/features/builtin-skills/skill-file-loader.ts index 200798886..64a6fc4c5 100644 --- a/src/features/builtin-skills/skill-file-loader.ts +++ b/src/features/builtin-skills/skill-file-loader.ts @@ -1,28 +1,38 @@ import { readFileSync } from "node:fs" -import { join } from "node:path" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" import { parseFrontmatter } from "../../shared/frontmatter" - type SkillFileReader = (path: string, encoding: "utf8") => string - -export function createSharedSkillTemplateLoader(readFile: SkillFileReader = readFileSync): (skillName: string) => string { +const SHARED_SKILL_PATHS = [ + ["..", "packages", "shared-skills", "skills"], + ["..", "..", "..", "packages", "shared-skills", "skills"], +] as const +const moduleDir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(fileURLToPath(import.meta.url)) +export function createSharedSkillTemplateLoader( + readFile: SkillFileReader = readFileSync, + baseDir: string = moduleDir, +): (skillName: string) => string { const cache = new Map() - return (skillName) => { const cached = cache.get(skillName) - if (cached !== undefined) { - return cached + if (cached !== undefined) return cached + let missingFileError: unknown + for (const segments of SHARED_SKILL_PATHS) { + try { + const { body } = parseFrontmatter(readFile(join(baseDir, ...segments, skillName, "SKILL.md"), "utf8")) + cache.set(skillName, body) + return body + } catch (error) { + if (!(error instanceof Error && Reflect.get(error, "code") === "ENOENT")) { + throw error + } + missingFileError ??= error + } } - - const skillPath = join(import.meta.dir, "..", "..", "..", "packages", "shared-skills", "skills", skillName, "SKILL.md") - const content = readFile(skillPath, "utf8") - const { body } = parseFrontmatter(content) - cache.set(skillName, body) - return body + throw missingFileError ?? new Error(`missing shared skill template: ${skillName}`) } } - const loadSharedSkillTemplateFromDisk = createSharedSkillTemplateLoader() - export function loadSharedSkillTemplate(skillName: string): string { return loadSharedSkillTemplateFromDisk(skillName) }