import { access, cp, lstat, mkdir, readFile, realpath, rm, stat, symlink, chmod, unlink, readdir } from "node:fs/promises"; import { constants, existsSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; export const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../.."); export const CLIENTS = { codex: { id: "codex", label: "Codex", detectCommands: [["codex", "--version"]], scopes: { global: { skillsRoot: "~/.codex/skills" } }, variant: "codex", superpowers: { roots: ["~/.agents/skills/superpowers", "~/.codex/superpowers/skills"], layout: "flat", }, reviewerRuntime: { root: "~/.codex/skills/reviewer-runtime", source: "skills/reviewer-runtime", files: ["run-review.sh", "notify-telegram.sh"], }, }, "claude-code": { id: "claude-code", label: "Claude Code", detectCommands: [["claude", "--version"]], scopes: { global: { skillsRoot: "~/.claude/skills" } }, variant: "claude-code", superpowers: { roots: ["~/.claude/skills/superpowers"], layout: "flat", }, reviewerRuntime: { root: "~/.claude/skills/reviewer-runtime", source: "skills/reviewer-runtime", files: ["run-review.sh", "notify-telegram.sh"], }, }, cursor: { id: "cursor", label: "Cursor", detectCommands: [["cursor-agent", "--version"], ["cursor", "agent", "--version"]], scopes: { local: { skillsRoot: ".cursor/skills" }, global: { skillsRoot: "~/.cursor/skills" }, }, variant: "cursor", superpowers: { roots: [".cursor/skills/superpowers/skills", "~/.cursor/skills/superpowers/skills"], layout: "cursor-nested", targetSuffix: "superpowers/skills", }, reviewerRuntime: { root: "/reviewer-runtime", source: "skills/reviewer-runtime", files: ["run-review.sh", "notify-telegram.sh"], }, }, opencode: { id: "opencode", label: "OpenCode", detectCommands: [["opencode", "--version"]], scopes: { global: { skillsRoot: "~/.config/opencode/skills" } }, variant: "opencode", superpowers: { roots: ["~/.agents/skills/superpowers", "~/.config/opencode/skills/superpowers"], layout: "flat", }, reviewerRuntime: { root: "~/.config/opencode/skills/reviewer-runtime", source: "skills/reviewer-runtime", files: ["run-review.sh", "notify-telegram.sh"], }, }, pi: { id: "pi", label: "Pi", detectCommands: [["pi", "--version"]], scopes: { packageGlobal: { skillsRoot: "~/.pi/agent/skills", packageMode: true, piInstallArg: "" }, packageLocal: { skillsRoot: ".pi/skills", packageMode: true, piInstallArg: "-l" }, global: { skillsRoot: "~/.pi/agent/skills" }, local: { skillsRoot: ".pi/skills" }, }, variant: "pi", superpowers: { roots: ["~/.agents/skills/superpowers", "~/.pi/agent/skills/superpowers", ".pi/skills/superpowers"], layout: "flat", }, reviewerRuntime: { root: "/reviewer-runtime/pi", source: "skills/reviewer-runtime/pi", files: ["run-review.sh", "notify-telegram.sh"], }, }, }; export const SKILLS = { atlassian: { name: "atlassian", variants: ["codex", "claude-code", "cursor", "opencode", "pi"], requiresSuperpowers: [], requiresReviewerRuntime: false, bootstrap: "pnpm-install", bootstrapPrerequisites: ["node>=20", "pnpm|corepack"], auxiliary: { shared: "build-time/shared-not-installed" }, }, "create-plan": { name: "create-plan", variants: ["codex", "claude-code", "cursor", "opencode", "pi"], requiresSuperpowers: ["brainstorming", "writing-plans"], requiresReviewerRuntime: true, }, "do-task": { name: "do-task", variants: ["codex", "claude-code", "cursor", "opencode", "pi"], requiresSuperpowers: ["brainstorming", "test-driven-development", "verification-before-completion", "finishing-a-development-branch"], conditionalSuperpowers: ["using-git-worktrees"], requiresReviewerRuntime: true, requiresNotifier: true, }, "implement-plan": { name: "implement-plan", variants: ["codex", "claude-code", "cursor", "opencode", "pi"], requiresSuperpowers: ["executing-plans", "using-git-worktrees", "verification-before-completion", "finishing-a-development-branch"], requiresReviewerRuntime: true, }, "web-automation": { name: "web-automation", variants: ["codex", "claude-code", "cursor", "opencode", "pi"], requiresSuperpowers: [], requiresReviewerRuntime: false, bootstrap: "web-automation", bootstrapPrerequisites: ["node>=20", "pnpm|corepack", "cloakbrowser"], }, }; export function expandHome(value, cwd = process.cwd(), homeDir = homedir()) { if (!value) return value; let expanded = value.replace(/^~(?=$|\/)/, homeDir); if (!path.isAbsolute(expanded)) expanded = path.resolve(cwd, expanded); return expanded; } export function resolveClientScope(clientId, scope = "global", cwd = process.cwd()) { const client = CLIENTS[clientId]; if (!client) throw new Error(`Unsupported client: ${clientId}`); const scopeConfig = client.scopes[scope]; if (!scopeConfig) throw new Error(`Unsupported scope for ${clientId}: ${scope}`); const skillsRoot = expandHome(scopeConfig.skillsRoot, cwd); return { ...scopeConfig, skillsRoot }; } export function reviewerRuntimeRoot(clientId, skillsRoot, cwd = process.cwd()) { const template = CLIENTS[clientId].reviewerRuntime.root; return expandHome(template.replace("", skillsRoot), cwd); } export function parseReviewerShorthand(value) { if (typeof value !== "string") return null; const slash = value.indexOf("/"); if (slash <= 0) return null; if (value.slice(0, slash) !== "pi") return null; const model = value.slice(slash + 1); if (!model) return null; return { reviewerCli: "pi", reviewerModel: model }; } export function getSkillSource(skillName, clientId, repoRoot = process.cwd()) { const skill = SKILLS[skillName]; const client = CLIENTS[clientId]; if (!skill || !client || !skill.variants.includes(client.variant)) return null; if (clientId === "pi") return path.join(repoRoot, "pi-package", "skills", skillName); return path.join(repoRoot, "skills", skillName, client.variant); } export async function pathExists(candidate) { try { await access(candidate, constants.F_OK); return true; } catch { return false; } } export async function detectInstalledClients({ cwd = process.cwd() } = {}) { const result = {}; for (const [clientId, client] of Object.entries(CLIENTS)) { let confidence = "not-found"; let detail = "not detected"; for (const command of client.detectCommands) { const proc = spawnSync(command[0], command.slice(1), { encoding: "utf8" }); if (proc.status === 0) { confidence = "cli"; detail = (proc.stdout || proc.stderr || "detected").trim().split("\n")[0]; break; } } if (confidence === "not-found") { for (const scopeName of Object.keys(client.scopes)) { const { skillsRoot } = resolveClientScope(clientId, scopeName, cwd); if (await pathExists(skillsRoot)) { confidence = "directory"; detail = skillsRoot; break; } } } result[clientId] = { clientId, label: client.label, confidence, detail }; } return result; } export async function detectInstalledSkills({ skillsRoot, clientId, repoRoot = process.cwd() }) { const state = {}; for (const skillName of Object.keys(SKILLS)) { if (getSkillSource(skillName, clientId) === null) { state[skillName] = { state: "unsupported", path: null }; continue; } const target = path.join(skillsRoot, skillName); const skillMd = path.join(target, "SKILL.md"); let installState = "not-installed"; if (existsSync(skillMd)) { installState = "installed"; const source = getSkillSource(skillName, clientId, repoRoot); const sourceSkill = source && path.join(source, "SKILL.md"); if (sourceSkill && existsSync(sourceSkill)) { const [targetStat, sourceStat] = await Promise.all([stat(skillMd), stat(sourceSkill)]); if (sourceStat.mtimeMs > targetStat.mtimeMs + 1000) installState = "stale"; } else { installState = "unknown"; } } state[skillName] = { state: installState, path: target, }; } return state; } export async function findInstalledSuperpowers(clientId, cwd = process.cwd(), { homeDir = homedir() } = {}) { const client = CLIENTS[clientId]; const found = new Set(); for (const root of client.superpowers.roots) { const expanded = expandHome(root, cwd, homeDir); if (await pathExists(expanded)) found.add(expanded); } if (clientId === "claude-code") { for (const root of await findClaudeCodeSuperpowersPluginRoots(homeDir)) found.add(root); } if (clientId === "cursor") { for (const root of await findCursorSuperpowersPluginRoots(homeDir)) found.add(root); } return [...found]; } async function findClaudeCodeSuperpowersPluginRoots(homeDir) { const pluginId = "superpowers@claude-plugins-official"; const settings = await readJsonIfExists(path.join(homeDir, ".claude", "settings.json")); if (settings?.enabledPlugins?.[pluginId] !== true) return []; const installed = await readJsonIfExists(path.join(homeDir, ".claude", "plugins", "installed_plugins.json")); const entries = installed?.plugins?.[pluginId]; if (!Array.isArray(entries)) return []; const roots = []; for (const entry of entries) { if (!entry?.installPath) continue; const skillsRoot = path.join(entry.installPath, "skills"); if (await pathExists(skillsRoot)) roots.push(skillsRoot); } return roots; } async function findCursorSuperpowersPluginRoots(homeDir) { const pluginRoot = path.join(homeDir, ".cursor", "plugins", "cache", "cursor-public", "superpowers"); let entries; try { entries = await readdir(pluginRoot, { withFileTypes: true }); } catch (error) { if (error?.code === "ENOENT") return []; throw error; } const roots = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const skillsRoot = path.join(pluginRoot, entry.name, "skills"); if (await pathExists(skillsRoot)) roots.push(skillsRoot); } return roots.sort(); } function piPackageSettingsPath(scope, repoRoot) { if (scope === "packageLocal") return path.join(repoRoot, ".pi", "settings.json"); return path.join(homedir(), ".pi", "agent", "settings.json"); } async function readJsonIfExists(filePath) { try { return JSON.parse(await readFile(filePath, "utf8")); } catch (error) { if (error?.code === "ENOENT") return null; throw error; } } export async function isPiPackageInstalled({ scope, repoRoot }) { const settingsPath = piPackageSettingsPath(scope, repoRoot); const settings = await readJsonIfExists(settingsPath); if (!Array.isArray(settings?.packages)) return false; const settingsDir = path.dirname(settingsPath); const expected = path.resolve(repoRoot); return settings.packages.some((packagePath) => path.resolve(settingsDir, packagePath) === expected); } export function isBootstrapInstalled(action, target) { const scriptsDir = path.join(target, "scripts"); if (action === "pnpm-install") return existsSync(path.join(scriptsDir, "node_modules")); if (action === "web-automation") { return existsSync(path.join(scriptsDir, "node_modules")) && existsSync(path.join(scriptsDir, "node_modules", ".bin", "cloakbrowser")); } return false; } export async function detectHelperInstallState({ source, target, files }) { let differs = false; for (const file of files) { const sourceFile = path.join(source, file); const targetFile = path.join(target, file); if (!existsSync(targetFile)) return "not-installed"; if (!existsSync(sourceFile)) return "unknown"; const [sourceContents, targetContents] = await Promise.all([readFile(sourceFile), readFile(targetFile)]); if (!sourceContents.equals(targetContents)) differs = true; } return differs ? "stale" : "installed"; } export function piPackageCommand({ action, repoRoot, piInstallArg = "" }) { if (["install", "update", "reinstall"].includes(action)) { const args = ["install"]; if (piInstallArg) args.push(piInstallArg); args.push(repoRoot); return ["pi", args]; } if (action === "remove") { const args = ["remove"]; if (piInstallArg) args.push(piInstallArg); args.push(repoRoot); return ["pi", args]; } throw new Error(`pi package mode does not support action: ${action}`); } export function requiredSuperpowersFor(actions) { const required = new Set(); for (const [skillName, action] of Object.entries(actions || {})) { if (!["install", "update", "reinstall"].includes(action)) continue; for (const skill of SKILLS[skillName]?.requiresSuperpowers || []) required.add(skill); } return [...required]; } async function buildReviewerRuntimeOperation({ clientId, scope, skillsRoot, repoRoot, action = null }) { if (action === "skip") return null; const helperTarget = reviewerRuntimeRoot(clientId, skillsRoot, repoRoot); const helperSource = path.join(repoRoot, CLIENTS[clientId].reviewerRuntime.source); const helperFiles = CLIENTS[clientId].reviewerRuntime.files; const helperState = await detectHelperInstallState({ source: helperSource, target: helperTarget, files: helperFiles }); const effectiveAction = action || (helperState === "stale" || helperState === "unknown" ? "update" : "install"); const operation = { kind: "helper", helper: "reviewer-runtime", clientId, scope, action: effectiveAction, source: helperSource, target: helperTarget, files: helperFiles, skillsRoot, }; if (!action && helperState === "installed") { operation.status = "skipped"; operation.details = "runtime helper already installed"; } return operation; } export async function buildOperationPlan({ selections, repoRoot = process.cwd(), assumeYes = false, superpowersByClient = null }) { const operations = []; const prompts = []; const reportRows = []; const plannedHelpers = new Set(); for (const selection of selections || []) { const { clientId, scope = "global" } = selection; const client = CLIENTS[clientId]; if (!client) throw new Error(`Unsupported client: ${clientId}`); const scopeInfo = selection.skillsRoot ? { skillsRoot: selection.skillsRoot } : resolveClientScope(clientId, scope, repoRoot); const skillsRoot = scopeInfo.skillsRoot; if (clientId === "pi" && scopeInfo.packageMode) { const action = selection.action || "install"; if (action === "skip") continue; if (["update", "reinstall"].includes(action)) { operations.push({ kind: "sync-pi-package", clientId, scope, action: "sync", repoRoot }); } const packageInstalled = await isPiPackageInstalled({ scope, repoRoot }); if (action === "remove") { operations.push({ kind: "pi-package", clientId, scope, action, repoRoot, piInstallArg: scopeInfo.piInstallArg || "", status: packageInstalled ? undefined : "skipped", details: packageInstalled ? "" : "not installed in Pi settings", }); continue; } operations.push({ kind: "pi-package", clientId, scope, action, repoRoot, piInstallArg: scopeInfo.piInstallArg || "", status: packageInstalled && action === "install" ? "skipped" : undefined, details: packageInstalled && action === "install" ? "already installed in Pi settings" : "", }); for (const skillName of Object.keys(SKILLS)) { if (getSkillSource(skillName, clientId, repoRoot)) { operations.push({ kind: "package-skill", clientId, scope, skill: skillName, action: "included", status: "included", details: "included in Pi package" }); } } for (const bootstrap of [ { skill: "atlassian", action: "pnpm-install" }, { skill: "web-automation", action: "web-automation" }, ]) { const target = path.join(repoRoot, "pi-package", "skills", bootstrap.skill); const installed = isBootstrapInstalled(bootstrap.action, target); const skipBootstrap = action === "install" && installed; operations.push({ kind: "bootstrap", clientId, scope, skill: bootstrap.skill, action: bootstrap.action, displayAction: "bootstrap-deps", target, status: skipBootstrap ? "skipped" : undefined, details: skipBootstrap ? "runtime dependencies already installed" : target, }); } continue; } const installed = await detectInstalledSkills({ skillsRoot, clientId, repoRoot }); const actions = selection.actions || {}; const helperActions = selection.helperActions || {}; const missingSuperpowers = requiredSuperpowersFor(actions); if (missingSuperpowers.length > 0) { const installedSuperpowers = superpowersByClient && Object.hasOwn(superpowersByClient, clientId) ? superpowersByClient[clientId] : await findInstalledSuperpowers(clientId, repoRoot); if (installedSuperpowers.length === 0) { prompts.push({ kind: "missing-superpowers", clientId, scope, required: missingSuperpowers }); } } for (const [helper, action] of Object.entries(helperActions)) { if (helper !== "reviewer-runtime") continue; const helperTarget = reviewerRuntimeRoot(clientId, skillsRoot, repoRoot); const helperKey = `${clientId}\0${scope}\0${helperTarget}`; if (plannedHelpers.has(helperKey)) continue; plannedHelpers.add(helperKey); const helperOperation = await buildReviewerRuntimeOperation({ clientId, scope, skillsRoot, repoRoot, action }); if (helperOperation) operations.push(helperOperation); } for (const [skillName, action] of Object.entries(actions)) { const source = getSkillSource(skillName, clientId, repoRoot); if (!source) { operations.push({ kind: "skill", clientId, scope, skill: skillName, action: "unsupported", status: "skipped", details: "variant unavailable" }); continue; } if (action === "skip") continue; const target = path.join(skillsRoot, skillName); if (clientId === "pi" && !operations.some((op) => op.kind === "sync-pi-package")) { operations.push({ kind: "sync-pi-package", clientId, scope, action: "sync", repoRoot }); } operations.push({ kind: "skill", clientId, scope, skill: skillName, action, source, target, skillsRoot }); if (["install", "update", "reinstall"].includes(action) && SKILLS[skillName].requiresReviewerRuntime) { const helperTarget = reviewerRuntimeRoot(clientId, skillsRoot, repoRoot); const helperKey = `${clientId}\0${scope}\0${helperTarget}`; if (!plannedHelpers.has(helperKey)) { plannedHelpers.add(helperKey); const helperOperation = await buildReviewerRuntimeOperation({ clientId, scope, skillsRoot, repoRoot }); if (helperOperation) operations.push(helperOperation); } } if (["install", "update", "reinstall"].includes(action) && SKILLS[skillName].bootstrap) { operations.push({ kind: "bootstrap", clientId, scope, skill: skillName, item: `${skillName} deps`, action: SKILLS[skillName].bootstrap, displayAction: "bootstrap-deps", target }); } } const workflowSkills = Object.keys(SKILLS).filter((skillName) => (SKILLS[skillName].requiresSuperpowers || []).length > 0); const removing = workflowSkills.filter((skillName) => actions[skillName] === "remove"); if (removing.length > 0) { const remaining = workflowSkills.some((skillName) => actions[skillName] !== "remove" && installed[skillName]?.state === "installed"); if (!remaining) prompts.push({ kind: "remove-superpowers", clientId, scope, removedWorkflowSkills: removing }); } } for (const op of operations) { reportRows.push({ client: op.clientId, scope: op.scope, item: op.item || op.skill || op.helper || op.kind, action: op.displayAction || op.action, status: op.status || "planned", details: op.details || op.target || "", }); } return { operations, prompts, reportRows, assumeYes }; } export async function validateRemoveTarget(target, skillsRoot, { repoRoot = process.cwd() } = {}) { const resolvedRoot = path.resolve(skillsRoot); const resolvedTarget = path.resolve(target); const home = path.resolve(homedir()); const resolvedRepo = path.resolve(repoRoot); if (resolvedTarget === resolvedRoot) throw new Error("refusing to remove skills root itself"); if (resolvedTarget === home) throw new Error("refusing to remove home directory"); if (resolvedTarget === resolvedRepo) throw new Error("refusing to remove repository root"); if (resolvedTarget === path.parse(resolvedTarget).root) throw new Error("refusing to remove filesystem root"); const relative = path.relative(resolvedRoot, resolvedTarget); if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") { throw new Error(`refusing to remove target outside skills root: ${target}`); } try { const info = await lstat(resolvedTarget); if (!info.isSymbolicLink()) { const [realRoot, realTarget] = await Promise.all([realpath(resolvedRoot), realpath(resolvedTarget)]); const realRelative = path.relative(realRoot, realTarget); if (realRelative.startsWith("..") || path.isAbsolute(realRelative) || realRelative === "") { throw new Error(`refusing to remove real target outside skills root: ${target}`); } } } catch (error) { if (error && error.code !== "ENOENT") throw error; } return true; } export async function copyDirectoryReplacing(source, target) { await mkdir(path.dirname(target), { recursive: true }); await rm(target, { recursive: true, force: true }); await cp(source, target, { recursive: true, force: true, dereference: false }); } export async function installHelperAllowlist({ source, target, files }) { await mkdir(target, { recursive: true }); for (const file of files) { await cp(path.join(source, file), path.join(target, file), { force: true }); if (file.endsWith(".sh")) await chmod(path.join(target, file), 0o755); } } function runCommand(command, args, options = {}) { const result = spawnSync(command, args, { encoding: "utf8", stdio: "pipe", ...options }); if (result.status !== 0) { const detail = (result.stderr || result.stdout || `${command} exited ${result.status}`).trim(); throw new Error(detail); } return (result.stdout || result.stderr || "ok").trim(); } function resolvePnpmCommand() { if (spawnSync("pnpm", ["--version"], { encoding: "utf8" }).status === 0) return ["pnpm"]; if (spawnSync("corepack", ["--version"], { encoding: "utf8" }).status === 0) return ["corepack", "pnpm"]; throw new Error("missing prerequisite: pnpm or corepack"); } function requireNode20() { const major = Number.parseInt(process.versions.node.split(".")[0], 10); if (major < 20) throw new Error(`missing prerequisite: Node.js 20+ (found ${process.version})`); } export async function executeOperation(op) { if (op.action === "unsupported" || op.status === "skipped") return { ...op, status: "skipped" }; if (op.kind === "package-skill") return { ...op, status: "included" }; if (op.kind === "sync-pi-package") { // Use the canonical generator (pnpm run sync:pi / node scripts/generate-skills.mjs). // The legacy sync-pi-package-skills.sh is retired in M3; it bypassed the // generator and copied skills/*/pi into pi-package directly, corrupting manifests. runCommand(process.execPath, [path.join(op.repoRoot, "scripts", "generate-skills.mjs")], { cwd: op.repoRoot }); return { ...op, status: "ok" }; } if (op.kind === "pi-package") { const [command, args] = piPackageCommand(op); runCommand(command, args, { cwd: op.repoRoot }); return { ...op, status: "ok" }; } if (op.kind === "skill") { if (op.action === "remove") { await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT }); const info = existsSync(op.target) ? await lstat(op.target) : null; if (info?.isSymbolicLink()) await unlink(op.target); else await rm(op.target, { recursive: true, force: true }); return { ...op, status: "ok" }; } await copyDirectoryReplacing(op.source, op.target); return { ...op, status: "ok" }; } if (op.kind === "helper") { if (op.action === "remove") { await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT }); const info = existsSync(op.target) ? await lstat(op.target) : null; if (info?.isSymbolicLink()) await unlink(op.target); else await rm(op.target, { recursive: true, force: true }); return { ...op, status: "ok" }; } await installHelperAllowlist(op); return { ...op, status: "ok" }; } if (op.kind === "superpowers") { await mkdir(path.dirname(op.target), { recursive: true }); if (op.action === "remove") { await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT }); await rm(op.target, { recursive: true, force: true }); } else if (op.mode === "copy") { await copyDirectoryReplacing(op.source, op.target); } else { await rm(op.target, { recursive: true, force: true }); await symlink(op.source, op.target, "dir"); } return { ...op, status: "ok" }; } if (op.kind === "bootstrap") { requireNode20(); const pnpm = resolvePnpmCommand(); const scriptsDir = path.join(op.target, "scripts"); if (op.action === "pnpm-install") { runCommand(pnpm[0], [...pnpm.slice(1), "install", "--frozen-lockfile", "--dir", scriptsDir]); } else if (op.action === "web-automation") { runCommand(pnpm[0], [...pnpm.slice(1), "install", "--frozen-lockfile", "--dir", scriptsDir]); runCommand(pnpm[0], [...pnpm.slice(1), "--dir", scriptsDir, "exec", "cloakbrowser", "install"]); runCommand(pnpm[0], [...pnpm.slice(1), "rebuild", "--dir", scriptsDir, "better-sqlite3", "esbuild"]); } return { ...op, status: "ok" }; } return { ...op, status: "warning", details: "operation is planned/manual" }; }