Skip to content

Feat/uninstall skills#138

Open
persano wants to merge 3 commits into
midudev:mainfrom
persano:feat/uninstall-skills
Open

Feat/uninstall skills#138
persano wants to merge 3 commits into
midudev:mainfrom
persano:feat/uninstall-skills

Conversation

@persano
Copy link
Copy Markdown

@persano persano commented May 18, 2026

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/N confirmation runs before any destructive action. A new -r / --remove flag 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 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.

Why

Today the only way to remove a skill is to manually delete its directory, each per-agent symlink, the skills-lock.json entry, and (if present) the CLAUDE.md section. 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):

  • Installed skills are now pre-checked (previously pre-unchecked).
  • Uncheck an installed skill to mark it for removal.
  • Check a new skill to mark it for install.
  • One [enter] confirms both. The bottom help line reads — install N, remove M when both apply, — remove M for pure-remove, and falls back to the existing (checked/total) counter when nothing will be removed.
  • If any removals are queued, a pre-confirm prompt appears before any filesystem changes: About to install N and remove M skill(s). Continue? [y/N]. Default N. Anything other than y/Y aborts cleanly with no changes.
  • Pure-install runs (no removals) skip the prompt — behavior unchanged for the common case.

-r / --remove flag:

  • Lists every installed skill highlighted in red, all rows pre-unchecked.
  • [space] to toggle, [enter] to remove.
  • Useful for scripts (-r -y removes everything non-interactively) and when you want an explicit remove-only flow.

Behavior changes worth flagging for reviewers

  • Installed skills are now pre-checked in the default picker. Previously they were pre-unchecked. This is required for symmetric semantics.
  • Interactive mode skips re-downloading already-installed skills the user didn't touch. Previously npx autoskills would re-run the install for every checked row regardless of state; now untouched installed rows are no-ops. -y still installs everything detected (installs ∪ keeps), preserving today's -y behavior exactly.

Commits

  1. feat(cli): add interactive --remove flag to uninstall skills — the -r flag, uninstaller.ts module, lockfile cleanup, parent-dir pruning, multiSelect confirmLabel option in ui.ts.
  2. feat(cli): handle removals in the default install picker — the symmetric checkbox flow.

Kept as two commits for reviewability.

What's included

  • New uninstaller.tslistInstalledSkills, uninstallSkill, uninstallAll (mirrors installer.ts structure including TTY/verbose/simple render modes).
  • New changes.tscomputeChanges (pure bucketing into installs / removes / keeps / skips) and confirmDestructive (y/N prompt with injectable input/output streams for testability). Lives in its own module because importing helpers from main.ts from a test triggers its top-level main() and aborts the test process — putting them in their own file is the cleaner fix.
  • ui.tsmultiSelect gains optional confirmLabel (used by -r) and optional confirmHintFn (used by the default picker to render the — install N, remove M suffix). Existing callers are unaffected.
  • main.tsselectSkills returns the boolean state array; main() calls computeChanges and dispatches to installAll and/or uninstallAll; new printCombinedSummary for the mixed case. Help text updated.
  • READMEs — both top-level and packages/autoskills/README.md document removal from the default picker and the -r flag.

Safety

  • Skill names are validated against path separators and .. traversal before any filesystem operation.
  • Pre-confirm prompt before any destructive action when removals are involved.
  • Broken symlinks left over from prior failed installs are still cleaned up (uses lstatSync).
  • -r mode pre-unchecks every row — destructive action stays opt-in.
  • -y semantics are unchanged: still install-only, never removes anything. Use -r -y for unattended removal.

Tests

387/387 passing (369 existing + 18 new in tests/changes.test.ts covering 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 + oxlint clean.

Design notes

Three options were on the table for the default picker:

  • Pure symmetric — uncheck = remove, no prompt. Rejected: one fat-fingered space + enter silently removes work.
  • Modal [r] to toggle remove-mode — rejected: just duplicates -r with worse discoverability, and modal toggles in TUIs invite "which mode am I in" confusion.
  • Symmetric with pre-confirm (this PR) — same mental model as the checkbox visually implies, with a cheap confirmation gate matching the pattern apt, brew, and npm all use.

-r was 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.

Santiago and others added 3 commits May 18, 2026 13:47
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant