import { cp, readdir, rm } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { SKILLS_DIR } from './load'; import type { DiscoveredPlugin } from '../plugins/manifest'; // Publishing a plugin's skills, the same shape as affordances and notification catalogues. // // Deliberately NOT namespaced by publisher, unlike those two. A skill's directory name is what appears in // the index and what the agent reads a path to, and `officerdev/gitea/SKILL.md` in a prompt is noise for // a human and for a model. Collisions are handled by the loader taking whichever sorts first, which is // the same rule the items store has always used — one flat namespace, first come. // // That trade is only acceptable because a skill is REFERENCE. An affordance's namespace matters because a // plugin must not be able to claim `officer/core:`; a skill has no authority to usurp. export const skillDirFor = (plugin: DiscoveredPlugin): string => join(SKILLS_DIR, plugin.appName); /** What the plugin ships, if anything. */ export const skillSourceFor = (plugin: DiscoveredPlugin): string => join(plugin.dir, 'skills'); /** Publish a plugin's skills. Returns the number of directories, or 0 when it ships none. */ export async function publishSkills(plugin: DiscoveredPlugin): Promise { const source = skillSourceFor(plugin); if (!existsSync(source)) return 0; await cp(source, SKILLS_DIR, { recursive: true, force: true }); try { return (await readdir(source, { withFileTypes: true })).filter((e) => e.isDirectory()).length; } catch { return 0; } } /** * Drop a plugin's skills. * * Removes only the directories THIS plugin shipped, read from its own source tree — not everything under * `skills/`, which holds the platform's own and every other plugin's. */ export async function unpublishSkills(plugin: DiscoveredPlugin): Promise { const source = skillSourceFor(plugin); if (!existsSync(source)) return; let dirs: string[]; try { dirs = (await readdir(source, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return; } for (const dir of dirs) await rm(join(SKILLS_DIR, dir), { recursive: true, force: true }); }