feat(artifacts): storefront layout and Uninstall for installed skills/plugins
- Artifacts page laid out like an app store: hero search, category chips, shelves by kind, app-icon cards with one primary pill and a detail sheet - POST /api/artifacts/:id/uninstall: a skill's copies in ~/.claude/skills and ~/.agents/skills are moved to ~/.claude/agenthub-replaced-skills/ (-uninstalled-<time>), never deleted; a plugin runs `claude plugin uninstall --scope user --keep-data` as the caller; ENOTINSTALLED when nothing is there; allowed while installs are off - Uninstall on the card, the ⋯ menu and the detail sheet, after a confirm
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* claude.ai pages (link allowlist, hook capture, one row per URL), workspace
|
||||
* file candidates and pins (a snapshot, served sandboxed), skills (snapshot,
|
||||
* symlink/secret refusal, install into the caller's ~/.claude and ~/.agents skills with
|
||||
* replace moving the old one aside), plugins (marketplace allowlist, install
|
||||
* replace and uninstall moving the old one aside), plugins (marketplace allowlist, install
|
||||
* argv; a marketplace link read into its plugin list, the same plugin added
|
||||
* twice kept as one row, key/market edited), skills uploaded (one Markdown
|
||||
* file, a folder, a zip, several skills at once — name and frontmatter set,
|
||||
@@ -200,6 +200,18 @@ describe("skills", () => {
|
||||
const old = fs.readdirSync(aside).find((n) => !n.includes("-agents-"));
|
||||
assert.equal(fs.readFileSync(path.join(aside, old, "mine.txt"), "utf8"), "keep me");
|
||||
assert.ok(fs.readdirSync(aside).some((n) => n.startsWith("deploy-agents-")));
|
||||
|
||||
// Uninstall moves both copies aside too, never deletes them.
|
||||
const gone = await request("POST", `/api/artifacts/${id}/uninstall`, {});
|
||||
assert.equal(gone.status, 200, gone.text);
|
||||
assert.deepEqual(gone.body.paths, [target, shared]);
|
||||
assert.ok(!fs.existsSync(target) && !fs.existsSync(shared));
|
||||
assert.equal((await listed()).installed, false);
|
||||
const kept = fs.readdirSync(aside).filter((n) => n.includes("-uninstalled-"));
|
||||
assert.equal(kept.length, 2);
|
||||
const none = await request("POST", `/api/artifacts/${id}/uninstall`, {});
|
||||
assert.equal(none.status, 409);
|
||||
assert.equal(none.body.error.code, "ENOTINSTALLED");
|
||||
});
|
||||
|
||||
it("edits a skill's name, description and instructions into a new snapshot", async () => {
|
||||
@@ -271,6 +283,12 @@ describe("plugins", () => {
|
||||
["claude", "plugin", "marketplace", "add", "o/r"],
|
||||
["claude", "plugin", "install", "--scope", "user", "tool@market"],
|
||||
]);
|
||||
calls.length = 0;
|
||||
const un = await request("POST", `/api/artifacts/${r.body.item.id}/uninstall`, {});
|
||||
assert.equal(un.status, 200, un.text);
|
||||
assert.deepEqual(calls, [
|
||||
["claude", "plugin", "uninstall", "--scope", "user", "--keep-data", "tool@market"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user