import { lstat, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; export const CURATED_SKILL_PLUGIN_NAME = "cph-curated"; export const CURATED_SKILL_NAMES = [ "outline", "lesson-project", "data-processing-spec", ] as const; export const CURATED_SKILL_IDS = CURATED_SKILL_NAMES.map( (name) => `${CURATED_SKILL_PLUGIN_NAME}:${name}`, ); export interface CuratedSkillPlugin { readonly root: string; readonly skillIds: readonly string[]; } /** Validate the immutable, release-owned plugin before exposing it read-only. */ export async function validateCuratedSkillPlugin( root = curatedSkillPluginRoot(), ): Promise { await assertExactDirectoryEntries(root, [".claude-plugin", "skills"], ""); await assertExactDirectoryEntries(join(root, ".claude-plugin"), ["plugin.json"], ".claude-plugin"); await assertExactDirectoryEntries(join(root, "skills"), CURATED_SKILL_NAMES, "skills"); const pluginManifest = join(root, ".claude-plugin", "plugin.json"); let pluginName: unknown; try { const stat = await lstat(pluginManifest); if (!stat.isFile()) throw new Error(`not a regular file: ${pluginManifest}`); pluginName = JSON.parse(await readFile(pluginManifest, "utf8")).name; } catch (error) { throw new Error(`curated skill plugin manifest invalid: ${pluginManifest}`, { cause: error }); } if (pluginName !== CURATED_SKILL_PLUGIN_NAME) { throw new Error(`curated skill plugin name mismatch: expected ${CURATED_SKILL_PLUGIN_NAME}, got ${String(pluginName)}`); } for (const name of CURATED_SKILL_NAMES) { const manifest = join(root, "skills", name, "SKILL.md"); try { const stat = await lstat(manifest); if (!stat.isFile()) throw new Error(`not a regular file: ${manifest}`); const contents = await readFile(manifest, "utf8"); const declaredName = /^name:\s*['"]?([^'"\r\n]+)['"]?\s*$/m.exec(contents)?.[1]?.trim(); if (declaredName !== name) { throw new Error(`curated skill manifest name mismatch: expected ${name}, got ${declaredName ?? "missing"}`); } } catch (error) { if (error instanceof Error && error.message.startsWith("curated skill manifest name mismatch:")) throw error; throw new Error(`curated skill source missing: ${manifest}`, { cause: error }); } } return { root, skillIds: CURATED_SKILL_IDS }; } export function curatedSkillPluginRoot(): string { return fileURLToPath(new URL("../../curated-skills-plugin/", import.meta.url)); } async function assertExactDirectoryEntries( directory: string, allowedNames: readonly string[], relativeDirectory: string, ): Promise { let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { throw new Error(`curated plugin directory missing: ${directory}`, { cause: error }); } const allowed = new Set(allowedNames); for (const entry of entries) { if (!allowed.has(entry.name)) { const relativePath = relativeDirectory === "" ? entry.name : `${relativeDirectory}/${entry.name}`; throw new Error(`unexpected curated plugin entry: ${relativePath}`); } } for (const name of allowedNames) { if (!entries.some((entry) => entry.name === name)) { const relativePath = relativeDirectory === "" ? name : `${relativeDirectory}/${name}`; throw new Error(`curated plugin entry missing: ${relativePath}`); } } }