feat(installer): support pi package remove and update
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
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,
|
||||
@@ -10,10 +12,13 @@ import {
|
||||
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);
|
||||
@@ -100,6 +105,131 @@ test("pi package mode plans full package install instead of per-skill copy", asy
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user