@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
118 lines (117 loc) • 4.36 kB
JavaScript
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;
}