import fs from "node:fs"; import path from "node:path"; import { PACKAGE_ROOT } from "../../util"; export const SKILLS_SRC = path.join(PACKAGE_ROOT, "skills"); const WORKFLOWS_SRC = path.join(PACKAGE_ROOT, "templates", "workflows"); const ACTIONS_SRC = path.join(PACKAGE_ROOT, "templates", "actions"); export interface CopyResult { copied: number; removed: number; } function copyDirectoryRecursive(srcDir: string, destDir: string): number { let copied = 0; for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { const srcPath = path.join(srcDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { copied += copyDirectoryRecursive(srcPath, destPath); } else { fs.mkdirSync(destDir, { recursive: true }); fs.copyFileSync(srcPath, destPath); copied++; } } return copied; } // Replace erp-kit-* skills in destDir with the current ones, leaving custom skills untouched. export function copySkills(destDir: string): CopyResult { let removed = 0; if (fs.existsSync(destDir)) { for (const entry of fs.readdirSync(destDir)) { if (entry.startsWith("erp-kit-")) { fs.rmSync(path.join(destDir, entry), { recursive: true, force: true }); removed++; } } } let copied = 0; for (const entry of fs.readdirSync(SKILLS_SRC)) { fs.cpSync(path.join(SKILLS_SRC, entry), path.join(destDir, entry), { recursive: true }); copied++; } return { copied, removed }; } export function copyWorkflows(cwd: string): CopyResult { const workflowsDest = path.join(cwd, ".github", "workflows"); if (!fs.existsSync(WORKFLOWS_SRC)) return { copied: 0, removed: 0 }; const workflowFiles = fs .readdirSync(WORKFLOWS_SRC) .filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")); const sourceNames = new Set(workflowFiles); let removed = 0; if (fs.existsSync(workflowsDest)) { for (const entry of fs.readdirSync(workflowsDest)) { if (entry.startsWith("erp-kit-") && !sourceNames.has(entry)) { fs.rmSync(path.join(workflowsDest, entry), { force: true }); removed++; } } } if (workflowFiles.length === 0) return { copied: 0, removed }; fs.mkdirSync(workflowsDest, { recursive: true }); let copied = 0; for (const file of workflowFiles) { fs.copyFileSync(path.join(WORKFLOWS_SRC, file), path.join(workflowsDest, file)); copied++; } return { copied, removed }; } export function copyActions(cwd: string): CopyResult { const actionsDest = path.join(cwd, ".github", "actions"); if (!fs.existsSync(ACTIONS_SRC)) return { copied: 0, removed: 0 }; const actionDirs = fs .readdirSync(ACTIONS_SRC, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.startsWith("erp-kit-")); const sourceNames = new Set(actionDirs.map((d) => d.name)); let removed = 0; if (fs.existsSync(actionsDest)) { for (const entry of fs.readdirSync(actionsDest)) { if (entry.startsWith("erp-kit-") && !sourceNames.has(entry)) { fs.rmSync(path.join(actionsDest, entry), { recursive: true, force: true }); removed++; } } } let copied = 0; for (const dir of actionDirs) { const srcDir = path.join(ACTIONS_SRC, dir.name); const destDir = path.join(actionsDest, dir.name); copied += copyDirectoryRecursive(srcDir, destDir); } return { copied, removed }; } export type SymlinkResult = "linked" | "already_linked" | "skipped_directory" | "copied"; function createSymlink(target: string, linkPath: string): void { const type = process.platform === "win32" ? "junction" : undefined; // Junctions on Windows require absolute targets const resolvedTarget = type === "junction" ? path.resolve(path.dirname(linkPath), target) : target; fs.symlinkSync(resolvedTarget, linkPath, type); } export function setupSymlink(cwd: string): SymlinkResult { const skillsDest = path.join(cwd, ".agents", "skills"); const claudeSkills = path.join(cwd, ".claude", "skills"); const relTarget = path.relative(path.dirname(claudeSkills), skillsDest); let exists = false; try { fs.lstatSync(claudeSkills); exists = true; } catch { // doesn't exist } if (exists) { const stat = fs.lstatSync(claudeSkills); if (stat.isSymbolicLink()) { const current = fs.readlinkSync(claudeSkills); if (current === relTarget) return "already_linked"; fs.unlinkSync(claudeSkills); try { createSymlink(relTarget, claudeSkills); return "linked"; } catch { copySkills(claudeSkills); return "copied"; } } return "skipped_directory"; } fs.mkdirSync(path.dirname(claudeSkills), { recursive: true }); try { createSymlink(relTarget, claudeSkills); return "linked"; } catch { copySkills(claudeSkills); return "copied"; } } /** * When .claude/skills is a real directory (copy fallback on Windows), * re-sync it with .agents/skills after an update. */ export function refreshCopiedSkills(cwd: string): boolean { const claudeSkills = path.join(cwd, ".claude", "skills"); try { const stat = fs.lstatSync(claudeSkills); if (stat.isSymbolicLink() || !stat.isDirectory()) return false; } catch { return false; } copySkills(claudeSkills); return true; } export function isAlreadyInitialized(cwd: string): boolean { const skillsDest = path.join(cwd, ".agents", "skills"); if (!fs.existsSync(skillsDest)) return false; return fs.readdirSync(skillsDest).some((entry) => entry.startsWith("erp-kit-")); }