UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

118 lines (117 loc) • 4.36 kB
import * as fs from "fs"; import * as path from "path"; import { homePath } from "../shared.js"; import { findProjectDir } from "../project-locator.js"; import { buildSkillManifest } from "./registry.js"; import { setSkillEnabled } from "./state.js"; import { errorMessage } from "../utils.js"; import { isManagedSymlink } from "../link/skills.js"; import { logger } from "../logger.js"; function normalizeSkillRemovalTarget(skillPath) { if (!skillPath) return skillPath; if (path.basename(skillPath).toLowerCase() === "skill.md") { return path.dirname(skillPath); } return skillPath; } function symlinkManagedSkill(src, dest, managedRoot) { try { const stat = fs.lstatSync(dest); if (stat.isSymbolicLink()) { const currentTarget = fs.readlinkSync(dest); const resolvedTarget = path.resolve(path.dirname(dest), currentTarget); if (resolvedTarget === path.resolve(src)) return; if (!isManagedSymlink(dest, managedRoot)) return; fs.unlinkSync(dest); } else { return; } } catch (err) { if (err.code !== "ENOENT") throw err; } fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.symlinkSync(src, dest); } function removeManagedSkillLink(dest, managedRoot) { try { if (!isManagedSymlink(dest, managedRoot)) return; fs.unlinkSync(dest); } catch (err) { if (err.code !== "ENOENT") logger.debug("skill-files", `removeManagedSkillLink: ${errorMessage(err)}`); } } function writeSkillArtifacts(destDir, manifest) { const parentDir = path.dirname(destDir); fs.mkdirSync(parentDir, { recursive: true }); fs.writeFileSync(path.join(parentDir, "skill-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); fs.writeFileSync(path.join(parentDir, "skill-commands.json"), `${JSON.stringify({ scope: manifest.scope, project: manifest.project, generatedAt: manifest.generatedAt, commands: manifest.commands.filter((command) => command.registered), problems: manifest.problems, }, null, 2)}\n`); } export function syncScopeSkillsToDir(phrenPath, scope, destDir) { const manifest = buildSkillManifest(phrenPath, "", scope, destDir); const expectedNames = new Set(); fs.mkdirSync(destDir, { recursive: true }); for (const skill of manifest.skills) { const destName = skill.format === "folder" ? skill.name : path.basename(skill.path); const destPath = path.join(destDir, destName); if (!skill.visibleToAgents) { removeManagedSkillLink(destPath, phrenPath); continue; } expectedNames.add(destName); symlinkManagedSkill(skill.root, destPath, phrenPath); } for (const entry of fs.readdirSync(destDir)) { if (expectedNames.has(entry)) continue; removeManagedSkillLink(path.join(destDir, entry), phrenPath); } writeSkillArtifacts(destDir, manifest); return manifest; } export function syncSkillLinksForScope(phrenPath, scope) { if (scope.toLowerCase() === "global") { const manifest = syncScopeSkillsToDir(phrenPath, "global", homePath(".claude", "skills")); for (const tool of [".agents", ".copilot"]) { const dir = homePath(tool, "skills"); if (fs.existsSync(path.join(path.dirname(dir), "skill-manifest.json"))) syncScopeSkillsToDir(phrenPath, "global", dir); } return manifest; } const projectDir = findProjectDir(scope); if (!projectDir) return null; return syncScopeSkillsToDir(phrenPath, scope, path.join(projectDir, ".claude", "skills")); } export function setSkillEnabledAndSync(phrenPath, scope, name, enabled) { setSkillEnabled(phrenPath, scope, name, enabled); syncSkillLinksForScope(phrenPath, scope); } export function removeSkillPath(skillPath) { const target = normalizeSkillRemovalTarget(skillPath); if (!target || !fs.existsSync(target)) return target; const stat = fs.lstatSync(target); if (stat.isDirectory()) { fs.rmSync(target, { recursive: true, force: true }); } else { fs.unlinkSync(target); } return target; }