@spardutti/claude-skills 1.28.1 → 2.0.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 +141 -94
- package/bin/cli.mjs +107 -14
- package/lib/manifest.mjs +106 -0
- package/lib/prompt.mjs +18 -2
- package/lib/setup-hook.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,142 +1,171 @@
|
|
|
1
1
|
# Claude Skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> An interactive CLI that installs a curated catalog of Claude Code **skills**, **slash commands**, and **subagents** into any project.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@spardutti/claude-skills)
|
|
6
|
+
[](https://www.npmjs.com/package/@spardutti/claude-skills)
|
|
7
|
+
[](./LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
Run from any project directory:
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx @spardutti/claude-skills
|
|
17
|
+
```
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
```text
|
|
20
|
+
Claude Skills Installer v2.0.0
|
|
21
|
+
|
|
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
|
+
```
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
**14 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 |
|
|
37
|
-
|
|
38
|
-
| `
|
|
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` 📦 | FastAPI — async 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,
|
|
45
|
-
| `
|
|
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 |
|
|
51
|
-
|
|
52
|
-
| `sql
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
### TypeScript
|
|
73
|
+
|
|
74
|
+
| Skill | What it covers |
|
|
75
|
+
|-------|----------------|
|
|
76
|
+
| `typescript-best-practices` | TypeScript 6.x — type design, generics, type guards, `satisfies`, `using`, error handling, `tsconfig` |
|
|
77
|
+
|
|
78
|
+
### Quality
|
|
79
|
+
|
|
80
|
+
| Skill | What it covers |
|
|
81
|
+
|-------|----------------|
|
|
82
|
+
| `testing-best-practices` | Arrange-Act-Assert, factory-based test data, isolation, mocking boundaries, a pyramid-balanced suite |
|
|
83
|
+
| `security-practices` | OWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
|
|
56
84
|
|
|
57
85
|
### Architecture
|
|
58
86
|
|
|
59
|
-
| Skill |
|
|
60
|
-
|
|
61
|
-
| `single-responsibility` |
|
|
87
|
+
| Skill | What it covers |
|
|
88
|
+
|-------|----------------|
|
|
89
|
+
| `single-responsibility` | Language-agnostic SRP — file-size limits, CQS, separation of concerns, smell tests |
|
|
62
90
|
| `avoid-hasty-abstractions` | AHA / Rule of Three — prefer duplication over the wrong abstraction, boolean-parameter creep, undoing bad extractions |
|
|
63
91
|
|
|
64
|
-
###
|
|
92
|
+
### Desktop
|
|
65
93
|
|
|
66
|
-
| Skill |
|
|
67
|
-
|
|
68
|
-
| `
|
|
69
|
-
| `security-practices` | Web security — OWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
|
|
94
|
+
| Skill | What it covers |
|
|
95
|
+
|-------|----------------|
|
|
96
|
+
| `tauri-v2` | Tauri v2 — IPC commands, plugins, window management, system tray, global shortcuts, capabilities/permissions, events |
|
|
70
97
|
|
|
71
98
|
## Commands
|
|
72
99
|
|
|
73
|
-
Portable slash commands
|
|
100
|
+
Portable slash commands installed to `.claude/commands/`. Some orchestrate parallel subagents — those are pulled in automatically.
|
|
74
101
|
|
|
75
|
-
| Command |
|
|
76
|
-
|
|
77
|
-
| `/
|
|
78
|
-
| `/
|
|
79
|
-
| `/
|
|
80
|
-
| `/refactor` | Detect size/complexity/duplication/coupling issues via 4 parallel
|
|
81
|
-
| `/deep-review` | Multi-agent deep code review — 5 parallel
|
|
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 |
|
|
102
|
+
| Command | What it does |
|
|
103
|
+
|---------|--------------|
|
|
104
|
+
| `/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 |
|
|
105
|
+
| `/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` |
|
|
106
|
+
| `/plan-feature` | Integration-first feature planning — 3 parallel subagents scan for reusable code, patterns, and touch points before producing a short plan |
|
|
107
|
+
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
108
|
+
| `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
|
|
83
109
|
|
|
84
|
-
##
|
|
110
|
+
## How It Works
|
|
85
111
|
|
|
86
|
-
|
|
112
|
+
The CLI installs three kinds of artifact into your project's `.claude/` directory:
|
|
113
|
+
|
|
114
|
+
- **Skills** → `.claude/skills/` — playbooks Claude loads while coding.
|
|
115
|
+
- **Commands** → `.claude/commands/` — slash commands you invoke directly.
|
|
116
|
+
- **Subagents** → `.claude/agents/` — declared by commands via `requires-agents`, installed for you.
|
|
117
|
+
|
|
118
|
+
### Tracking & Updates
|
|
119
|
+
|
|
120
|
+
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:
|
|
121
|
+
|
|
122
|
+
- **Pre-check what you already have** in the picker — re-running doubles as an update screen; toggle to add or remove.
|
|
123
|
+
- **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.
|
|
124
|
+
- **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.
|
|
87
125
|
|
|
88
126
|
```bash
|
|
89
|
-
npx @spardutti/claude-skills
|
|
127
|
+
npx @spardutti/claude-skills --sync
|
|
90
128
|
```
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
130
|
+
`--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.
|
|
99
131
|
|
|
100
|
-
|
|
132
|
+
### Automatic Skill Evaluation
|
|
101
133
|
|
|
102
|
-
After installing skills, the CLI
|
|
134
|
+
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.
|
|
103
135
|
|
|
104
|
-
|
|
105
|
-
- **Update your `CLAUDE.md`** with the skill-evaluation rule
|
|
136
|
+
It installs two hooks and appends a rule to your `CLAUDE.md`:
|
|
106
137
|
|
|
107
|
-
|
|
138
|
+
- `skill-gate.sh` — a `PreToolUse` gate on `Write|Edit|MultiEdit`
|
|
139
|
+
- `skill-gate-automark.sh` — a `PostToolUse` hook on `Skill` that clears the gate
|
|
108
140
|
|
|
109
|
-
|
|
141
|
+
<details>
|
|
142
|
+
<summary>How the gate works</summary>
|
|
110
143
|
|
|
111
|
-
The gate
|
|
144
|
+
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>`.
|
|
112
145
|
|
|
113
|
-
|
|
146
|
+
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.
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
It registers in `.claude/settings.json`:
|
|
116
149
|
|
|
117
150
|
```json
|
|
118
151
|
{
|
|
119
152
|
"hooks": {
|
|
120
153
|
"PreToolUse": [
|
|
121
|
-
{
|
|
122
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
]
|
|
129
|
-
}
|
|
154
|
+
{ "matcher": "Write|Edit|MultiEdit", "hooks": [
|
|
155
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate.sh" } ] }
|
|
156
|
+
],
|
|
157
|
+
"PostToolUse": [
|
|
158
|
+
{ "matcher": "Skill", "hooks": [
|
|
159
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-gate-automark.sh" } ] }
|
|
130
160
|
]
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
```
|
|
134
164
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## Manual Install
|
|
165
|
+
</details>
|
|
138
166
|
|
|
139
|
-
|
|
167
|
+
<details>
|
|
168
|
+
<summary>Manual install (without the CLI)</summary>
|
|
140
169
|
|
|
141
170
|
```bash
|
|
142
171
|
# Skills
|
|
@@ -145,15 +174,33 @@ cp -r skills/<skill-name> /path/to/project/.claude/skills/
|
|
|
145
174
|
# Commands
|
|
146
175
|
cp commands/<command-name>.md /path/to/project/.claude/commands/
|
|
147
176
|
|
|
148
|
-
# Subagents
|
|
177
|
+
# Subagents — see the command's `requires-agents` frontmatter
|
|
149
178
|
cp agents/<agent-name>.md /path/to/project/.claude/agents/
|
|
150
179
|
```
|
|
151
180
|
|
|
181
|
+
</details>
|
|
182
|
+
|
|
152
183
|
## Repository Layout
|
|
153
184
|
|
|
185
|
+
```text
|
|
186
|
+
skills/ Skill playbooks — some are bundles (SKILL.md + on-demand reference files)
|
|
187
|
+
commands/ Slash commands installed to .claude/commands/
|
|
188
|
+
agents/ Subagent definitions — commands declare which they need via requires-agents
|
|
189
|
+
scripts/ validate-skills.mjs — checks skill length caps and reference integrity
|
|
190
|
+
cli/ The npm installer (npx @spardutti/claude-skills); version in cli/package.json
|
|
191
|
+
.husky/ pre-push hook running the skill validator
|
|
192
|
+
package.json Private dev-tooling package (claude-skills-dev) — not the published one
|
|
154
193
|
```
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Skills live in `skills/<name>/SKILL.md`. Authoring conventions are in [CLAUDE.md](./CLAUDE.md) — the short version:
|
|
198
|
+
|
|
199
|
+
- BAD/GOOD code pairs are the primary teaching tool; end every skill with a **Rules** section.
|
|
200
|
+
- `SKILL.md` ≤ 350 lines; reference files ≤ 500 and need a `## Contents` TOC past 100 lines.
|
|
201
|
+
- References are one level deep — `SKILL.md` links them, they don't link each other.
|
|
202
|
+
- `npm run validate-skills` enforces this; it also runs on `pre-push`.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
79
|
-
console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(", ")} installed
|
|
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) => {
|
package/lib/manifest.mjs
ADDED
|
@@ -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
|
@@ -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/lib/setup-hook.mjs
CHANGED
|
@@ -34,7 +34,7 @@ if [ -f "$MARKER" ]; then
|
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
36
|
cat <<EOF
|
|
37
|
-
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"
|
|
37
|
+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED: skill evaluation required before file edits in this session.\\n\\nStep 1 — evaluate every available skill as ACTIVATE or SKIP with a one-line reason.\\n\\nStep 2 — you MUST take EXACTLY ONE of these tool actions to clear the gate. Listing skills in text is NOT enough; retrying the edit without doing one of these will be denied again:\\n (a) If any skill is ACTIVATE → call Skill(name) for it. This auto-clears the gate.\\n (b) If ALL skills are SKIP → run this Bash tool call: touch /tmp/claude-skill-gate-$SESSION_ID\\n\\nStep 3 — only after Step 2 completes, retry the file edit."}}
|
|
38
38
|
EOF
|
|
39
39
|
exit 0
|
|
40
40
|
`;
|