Feat/uninstall skills#138
Open
persano wants to merge 3 commits into
Open
Conversation
Adds `-r` / `--remove` mode that lists currently installed skills, highlights them in red, and lets users select which to remove with the existing multi-select UI ([space] to toggle, [enter] to remove). Removal is symmetrical with install: cleans up the canonical .agents/skills/<name> directory, every per-agent symlink/copy (.claude/skills/<name>, .cline/skills/<name>, etc.), the matching skills-lock.json entry, and prunes empty parent directories. When the last skill is removed, the autoskills section of CLAUDE.md is swept too. Implementation: - New `uninstaller.ts` module with `listInstalledSkills`, `uninstallSkill`, and `uninstallAll` (mirroring installer.ts's TTY / verbose / simple modes). - `multiSelect` in ui.ts accepts an optional `confirmLabel` so the bottom hint reads '[enter] remove' instead of 'confirm'. Default is unchanged. - `main.ts` wires the flag, adds the destructive-by-default pre-unchecked selection screen, and reuses `cleanupClaudeMd`. Safety: - Skill names are validated against path separators and traversal. - Pre-unchecks every row — removal is opt-in. - Broken symlinks left by prior failures are still cleaned up. Tests: 14 new tests in tests/uninstaller.test.ts covering listing, removal, lockfile preservation, missing-lockfile fallback, parent- dir pruning, trace messages, broken symlinks, and unsafe-name rejection. Full suite: 369/369 passing.
Adds symmetric checkbox semantics to the default picker: installed skills now start checked, new skills stay checked, and unchecking an installed skill marks it for removal. Confirming with [enter] runs both the install and the remove pass in one go. When the resolved selection involves any removal, the CLI prints "About to install N and remove M skill(s). Continue? [y/N]" and aborts cleanly on anything other than y/Y — destructive moves stay opt-in. Implementation: - New `changes.ts` module with `computeChanges` (pure bucketing of installs / removes / keeps / skips) and `confirmDestructive` (y/N prompt; takes optional input/output streams for tests). Kept in a separate file so the test suite can import the helpers without executing main.ts's top-level CLI bootstrap. - `multiSelect` gains `confirmHintFn`, a per-render callback that swaps the trailing `(checked/total)` counter for a richer label. The default picker uses it to show `— install N, remove M` whenever any installed row is unchecked, and falls back to the existing counter otherwise. - `selectSkills` now returns the per-skill checked state instead of the filtered list; `main()` calls `computeChanges` + `confirmDestructive` and dispatches to `installAll` and/or `uninstallAll` as needed, then routes through `printSummary` / `printRemoveSummary` / a new `printCombinedSummary` depending on what ran. - `-y` keeps its existing behaviour: it installs everything detected (installs ∪ keeps) and skips the prompt; no removals happen in -y mode — use `-r -y` for unattended removal. `-r` / `--remove` is unchanged and remains the way to skip detection. Tests: 16 new tests in tests/changes.test.ts covering the bucketing (pure install / pure remove / mixed / keeps-not-installs / empty / length mismatch) and the prompt (y, Y, n, bare newline, non-y input, autoYes shortcut, no-removals shortcut, summary line format, singular/plural). Full suite: 385/385 passing. Docs: help text and both READMEs note that removal now also works from the default picker.
The default picker built its remove list from `SkillEntry.skill`, which is the full `author/repo/skillName` registry path. `uninstallSkill` rejects any name with a path separator and only operates on the bare last segment (the same key used in `skills-lock.json`). Effect before this fix: any removal triggered from the default picker failed with `invalid skill name: <author>/<repo>/<name>`. The dedicated `-r` flow was unaffected because `listInstalledSkills` reads keys straight from the lockfile, which are already bare names. `computeChanges` now extracts the bare name with `parseSkillPath().skillName` before pushing into `removes`, matching the contract used by `installer.ts` when it writes the lockfile and by `-r`'s flow. Raw URL skills (no parseable name) are dropped from `removes` rather than poisoning the call with an empty string. Tests: 2 new regression cases — one for the registry-shape path (`sickn33/antigravity-awesome-skills/nodejs-best-practices` → `nodejs-best-practices`) and one for URL-style skills (dropped). Existing `installs`/`keeps` assertions updated to reflect that those buckets still carry the full path (installAll needs it). 387/387 passing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two-way skill management to the autoskills CLI. The default picker now handles both install and uninstall in a single pass: installed skills are pre-checked, unchecking one marks it for removal, and a
y/Nconfirmation runs before any destructive action. A new-r/--removeflag is also added for scripted removal and users who want a dedicated remove flow.Removal is symmetrical with install: cleans up the canonical
.agents/skills/<name>directory, every per-agent symlink/copy (.claude/skills/<name>,.cline/skills/<name>, etc.), the matchingskills-lock.jsonentry, and prunes empty parent directories. When the last skill is removed, theautoskillssection ofCLAUDE.mdis swept too.Why
Today the only way to remove a skill is to manually delete its directory, each per-agent symlink, the
skills-lock.jsonentry, and (if present) theCLAUDE.mdsection. Easy to miss something. Re-running the installer to "deselect" doesn't help either — pre-checked installed skills don't have any removal pathway.How it works now
Default picker (
npx autoskills):[enter]confirms both. The bottom help line reads— install N, remove Mwhen both apply,— remove Mfor pure-remove, and falls back to the existing(checked/total)counter when nothing will be removed.About to install N and remove M skill(s). Continue? [y/N]. DefaultN. Anything other thany/Yaborts cleanly with no changes.-r/--removeflag:[space]to toggle,[enter]to remove.-r -yremoves everything non-interactively) and when you want an explicit remove-only flow.Behavior changes worth flagging for reviewers
npx autoskillswould re-run the install for every checked row regardless of state; now untouched installed rows are no-ops.-ystill installs everything detected (installs ∪ keeps), preserving today's-ybehavior exactly.Commits
feat(cli): add interactive --remove flag to uninstall skills— the-rflag,uninstaller.tsmodule, lockfile cleanup, parent-dir pruning,multiSelectconfirmLabeloption inui.ts.feat(cli): handle removals in the default install picker— the symmetric checkbox flow.Kept as two commits for reviewability.
What's included
uninstaller.ts—listInstalledSkills,uninstallSkill,uninstallAll(mirrorsinstaller.tsstructure including TTY/verbose/simple render modes).changes.ts—computeChanges(pure bucketing into installs / removes / keeps / skips) andconfirmDestructive(y/N prompt with injectable input/output streams for testability). Lives in its own module because importing helpers frommain.tsfrom a test triggers its top-levelmain()and aborts the test process — putting them in their own file is the cleaner fix.ui.ts—multiSelectgains optionalconfirmLabel(used by-r) and optionalconfirmHintFn(used by the default picker to render the— install N, remove Msuffix). Existing callers are unaffected.main.ts—selectSkillsreturns the boolean state array;main()callscomputeChangesand dispatches toinstallAlland/oruninstallAll; newprintCombinedSummaryfor the mixed case. Help text updated.packages/autoskills/README.mddocument removal from the default picker and the-rflag.Safety
..traversal before any filesystem operation.lstatSync).-rmode pre-unchecks every row — destructive action stays opt-in.-ysemantics are unchanged: still install-only, never removes anything. Use-r -yfor unattended removal.Tests
387/387 passing (369 existing + 18 new in
tests/changes.test.tscovering install-only/remove-only/mixed flows, prompt y/N handling, stream injection, and regression cases for full-path skill identifiers and URL-style entries). Typecheck clean,oxfmt+oxlintclean.Design notes
Three options were on the table for the default picker:
[r]to toggle remove-mode — rejected: just duplicates-rwith worse discoverability, and modal toggles in TUIs invite "which mode am I in" confusion.apt,brew, andnpmall use.-rwas kept rather than deprecated because it's useful for scripts and for users who want a no-ambiguity remove flow without scanning a mixed list.