@spardutti/claude-skills 1.29.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,157 +1,162 @@
1
1
  # Claude Skills
2
2
 
3
- Personal collection of reusable Claude Code **skills**, **slash commands**, and **subagents**. Install them into any project with one command — pick what you want from an interactive menu, and any subagents declared by the commands you pick get installed automatically.
3
+ > An interactive CLI that installs a curated catalog of Claude Code **skills**, **slash commands**, and **subagents** into any project.
4
4
 
5
- ## Skills
5
+ [![npm version](https://img.shields.io/npm/v/@spardutti/claude-skills)](https://www.npmjs.com/package/@spardutti/claude-skills)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@spardutti/claude-skills)](https://www.npmjs.com/package/@spardutti/claude-skills)
7
+ [![license](https://img.shields.io/npm/l/@spardutti/claude-skills)](./LICENSE)
6
8
 
7
- ### Frontend
9
+ Skills are reference playbooks Claude Code loads while it codes — enforcing current best practices for the tools you actually use. This repo is the source catalog; the CLI lets you pick exactly what each project needs from an interactive menu, and pulls in any subagents your chosen commands depend on automatically.
8
10
 
9
- | Skill | Description |
10
- |-------|-------------|
11
- | `react-best-practices` | React 19 — component design, state management, performance, React 19 features, TypeScript integration |
12
- | `react-use-effect` | React 19 useEffect best practices and anti-patterns |
13
- | `react-query` | TanStack React Query with @lukemorales/query-key-factory patterns |
14
- | `react-single-responsibility` | React single responsibility — component splitting, hook isolation, file size limits, complexity rules |
15
- | `tanstack-router-best-practices` | TanStack Router — file-based routing, type-safe navigation, loaders, search params, auth guards |
16
- | `trpc-react-query` | tRPC v11 — queryOptions/mutationOptions patterns, router organization, middleware, cache invalidation, optimistic updates |
17
- | `tailwind-tokens` | Enforce Tailwind CSS design tokens — no arbitrary values when a token exists |
18
- | `zustand` | Zustand — store design, selectors, persist/immer middleware, slices pattern, devtools, transient updates |
19
- | `dnd-kit` | @dnd-kit — sortable lists, sensors, collision detection, drag overlays, multi-container (kanban), accessibility |
20
- | `framer-motion` | Motion (Framer Motion) — AnimatePresence, layout animations, variants, gestures, useAnimate, performance |
11
+ ## Quick Start
21
12
 
22
- ### Desktop
13
+ Run from any project directory:
23
14
 
24
- | Skill | Description |
25
- |-------|-------------|
26
- | `tauri-v2` | Tauri v2 — IPC commands, plugins, window management, system tray, global shortcuts, capabilities/permissions, events |
15
+ ```bash
16
+ npx @spardutti/claude-skills
17
+ ```
27
18
 
28
- ### TypeScript
19
+ ```text
20
+ Claude Skills Installer v2.0.0
29
21
 
30
- | Skill | Description |
31
- |-------|-------------|
32
- | `typescript-best-practices` | TypeScript 5.x — type design, type safety, generics, error handling, tsconfig |
22
+ ── Frontend ──────────────────────────────
23
+ ◉ react ◯ tanstack-query
24
+ tanstack-router
25
+ ── Backend ───────────────────────────────
26
+ ◉ fastapi ◯ docker-best-practices
27
+ ◯ drf-best-practices ◯ drizzle-orm
28
+ ── Database ──────────────────────────────
29
+ ◉ sql
30
+
31
+ ↑↓ move · space select · enter confirm
32
+ ```
33
+
34
+ The CLI will:
35
+
36
+ 1. Fetch the latest skills, commands, and agents from GitHub
37
+ 2. Let you pick skills to install → `.claude/skills/`
38
+ 3. Let you pick commands to install → `.claude/commands/`
39
+ 4. Auto-install any subagents the selected commands declare → `.claude/agents/`
40
+ 5. Optionally set up the **skill-evaluation hook** (recommended — see [How It Works](#how-it-works))
41
+
42
+ ## Skill Catalog
43
+
44
+ **13 skills**, grouped the same way the installer presents them.
45
+
46
+ > [!NOTE]
47
+ > Skills marked **📦 Bundle** ship a concise always-loaded entry point plus reference files Claude reads only when a task needs them — comprehensive coverage at a low context cost.
48
+
49
+ ### Frontend
50
+
51
+ | Skill | What it covers |
52
+ |-------|----------------|
53
+ | `react` 📦 | React 19.2 — `use`, Actions, `ref` as prop, Rules of Hooks, React Compiler v1.0, component splitting, `useEffect` avoidance, performance, loading/empty states, Zustand, Tailwind v4 tokens |
54
+ | `tanstack-query` 📦 | TanStack Query v5 — queries, mutations (pessimistic & optimistic), `useInfiniteQuery`/`useSuspenseQuery`, query-key factories, v4→v5 migration |
55
+ | `tanstack-router` | File-based routing, type-safe navigation, loaders & caching, search params, `beforeLoad` auth guards, pending UI that prevents frozen-feeling navigation |
33
56
 
34
57
  ### Backend
35
58
 
36
- | Skill | Description |
37
- |-------|-------------|
38
- | `express-best-practices` | Express.jsfeature-based structure, 3-layer architecture, Zod validation, centralized error handling, security middleware |
39
- | `fastify-best-practices` | Fastify — plugin architecture, encapsulation, TypeBox validation/serialization, services as decorators, reply helpers, hooks |
40
- | `fastapi-best-practices` | FastAPI — async correctness, Pydantic validation, dependency injection, service layer, structured error handling |
41
- | `pydantic-best-practices` | Pydantic v2 — model_config, field/model validators, Annotated types, discriminated unions, computed_field, strict mode, TypeAdapter |
42
- | `celery-best-practices` | Celery — idempotency, acks_late, autoretry with backoff/jitter, canvas (chain/group/chord), routing, priorities, beat, time limits |
59
+ | Skill | What it covers |
60
+ |-------|----------------|
61
+ | `fastapi` 📦 | FastAPIasync correctness, `Annotated` dependency injection, `lifespan`, response models, testing with dependency overrides; bundle covers Pydantic, Alembic, Celery, and list endpoints (pagination/filtering/search/sorting) |
43
62
  | `drf-best-practices` | Django REST Framework — thin serializers, service layer, queryset optimization, object-level permissions |
44
- | `drizzle-orm` | Drizzle ORM — schema design, identity columns, relations, relational queries, migrations, drizzle-kit workflow, type inference |
45
- | `alembic-migrations` | Alembic naming conventions, autogenerate review, data migration safety, downgrades, production deployment |
46
- | `docker-best-practices` | Docker — multi-stage builds, layer caching, security hardening, Compose Watch for local dev, health checks |
63
+ | `drizzle-orm` | Drizzle ORM — schema design, identity columns, relations, migration safety, type inference |
64
+ | `docker-best-practices` | Multi-stage builds, layer caching, security hardening, Compose Watch, health checks |
47
65
 
48
66
  ### Database
49
67
 
50
- | Skill | Description |
51
- |-------|-------------|
52
- | `sql-joins` | SQL joins LEFT JOIN traps, fan-out, NOT IN NULL bug, EXISTS vs IN, FK design, junction tables, CASCADE pitfalls |
53
- | `sql-indexing` | SQL indexing — composite order, covering/partial/expression indexes, SARGability, EXPLAIN interpretation, keyset pagination |
54
- | `sql-schema-design` | SQL schema — normalization, data types (TIMESTAMPTZ, NUMERIC), constraints, anti-patterns, safe migrations |
55
- | `sql-orm-patterns` | SQL ORM — N+1 fixes for Prisma/Django/SQLAlchemy/ActiveRecord/TypeORM, transactions, isolation levels, locking |
68
+ | Skill | What it covers |
69
+ |-------|----------------|
70
+ | `sql` 📦 | Schema design, data types, indexing & `EXPLAIN`, joins & subqueries, ORM patterns (N+1, transactions, locking), safe migrations |
71
+
72
+ ### Desktop
56
73
 
57
- ### Architecture
74
+ | Skill | What it covers |
75
+ |-------|----------------|
76
+ | `tauri-v2` | Tauri v2 — IPC commands, plugins, window management, system tray, global shortcuts, capabilities/permissions, events |
58
77
 
59
- | Skill | Description |
60
- |-------|-------------|
61
- | `single-responsibility` | Single Responsibility Principle — language-agnostic SRP, file size limits, CQS, separation of concerns, smell tests |
62
- | `avoid-hasty-abstractions` | AHA / Rule of Three — prefer duplication over the wrong abstraction, boolean-parameter creep, undoing bad extractions |
78
+ ### Foundations
63
79
 
64
- ### Quality
80
+ Cross-cutting craft — applies to any stack, any language.
65
81
 
66
- | Skill | Description |
67
- |-------|-------------|
68
- | `testing-best-practices` | Testing Arrange-Act-Assert, factory-based test data, test isolation, mocking boundaries, pyramid-balanced coverage |
69
- | `security-practices` | Web securityOWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
82
+ | Skill | What it covers |
83
+ |-------|----------------|
84
+ | `code-structure` 📦 | Single Responsibility (when to split) + Avoid Hasty Abstractions (when *not* to extract) — hard size limits, separation of concerns, the Rule of Three |
85
+ | `typescript-best-practices` | TypeScript 6.xtype design, generics, type guards, `satisfies`, `using`, error handling, `tsconfig` |
86
+ | `testing-best-practices` | Arrange-Act-Assert, factory-based test data, isolation, mocking boundaries, a pyramid-balanced suite |
87
+ | `security-practices` | OWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
70
88
 
71
89
  ## Commands
72
90
 
73
- Portable slash commands for common git workflows. Installed to `.claude/commands/` in your project.
91
+ Portable slash commands installed to `.claude/commands/`. Some orchestrate parallel subagents — those are pulled in automatically.
74
92
 
75
- | Command | Description |
76
- |---------|-------------|
77
- | `/commit` | Smart commitbranch safety, atomic staging, conventional commits |
78
- | `/pr` | Create PRauto-detect base branch, structured summary and test plan |
79
- | `/release` | Release flowdev→main PR with semver, changelog, tag, and GitHub release |
80
- | `/refactor` | Detect size/complexity/duplication/coupling issues via 4 parallel Haiku subagents, then refactor |
81
- | `/deep-review` | Multi-agent deep code review — 5 parallel Sonnet subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
82
- | `/plan-feature` | Integration-first feature planning — 3 parallel Haiku subagents scan for reusable code, established patterns, and touch points before producing a short integration plan |
93
+ | Command | What it does |
94
+ |---------|--------------|
95
+ | `/ship` | Unified delivery pipeline commit PR merge release. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
96
+ | `/preplan` | Resolve a fuzzy feature idea into concrete decisions 6 fixed phases, one question at a time, ends with a decision log. Run before `/plan-feature` |
97
+ | `/plan-feature` | Integration-first feature planning 3 parallel subagents scan for reusable code, patterns, and touch points before producing a short plan |
98
+ | `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
99
+ | `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
83
100
 
84
- ## Quick Start
101
+ ## How It Works
85
102
 
86
- Run from any project directory:
103
+ The CLI installs three kinds of artifact into your project's `.claude/` directory:
87
104
 
88
- ```bash
89
- npx @spardutti/claude-skills
90
- ```
105
+ - **Skills** → `.claude/skills/` — playbooks Claude loads while coding.
106
+ - **Commands** → `.claude/commands/` — slash commands you invoke directly.
107
+ - **Subagents** → `.claude/agents/` — declared by commands via `requires-agents`, installed for you.
91
108
 
92
- The CLI will:
109
+ ### Tracking & Updates
93
110
 
94
- 1. Fetch the latest skills, commands, and agents from GitHub
95
- 2. Let you pick which skills to install → `.claude/skills/`
96
- 3. Let you pick which commands to install → `.claude/commands/`
97
- 4. Auto-install any subagents declared by the selected commands → `.claude/agents/`
98
- 5. **Optionally set up automatic skill evaluation** (recommended — see below)
111
+ Every install writes a manifest at `.claude/.claude-skills.json` recording what the CLI installed and the catalog version. On the next run it uses the manifest to:
99
112
 
100
- ## Automatic Skill Evaluation
113
+ - **Pre-check what you already have** in the picker — re-running doubles as an update screen; toggle to add or remove.
114
+ - **Detect stale items** — skills/commands renamed or removed from the catalog upstream (e.g. when several skills are merged into a bundle) are flagged, and the CLI offers to delete them.
115
+ - **Never touch what it didn't install** — the manifest is the CLI's own record; hand-written skills are invisible to it and always safe.
101
116
 
102
- After installing skills, the CLI asks if you want to set up automatic skill evaluation. If you say yes, it will:
117
+ ```bash
118
+ npx @spardutti/claude-skills --sync
119
+ ```
103
120
 
104
- - **Install two hooks** in `.claude/hooks/`:
105
- - `skill-gate.sh` — PreToolUse gate on `Write|Edit|MultiEdit`
106
- - `skill-gate-automark.sh` — PostToolUse on `Skill` that auto-clears the gate
107
- - **Update your `CLAUDE.md`** with the skill-evaluation rule
121
+ `--sync` refreshes every tracked item to the latest catalog and prunes stale ones in one shot — no menu. For a project that predates the manifest, the first normal run offers a one-time cleanup of `.claude/` content no longer in the catalog.
108
122
 
109
- The gate hard-blocks `Write`, `Edit`, and `MultiEdit` until a per-session marker file exists at `/tmp/claude-skill-gate-<SESSION_ID>`. The marker is created automatically the first time Claude invokes any `Skill()` in the session — so the normal flow is: Claude lists skills as ACTIVATE/SKIP, calls `Skill()` for the ACTIVATE ones, and the gate clears for the rest of the session. If every skill is SKIP, Claude clears the gate by running `touch /tmp/claude-skill-gate-<SESSION_ID>`.
123
+ ### Automatic Skill Evaluation
110
124
 
111
- The marker is **per-session, not per-turn** short follow-ups like "yes" don't re-lock the gate after evaluation has already happened.
125
+ After installing skills, the CLI offers to set up a hook that **guarantees** Claude evaluates your skills before writing code instead of a soft reminder it can ignore.
112
126
 
113
- Unlike a soft reminder injected into context (which Claude can ignore), the gate denies the tool call outright — so the only path forward is to actually evaluate skills.
127
+ It installs two hooks and appends a rule to your `CLAUDE.md`:
114
128
 
115
- The gate auto-passes when the project has no `.claude/skills/*/SKILL.md` files, so it's safe to leave on globally.
129
+ - `skill-gate.sh` a `PreToolUse` gate on `Write|Edit|MultiEdit`
130
+ - `skill-gate-automark.sh` — a `PostToolUse` hook on `Skill` that clears the gate
116
131
 
117
- ### What gets created
132
+ <details>
133
+ <summary>How the gate works</summary>
118
134
 
119
- **`.claude/settings.json`** Registers both hooks:
135
+ The gate hard-blocks `Write`, `Edit`, and `MultiEdit` until a per-session marker exists at `/tmp/claude-skill-gate-<SESSION_ID>`. The marker is created automatically the first time Claude invokes any `Skill()` in the session — so the normal flow is: Claude lists skills as ACTIVATE/SKIP, calls `Skill()` for the ACTIVATE ones, and the gate clears for the rest of the session. If every skill is SKIP, Claude clears the gate with `touch /tmp/claude-skill-gate-<SESSION_ID>`.
136
+
137
+ The marker is **per-session, not per-turn** — short follow-ups like "yes" don't re-lock it. The gate auto-passes when a project has no `.claude/skills/*/SKILL.md`, so it's safe to leave on globally.
138
+
139
+ It registers in `.claude/settings.json`:
120
140
 
121
141
  ```json
122
142
  {
123
143
  "hooks": {
124
144
  "PreToolUse": [
125
- {
126
- "matcher": "Write|Edit|MultiEdit",
127
- "hooks": [
128
- {
129
- "type": "command",
130
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh"
131
- }
132
- ]
133
- }
145
+ { "matcher": "Write|Edit|MultiEdit", "hooks": [
146
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh" } ] }
134
147
  ],
135
148
  "PostToolUse": [
136
- {
137
- "matcher": "Skill",
138
- "hooks": [
139
- {
140
- "type": "command",
141
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate-automark.sh"
142
- }
143
- ]
144
- }
149
+ { "matcher": "Skill", "hooks": [
150
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate-automark.sh" } ] }
145
151
  ]
146
152
  }
147
153
  }
148
154
  ```
149
155
 
150
- **`CLAUDE.md`** — Appends the skill-evaluation rule that tells Claude to enumerate skills as ACTIVATE/SKIP and call `Skill()` for ACTIVATE entries before writing code.
151
-
152
- ## Manual Install
156
+ </details>
153
157
 
154
- If you don't want to use the CLI, copy files directly into your project:
158
+ <details>
159
+ <summary>Manual install (without the CLI)</summary>
155
160
 
156
161
  ```bash
157
162
  # Skills
@@ -160,15 +165,33 @@ cp -r skills/<skill-name> /path/to/project/.claude/skills/
160
165
  # Commands
161
166
  cp commands/<command-name>.md /path/to/project/.claude/commands/
162
167
 
163
- # Subagents (required by some commands — see the command's `requires-agents` frontmatter)
168
+ # Subagents — see the command's `requires-agents` frontmatter
164
169
  cp agents/<agent-name>.md /path/to/project/.claude/agents/
165
170
  ```
166
171
 
172
+ </details>
173
+
167
174
  ## Repository Layout
168
175
 
176
+ ```text
177
+ skills/ Skill playbooks — some are bundles (SKILL.md + on-demand reference files)
178
+ commands/ Slash commands installed to .claude/commands/
179
+ agents/ Subagent definitions — commands declare which they need via requires-agents
180
+ scripts/ validate-skills.mjs — checks skill length caps and reference integrity
181
+ cli/ The npm installer (npx @spardutti/claude-skills); version in cli/package.json
182
+ .husky/ pre-push hook running the skill validator
183
+ package.json Private dev-tooling package (claude-skills-dev) — not the published one
169
184
  ```
170
- skills/ Reference playbooks loaded by Claude during coding tasks
171
- commands/ Slash commands installed to .claude/commands/
172
- agents/ Subagent definitions — commands declare which ones they need
173
- cli/ The npm installer (npx @spardutti/claude-skills)
174
- ```
185
+
186
+ ## Contributing
187
+
188
+ Skills live in `skills/<name>/SKILL.md`. Authoring conventions are in [CLAUDE.md](./CLAUDE.md) — the short version:
189
+
190
+ - BAD/GOOD code pairs are the primary teaching tool; end every skill with a **Rules** section.
191
+ - `SKILL.md` ≤ 350 lines; reference files ≤ 500 and need a `## Contents` TOC past 100 lines.
192
+ - References are one level deep — `SKILL.md` links them, they don't link each other.
193
+ - `npm run validate-skills` enforces this; it also runs on `pre-push`.
194
+
195
+ ## License
196
+
197
+ MIT
package/bin/cli.mjs CHANGED
@@ -6,26 +6,103 @@ import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
9
- import { promptSkillSelection, promptCommandSelection } from "../lib/prompt.mjs";
9
+ import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
10
10
  import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
11
11
  import { setupHook } from "../lib/setup-hook.mjs";
12
12
  import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
13
+ import {
14
+ readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
15
+ MANIFEST_FILE,
16
+ } from "../lib/manifest.mjs";
13
17
 
14
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
19
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
20
+ const CWD = process.cwd();
21
+
22
+ // `--sync`: re-install everything the manifest records (refreshed to the latest
23
+ // catalog) and prune anything removed upstream — no interactive menu.
24
+ async function runSync(manifest, catalog) {
25
+ if (!manifest) {
26
+ console.log(` ${chalk.yellow("Nothing to sync")} — no manifest in this project. Run without --sync first.\n`);
27
+ return;
28
+ }
29
+ const orphans = computeOrphans(manifest, catalog);
30
+ const orphanCount = orphans.skills.length + orphans.commands.length + orphans.agents.length;
31
+ if (orphanCount > 0) {
32
+ await removeArtifacts(CWD, orphans);
33
+ console.log(` ${chalk.green("✔")} Pruned ${orphanCount} item(s) removed from the catalog.`);
34
+ }
35
+
36
+ const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
37
+ const commands = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
38
+ if (skills.length > 0) { console.log(); await installSkills(skills); }
39
+ if (commands.length > 0) { console.log(); await installCommands(commands); }
40
+ const { installed } = await installRequiredAgents(commands, catalog.agents, CWD);
41
+
42
+ await writeManifest(CWD, {
43
+ catalogVersion: pkg.version,
44
+ skills: skills.map((s) => s.dirName),
45
+ commands: commands.map((c) => c.fileName),
46
+ agents: installed.map((a) => a.fileName),
47
+ });
48
+ console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${pkg.version}.`)}\n`);
49
+ }
16
50
 
17
51
  async function main() {
52
+ const isSync = process.argv.includes("--sync");
18
53
  console.log(`\n ${chalk.bold.cyan("Claude Skills Installer")} ${chalk.dim(`v${pkg.version}`)}\n`);
19
54
 
20
55
  console.log(chalk.dim(" Fetching available skills, commands, and agents...\n"));
21
56
  const [skills, commands, agents] = await Promise.all([fetchSkills(), fetchCommands(), fetchAgents()]);
57
+ const catalog = { skills, commands, agents };
58
+ const manifest = await readManifest(CWD);
59
+
60
+ if (isSync) return runSync(manifest, catalog);
61
+
62
+ // --- Prune items renamed or removed from the catalog upstream ---
63
+ const orphans = computeOrphans(manifest, catalog);
64
+ const orphanNames = [...orphans.skills, ...orphans.commands, ...orphans.agents];
65
+ if (orphanNames.length > 0) {
66
+ console.log(` ${chalk.yellow("!")} ${orphanNames.length} installed item(s) are no longer in the catalog (renamed or removed):`);
67
+ for (const n of orphanNames) console.log(` ${chalk.dim("-")} ${n}`);
68
+ if (await confirm({ message: "Delete these stale items?", default: true })) {
69
+ await removeArtifacts(CWD, orphans);
70
+ console.log(` ${chalk.green("✔")} Removed ${orphanNames.length} stale item(s).`);
71
+ }
72
+ console.log();
73
+ }
74
+
75
+ // --- Legacy projects (no manifest): offer to clean content not in the catalog ---
76
+ if (!manifest) {
77
+ const disk = await scanInstalled(CWD);
78
+ const catSkills = new Set(skills.map((s) => s.dirName));
79
+ const catCommands = new Set(commands.map((c) => c.fileName));
80
+ const strayS = disk.skills.filter((d) => !catSkills.has(d));
81
+ const strayC = disk.commands.filter((f) => !catCommands.has(f));
82
+ if (strayS.length + strayC.length > 0) {
83
+ console.log(` ${chalk.yellow("!")} Untracked items in .claude/ that aren't in the catalog — possibly stale, possibly your own:`);
84
+ const toRemove = await promptRemoval(
85
+ [...strayS, ...strayC],
86
+ "Select any to delete (leave unchecked to keep):",
87
+ false,
88
+ );
89
+ if (toRemove.length > 0) {
90
+ await removeArtifacts(CWD, {
91
+ skills: toRemove.filter((n) => strayS.includes(n)),
92
+ commands: toRemove.filter((n) => strayC.includes(n)),
93
+ });
94
+ console.log(` ${chalk.green("✔")} Removed ${toRemove.length} item(s).`);
95
+ }
96
+ console.log();
97
+ }
98
+ }
22
99
 
23
100
  // --- Skills ---
24
101
  let selectedSkills = [];
25
102
  if (skills.length === 0) {
26
103
  console.log(" No skills found.");
27
104
  } else {
28
- selectedSkills = await promptSkillSelection(skills);
105
+ selectedSkills = await promptSkillSelection(skills, manifest?.skills ?? []);
29
106
  if (selectedSkills.length > 0) {
30
107
  console.log();
31
108
  await installSkills(selectedSkills);
@@ -34,25 +111,34 @@ async function main() {
34
111
 
35
112
  // --- Commands ---
36
113
  let selectedCommands = [];
37
- let installedAgentCount = 0;
114
+ let installedAgents = [];
38
115
  if (commands.length > 0) {
39
116
  console.log();
40
- selectedCommands = await promptCommandSelection(commands);
117
+ selectedCommands = await promptCommandSelection(commands, manifest?.commands ?? []);
41
118
  if (selectedCommands.length > 0) {
42
119
  console.log();
43
120
  await installCommands(selectedCommands);
44
-
45
- const { installed, missing } = await installRequiredAgents(selectedCommands, agents);
46
- installedAgentCount = installed.length;
121
+ const { installed, missing } = await installRequiredAgents(selectedCommands, agents, CWD);
122
+ installedAgents = installed;
47
123
  if (missing.length > 0) {
48
- console.log(
49
- ` ${chalk.yellow("!")} Missing agents referenced by commands: ${missing.join(", ")}`
50
- );
124
+ console.log(` ${chalk.yellow("!")} Missing agents referenced by commands: ${missing.join(", ")}`);
51
125
  }
52
126
  }
53
127
  }
54
128
 
55
- if (selectedSkills.length === 0 && selectedCommands.length === 0) {
129
+ // --- Remove items the user deselected (were installed, still in the catalog, now unchecked) ---
130
+ const removals = computeRemovals(manifest, catalog, {
131
+ skills: selectedSkills.map((s) => s.dirName),
132
+ commands: selectedCommands.map((c) => c.fileName),
133
+ agents: installedAgents.map((a) => a.fileName),
134
+ });
135
+ const removalCount = removals.skills.length + removals.commands.length + removals.agents.length;
136
+ if (removalCount > 0) {
137
+ await removeArtifacts(CWD, removals);
138
+ console.log(`\n ${chalk.dim(`Removed ${removalCount} deselected item(s): ${[...removals.skills, ...removals.commands].join(", ")}`)}`);
139
+ }
140
+
141
+ if (selectedSkills.length === 0 && selectedCommands.length === 0 && removalCount === 0) {
56
142
  console.log("\n Nothing selected.");
57
143
  process.exit(0);
58
144
  }
@@ -64,7 +150,6 @@ async function main() {
64
150
  message: "Set up skill evaluation hook + CLAUDE.md rule? (Recommended)",
65
151
  default: true,
66
152
  });
67
-
68
153
  if (shouldSetup) {
69
154
  console.log();
70
155
  await setupHook();
@@ -72,11 +157,19 @@ async function main() {
72
157
  }
73
158
  }
74
159
 
160
+ // --- Record what is now installed ---
161
+ await writeManifest(CWD, {
162
+ catalogVersion: pkg.version,
163
+ skills: selectedSkills.map((s) => s.dirName),
164
+ commands: selectedCommands.map((c) => c.fileName),
165
+ agents: installedAgents.map((a) => a.fileName),
166
+ });
167
+
75
168
  const parts = [];
76
169
  if (selectedSkills.length > 0) parts.push(`${selectedSkills.length} skill(s)`);
77
170
  if (selectedCommands.length > 0) parts.push(`${selectedCommands.length} command(s)`);
78
- if (installedAgentCount > 0) parts.push(`${installedAgentCount} agent(s)`);
79
- console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(", ")} installed successfully.`)}\n`);
171
+ if (installedAgents.length > 0) parts.push(`${installedAgents.length} agent(s)`);
172
+ console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(", ")} installed.`)} ${chalk.dim(`Tracked in .claude/${MANIFEST_FILE}`)}\n`);
80
173
  }
81
174
 
82
175
  main().catch((err) => {
@@ -0,0 +1,106 @@
1
+ import { readFile, writeFile, rm, readdir, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export const MANIFEST_FILE = ".claude-skills.json";
5
+
6
+ function manifestPath(targetDir) {
7
+ return join(targetDir, ".claude", MANIFEST_FILE);
8
+ }
9
+
10
+ // Read the install manifest, or null if this project has never been tracked.
11
+ export async function readManifest(targetDir = process.cwd()) {
12
+ try {
13
+ const data = JSON.parse(await readFile(manifestPath(targetDir), "utf-8"));
14
+ return {
15
+ catalogVersion: data.catalogVersion ?? null,
16
+ skills: data.skills ?? [],
17
+ commands: data.commands ?? [],
18
+ agents: data.agents ?? [],
19
+ };
20
+ } catch {
21
+ return null; // no manifest — a pre-manifest project or a fresh install
22
+ }
23
+ }
24
+
25
+ // Record exactly what the CLI has installed. This is the CLI's source of truth —
26
+ // it never deletes anything not listed here.
27
+ export async function writeManifest(targetDir, { catalogVersion, skills, commands, agents }) {
28
+ await mkdir(join(targetDir, ".claude"), { recursive: true });
29
+ const data = {
30
+ catalogVersion,
31
+ installedAt: new Date().toISOString(),
32
+ skills: [...new Set(skills)].sort(),
33
+ commands: [...new Set(commands)].sort(),
34
+ agents: [...new Set(agents)].sort(),
35
+ };
36
+ await writeFile(manifestPath(targetDir), JSON.stringify(data, null, 2) + "\n");
37
+ }
38
+
39
+ // Manifest entries that no longer exist in the current catalog (renamed or removed upstream).
40
+ export function computeOrphans(manifest, catalog) {
41
+ if (!manifest) return { skills: [], commands: [], agents: [] };
42
+ const has = {
43
+ skills: new Set(catalog.skills.map((s) => s.dirName)),
44
+ commands: new Set(catalog.commands.map((c) => c.fileName)),
45
+ agents: new Set(catalog.agents.map((a) => a.fileName)),
46
+ };
47
+ return {
48
+ skills: manifest.skills.filter((n) => !has.skills.has(n)),
49
+ commands: manifest.commands.filter((n) => !has.commands.has(n)),
50
+ agents: manifest.agents.filter((n) => !has.agents.has(n)),
51
+ };
52
+ }
53
+
54
+ // Manifest entries still in the catalog but absent from the new selection — deliberate removals.
55
+ export function computeRemovals(manifest, catalog, selection) {
56
+ if (!manifest) return { skills: [], commands: [], agents: [] };
57
+ const inCatalog = {
58
+ skills: new Set(catalog.skills.map((s) => s.dirName)),
59
+ commands: new Set(catalog.commands.map((c) => c.fileName)),
60
+ agents: new Set(catalog.agents.map((a) => a.fileName)),
61
+ };
62
+ const selected = {
63
+ skills: new Set(selection.skills),
64
+ commands: new Set(selection.commands),
65
+ agents: new Set(selection.agents),
66
+ };
67
+ return {
68
+ skills: manifest.skills.filter((n) => inCatalog.skills.has(n) && !selected.skills.has(n)),
69
+ commands: manifest.commands.filter((n) => inCatalog.commands.has(n) && !selected.commands.has(n)),
70
+ agents: manifest.agents.filter((n) => inCatalog.agents.has(n) && !selected.agents.has(n)),
71
+ };
72
+ }
73
+
74
+ // Directories/files actually present in .claude/ — used to spot stale content
75
+ // in legacy (pre-manifest) projects.
76
+ export async function scanInstalled(targetDir = process.cwd()) {
77
+ const list = async (subdir, dirsOnly) => {
78
+ try {
79
+ const entries = await readdir(join(targetDir, ".claude", subdir), { withFileTypes: true });
80
+ return entries
81
+ .filter((e) => (dirsOnly ? e.isDirectory() : e.isFile() && e.name.endsWith(".md")))
82
+ .map((e) => e.name);
83
+ } catch {
84
+ return [];
85
+ }
86
+ };
87
+ return {
88
+ skills: await list("skills", true),
89
+ commands: await list("commands", false),
90
+ agents: await list("agents", false),
91
+ };
92
+ }
93
+
94
+ // Delete skill directories and command/agent files. Only ever called with names
95
+ // the CLI knows it installed (from the manifest) or names the user explicitly picked.
96
+ export async function removeArtifacts(targetDir, { skills = [], commands = [], agents = [] }) {
97
+ for (const dir of skills) {
98
+ await rm(join(targetDir, ".claude", "skills", dir), { recursive: true, force: true });
99
+ }
100
+ for (const file of commands) {
101
+ await rm(join(targetDir, ".claude", "commands", file), { force: true });
102
+ }
103
+ for (const file of agents) {
104
+ await rm(join(targetDir, ".claude", "agents", file), { force: true });
105
+ }
106
+ }
package/lib/prompt.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { checkbox, Separator } from "@inquirer/prompts";
2
2
  import chalk from "chalk";
3
3
 
4
- const CATEGORY_ORDER = ["Frontend", "Desktop", "TypeScript", "Backend", "Database", "Architecture", "Quality", "General"];
4
+ const CATEGORY_ORDER = ["Frontend", "Backend", "Database", "Desktop", "Foundations", "General"];
5
5
  const COMMAND_CATEGORY_ORDER = ["Workflow", "General"];
6
6
 
7
7
  function humanName(skill) {
@@ -14,7 +14,9 @@ function stripQuotes(str) {
14
14
  return str.replace(/^["']|["']$/g, "");
15
15
  }
16
16
 
17
- export async function promptSkillSelection(skills) {
17
+ export async function promptSkillSelection(skills, installed = []) {
18
+ const installedSet = new Set(installed);
19
+
18
20
  // Group skills by category preserving order
19
21
  const grouped = new Map();
20
22
  for (const cat of CATEGORY_ORDER) {
@@ -31,6 +33,7 @@ export async function promptSkillSelection(skills) {
31
33
  name: chalk.bold(humanName(skill)),
32
34
  value: skill,
33
35
  description: chalk.dim(stripQuotes(skill.description)),
36
+ checked: installedSet.has(skill.dirName), // pre-check what's already installed
34
37
  });
35
38
  }
36
39
  }
@@ -53,7 +56,8 @@ export async function promptSkillSelection(skills) {
53
56
  return selected;
54
57
  }
55
58
 
56
- export async function promptCommandSelection(commands) {
59
+ export async function promptCommandSelection(commands, installed = []) {
60
+ const installedSet = new Set(installed);
57
61
  const grouped = new Map();
58
62
  for (const cat of COMMAND_CATEGORY_ORDER) {
59
63
  const items = commands.filter((c) => c.category === cat);
@@ -76,6 +80,7 @@ export async function promptCommandSelection(commands) {
76
80
  name: chalk.bold(humanName(cmd)),
77
81
  value: cmd,
78
82
  description: chalk.dim(stripQuotes(cmd.description)),
83
+ checked: installedSet.has(cmd.fileName), // pre-check what's already installed
79
84
  });
80
85
  }
81
86
  }
@@ -97,3 +102,14 @@ export async function promptCommandSelection(commands) {
97
102
 
98
103
  return selected;
99
104
  }
105
+
106
+ // Checkbox of removable items — used for catalog orphans and untracked stale content.
107
+ // `preChecked` true when the items are known-stale (manifest orphans).
108
+ export async function promptRemoval(names, message, preChecked = false) {
109
+ if (names.length === 0) return [];
110
+ return checkbox({
111
+ message,
112
+ choices: names.map((n) => ({ name: n, value: n, checked: preChecked })),
113
+ theme: { icon: { cursor: ">" }, style: { highlight: (t) => chalk.cyan(t) } },
114
+ });
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "1.29.1",
3
+ "version": "2.1.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {