import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { CLIENTS, SKILLS, buildOperationPlan, detectInstalledSkills, getSkillSource, piPackageCommand, parseReviewerShorthand, validateRemoveTarget, } from "../lib/skill-manager-core.mjs"; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); test("manifest records supported variants and helper allowlists", () => { assert.deepEqual(SKILLS["web-automation"].variants, ["codex", "claude-code", "opencode", "pi"]); assert.equal(SKILLS["web-automation"].variants.includes("cursor"), false); assert.deepEqual(CLIENTS.codex.reviewerRuntime.files, ["run-review.sh", "notify-telegram.sh"]); assert.deepEqual(CLIENTS.pi.reviewerRuntime.files, ["run-review.sh", "notify-telegram.sh"]); }); test("parseReviewerShorthand keeps provider-qualified model ids verbatim", () => { assert.deepEqual(parseReviewerShorthand("pi/claude-opus-4-7"), { reviewerCli: "pi", reviewerModel: "claude-opus-4-7", }); assert.deepEqual(parseReviewerShorthand("pi/anthropic/claude-opus-4-7"), { reviewerCli: "pi", reviewerModel: "anthropic/claude-opus-4-7", }); assert.equal(parseReviewerShorthand("codex/gpt-5"), null); }); test("unsupported skill variant is reported as unsupported", () => { assert.equal(getSkillSource("web-automation", "cursor"), null); assert.ok(getSkillSource("web-automation", "pi").endsWith("pi-package/skills/web-automation")); }); test("detectInstalledSkills reports installed and missing skills", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-detect-")); try { await mkdir(path.join(dir, "skills", "create-plan"), { recursive: true }); await writeFile(path.join(dir, "skills", "create-plan", "SKILL.md"), "---\nname: create-plan\n---\n"); const state = await detectInstalledSkills({ skillsRoot: path.join(dir, "skills"), clientId: "codex" }); assert.equal(state["create-plan"].state, "installed"); assert.equal(state["web-automation"].state, "not-installed"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("plan install workflow skill includes reviewer-runtime and missing Superpowers prompt", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-plan-")); try { await mkdir(path.join(dir, "repo", "skills", "create-plan", "codex"), { recursive: true }); await writeFile(path.join(dir, "repo", "skills", "create-plan", "codex", "SKILL.md"), "---\nname: create-plan\n---\n"); const plan = await buildOperationPlan({ selections: [{ clientId: "codex", scope: "global", skillsRoot: path.join(dir, "install"), actions: { "create-plan": "install" } }], assumeYes: true, repoRoot: path.join(dir, "repo"), superpowersByClient: { codex: [] }, }); assert.equal(plan.operations.some((op) => op.kind === "skill" && op.skill === "create-plan" && op.action === "install"), true); assert.equal(plan.operations.some((op) => op.kind === "helper" && op.helper === "reviewer-runtime"), true); assert.equal(plan.prompts.some((prompt) => prompt.kind === "missing-superpowers" && prompt.clientId === "codex"), true); } finally { await rm(dir, { recursive: true, force: true }); } }); test("plan removing last workflow skill prompts for optional Superpowers removal", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-remove-")); try { await mkdir(path.join(dir, "skills", "do-task"), { recursive: true }); await writeFile(path.join(dir, "skills", "do-task", "SKILL.md"), "---\nname: do-task\n---\n"); const plan = await buildOperationPlan({ selections: [{ clientId: "codex", scope: "global", skillsRoot: path.join(dir, "skills"), actions: { "do-task": "remove" } }], assumeYes: true, repoRoot: process.cwd(), }); assert.equal(plan.prompts.some((prompt) => prompt.kind === "remove-superpowers" && prompt.clientId === "codex"), true); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package mode plans full package install instead of per-skill copy", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-")); try { const plan = await buildOperationPlan({ selections: [{ clientId: "pi", scope: "packageLocal", action: "install", actions: { "create-plan": "skip", atlassian: "remove" } }], repoRoot: dir, }); assert.equal(plan.operations.some((op) => op.kind === "pi-package" && op.piInstallArg === "-l"), true); assert.equal(plan.operations.some((op) => op.kind === "skill"), false); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package mode surfaces bundled skills and skips already installed package resources", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-installed-")); try { const repo = path.join(dir, "repo"); await mkdir(path.join(repo, ".pi"), { recursive: true }); await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] })); for (const skill of Object.keys(SKILLS)) { await mkdir(path.join(repo, "pi-package", "skills", skill), { recursive: true }); } await mkdir(path.join(repo, "pi-package", "skills", "atlassian", "scripts", "node_modules"), { recursive: true }); await mkdir(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin"), { recursive: true }); await writeFile(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin", "cloakbrowser"), ""); const plan = await buildOperationPlan({ selections: [{ clientId: "pi", scope: "packageLocal", action: "install", actions: {} }], repoRoot: repo, }); const packageInstall = plan.operations.find((op) => op.kind === "pi-package"); assert.equal(packageInstall.status, "skipped"); assert.match(packageInstall.details, /already installed/); assert.deepEqual( plan.reportRows.filter((row) => row.action === "included").map((row) => row.item).sort(), Object.keys(SKILLS).sort() ); assert.deepEqual( plan.reportRows.filter((row) => row.action === "bootstrap-deps").map((row) => [row.item, row.status]).sort(), [["atlassian", "skipped"], ["web-automation", "skipped"]] ); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package mode remove skips when package is not installed", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-remove-")); try { const plan = await buildOperationPlan({ selections: [{ clientId: "pi", scope: "packageLocal", action: "remove", actions: {} }], repoRoot: dir, }); assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]); assert.equal(plan.operations[0].action, "remove"); assert.equal(plan.operations[0].status, "skipped"); assert.match(plan.operations[0].details, /not installed/); assert.equal(plan.reportRows[0].item, "pi-package"); assert.equal(plan.reportRows[0].action, "remove"); assert.equal(plan.reportRows[0].status, "skipped"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package mode remove plans removal when package is installed", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-remove-installed-")); try { const repo = path.join(dir, "repo"); await mkdir(path.join(repo, ".pi"), { recursive: true }); await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] })); const plan = await buildOperationPlan({ selections: [{ clientId: "pi", scope: "packageLocal", action: "remove", actions: {} }], repoRoot: repo, }); assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]); assert.equal(plan.operations[0].action, "remove"); assert.equal(plan.operations[0].status, undefined); assert.equal(plan.reportRows[0].status, "planned"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package mode update syncs and forces package reinstall plus dependency bootstrap", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-update-")); try { const repo = path.join(dir, "repo"); await mkdir(path.join(repo, ".pi"), { recursive: true }); await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] })); await mkdir(path.join(repo, "pi-package", "skills", "atlassian", "scripts", "node_modules"), { recursive: true }); await mkdir(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin"), { recursive: true }); await writeFile(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin", "cloakbrowser"), ""); const plan = await buildOperationPlan({ selections: [{ clientId: "pi", scope: "packageLocal", action: "update", actions: {} }], repoRoot: repo, }); assert.equal(plan.operations[0].kind, "sync-pi-package"); const packageUpdate = plan.operations.find((op) => op.kind === "pi-package"); assert.equal(packageUpdate.action, "update"); assert.equal(packageUpdate.status, undefined); assert.deepEqual( plan.reportRows.filter((row) => row.action === "bootstrap-deps").map((row) => [row.item, row.status]).sort(), [["atlassian", "planned"], ["web-automation", "planned"]] ); } finally { await rm(dir, { recursive: true, force: true }); } }); test("pi package command helper builds exact install and remove argv", () => { assert.deepEqual(piPackageCommand({ action: "install", repoRoot: "/repo", piInstallArg: "" }), ["pi", ["install", "/repo"]]); assert.deepEqual(piPackageCommand({ action: "update", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["install", "-l", "/repo"]]); assert.deepEqual(piPackageCommand({ action: "reinstall", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["install", "-l", "/repo"]]); assert.deepEqual(piPackageCommand({ action: "remove", repoRoot: "/repo", piInstallArg: "" }), ["pi", ["remove", "/repo"]]); assert.deepEqual(piPackageCommand({ action: "remove", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["remove", "-l", "/repo"]]); }); test("cli package mode preserves package action and ignores skill narrowing", () => { const output = execFileSync(process.execPath, [ path.join(REPO_ROOT, "scripts", "manage-skills.mjs"), "--client", "pi", "--scope", "packageGlobal", "--pi-package", "--skill", "create-plan", "--action", "remove", "--plan-only", ], { cwd: REPO_ROOT, encoding: "utf8" }); const plan = JSON.parse(output); assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]); assert.equal(plan.operations[0].action, "remove"); }); test("validateRemoveTarget rejects paths outside the manifest root", async () => { const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-safe-")); try { await mkdir(path.join(dir, "skills", "create-plan"), { recursive: true }); await mkdir(path.join(dir, "outside"), { recursive: true }); assert.equal(await validateRemoveTarget(path.join(dir, "skills", "create-plan"), path.join(dir, "skills")), true); await assert.rejects(() => validateRemoveTarget(path.join(dir, "outside"), path.join(dir, "skills")), /outside skills root/); await assert.rejects(() => validateRemoveTarget(dir, dir), /refusing to remove skills root itself/); } finally { await rm(dir, { recursive: true, force: true }); } });