@spardutti/claude-skills 1.19.1 → 1.21.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,67 +1,162 @@
1
- # @spardutti/claude-skills
1
+ # Claude Skills
2
2
 
3
- Interactive CLI to install reusable [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills into any project.
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.
4
4
 
5
- ## Usage
5
+ ## Skills
6
6
 
7
- ```bash
8
- npx @spardutti/claude-skills
9
- ```
7
+ ### Frontend
10
8
 
11
- Run this from your project's root directory. The CLI will:
12
-
13
- 1. Fetch the latest skills from [GitHub](https://github.com/Spardutti/claude-skills)
14
- 2. Let you interactively select which skills to install
15
- 3. Copy them into your project's `.claude/skills/` directory
16
- 4. Ask to set up **automatic skill evaluation** (hook + CLAUDE.md rule)
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 |
17
21
 
18
- ## Automatic Skill Evaluation
22
+ ### Desktop
19
23
 
20
- Skills alone don't guarantee Claude will use them. The CLI can optionally set up enforcement:
24
+ | Skill | Description |
25
+ |-------|-------------|
26
+ | `tauri-v2` | Tauri v2 — IPC commands, plugins, window management, system tray, global shortcuts, capabilities/permissions, events |
21
27
 
22
- - **Hook** (`.claude/hooks/skill-forced-eval-hook.sh`) — Runs on every prompt, injects a mandatory skill evaluation sequence into Claude's context
23
- - **CLAUDE.md rule** (`skill_evaluation` block) — Instructs Claude to list every skill as ACTIVATE/SKIP before writing any code
28
+ ### TypeScript
24
29
 
25
- Together, these force Claude to explicitly evaluate and activate relevant skills instead of silently ignoring them.
30
+ | Skill | Description |
31
+ |-------|-------------|
32
+ | `typescript-best-practices` | TypeScript 5.x — type design, type safety, generics, error handling, tsconfig |
26
33
 
27
- ## Available Skills
34
+ ### Backend
28
35
 
29
36
  | Skill | Description |
30
37
  |-------|-------------|
31
- | `react-best-practices` | React 19 — component design, state management, performance, React 19 features, TypeScript integration |
32
- | `react-use-effect` | React 19 useEffect best practices and anti-patterns |
33
- | `react-query` | TanStack React Query with query-key-factory patterns |
34
- | `tanstack-router-best-practices` | TanStack Router — file-based routing, type-safe navigation, loaders, search params, auth guards |
35
- | `typescript-best-practices` | TypeScript 5.x — type design, type safety, generics, error handling, tsconfig |
36
- | `react-single-responsibility` | React single responsibility — component splitting, hook isolation, file size limits, complexity rules |
37
- | `tailwind-tokens` | Enforce Tailwind CSS design tokens — no arbitrary values when a token exists |
38
- | `drf-best-practices` | Django REST Framework — thin serializers, service layer, queryset optimization, object-level permissions |
39
- | `fastapi-best-practices` | FastAPI — async correctness, Pydantic validation, dependency injection, service layer, structured error handling |
40
- | `security-practices` | Web security — OWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
41
- | `alembic-migrations` | Alembic — naming conventions, autogenerate review, data migration safety, downgrades, production deployment |
42
- | `testing-best-practices` | Testing — Arrange-Act-Assert, factory-based test data, test isolation, mocking boundaries, pyramid-balanced coverage |
43
- | `docker-best-practices` | Docker — multi-stage builds, layer caching, security hardening, Compose Watch for local dev, health checks |
44
- | `trpc-react-query` | tRPC v11 — queryOptions/mutationOptions patterns, router organization, middleware, cache invalidation, optimistic updates |
45
38
  | `express-best-practices` | Express.js — feature-based structure, 3-layer architecture, Zod validation, centralized error handling, security middleware |
46
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
+ | `drf-best-practices` | Django REST Framework — thin serializers, service layer, queryset optimization, object-level permissions |
47
42
  | `drizzle-orm` | Drizzle ORM — schema design, identity columns, relations, relational queries, migrations, drizzle-kit workflow, type inference |
48
- | `zustand` | Zustandstore design, selectors, persist/immer middleware, slices pattern, devtools, transient updates |
49
- | `tauri-v2` | Tauri v2 IPC commands, plugins, window management, system tray, global shortcuts, capabilities/permissions, events |
50
- | `dnd-kit` | @dnd-kit — sortable lists, sensors, collision detection, drag overlays, multi-container (kanban), accessibility |
51
- | `framer-motion` | Motion (Framer Motion) — AnimatePresence, layout animations, variants, gestures, useAnimate, performance |
43
+ | `alembic-migrations` | Alembicnaming conventions, autogenerate review, data migration safety, downgrades, production deployment |
44
+ | `docker-best-practices` | Dockermulti-stage builds, layer caching, security hardening, Compose Watch for local dev, health checks |
45
+
46
+ ### Database
47
+
48
+ | Skill | Description |
49
+ |-------|-------------|
50
+ | `sql-joins` | SQL joins — LEFT JOIN traps, fan-out, NOT IN NULL bug, EXISTS vs IN, FK design, junction tables, CASCADE pitfalls |
51
+ | `sql-indexing` | SQL indexing — composite order, covering/partial/expression indexes, SARGability, EXPLAIN interpretation, keyset pagination |
52
+ | `sql-schema-design` | SQL schema — normalization, data types (TIMESTAMPTZ, NUMERIC), constraints, anti-patterns, safe migrations |
53
+ | `sql-orm-patterns` | SQL ORM — N+1 fixes for Prisma/Django/SQLAlchemy/ActiveRecord/TypeORM, transactions, isolation levels, locking |
54
+
55
+ ### Architecture
56
+
57
+ | Skill | Description |
58
+ |-------|-------------|
52
59
  | `single-responsibility` | Single Responsibility Principle — language-agnostic SRP, file size limits, CQS, separation of concerns, smell tests |
53
60
 
54
- ## GitHub Authentication
61
+ ### Quality
55
62
 
56
- The CLI fetches skills via the GitHub API. Unauthenticated requests are limited to 60/hour. To avoid rate limits:
63
+ | Skill | Description |
64
+ |-------|-------------|
65
+ | `testing-best-practices` | Testing — Arrange-Act-Assert, factory-based test data, test isolation, mocking boundaries, pyramid-balanced coverage |
66
+ | `security-practices` | Web security — OWASP Top 10 prevention, input validation, auth, SQL injection, XSS, CSRF, secure defaults |
67
+
68
+ ## Commands
69
+
70
+ Portable slash commands for common git workflows. Installed to `.claude/commands/` in your project.
71
+
72
+ | Command | Description |
73
+ |---------|-------------|
74
+ | `/commit` | Smart commit — branch safety, atomic staging, conventional commits |
75
+ | `/pr` | Create PR — auto-detect base branch, structured summary and test plan |
76
+ | `/release` | Release flow — dev→main PR with semver, changelog, tag, and GitHub release |
77
+ | `/refactor` | Detect size/complexity/duplication/coupling issues via 4 parallel Haiku subagents, then refactor |
78
+ | `/deep-review` | Multi-agent deep code review — 5 parallel Sonnet subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
57
79
 
58
- - Install the [GitHub CLI](https://cli.github.com) and run `gh auth login` — the token is detected automatically
59
- - Or set `GITHUB_TOKEN` / `GH_TOKEN` as an environment variable
80
+ ## Quick Start
60
81
 
61
- ## What are Claude Code Skills?
82
+ Run from any project directory:
62
83
 
63
- Skills are markdown files placed in `.claude/skills/` that give Claude Code domain-specific knowledge and guidelines. They help Claude follow your team's patterns and best practices automatically.
84
+ ```bash
85
+ npx @spardutti/claude-skills
86
+ ```
64
87
 
65
- ## License
88
+ The CLI will:
66
89
 
67
- MIT
90
+ 1. Fetch the latest skills, commands, and agents from GitHub
91
+ 2. Let you pick which skills to install → `.claude/skills/`
92
+ 3. Let you pick which commands to install → `.claude/commands/`
93
+ 4. Auto-install any subagents declared by the selected commands → `.claude/agents/`
94
+ 5. **Optionally set up automatic skill evaluation** (recommended — see below)
95
+
96
+ ## Automatic Skill Evaluation
97
+
98
+ After installing skills, the CLI asks if you want to set up automatic skill evaluation. If you say yes, it will:
99
+
100
+ - **Create a hook** at `.claude/hooks/skill-forced-eval-hook.sh` that runs on every prompt
101
+ - **Update your `CLAUDE.md`** with a `skill_evaluation` rule
102
+
103
+ This forces Claude to explicitly evaluate every installed skill before writing code — listing each skill as ACTIVATE or SKIP with a reason, then calling the relevant ones. Without this, Claude may silently ignore your skills.
104
+
105
+ ### What gets created
106
+
107
+ **`.claude/settings.json`** — Registers the hook:
108
+
109
+ ```json
110
+ {
111
+ "hooks": {
112
+ "UserPromptSubmit": [
113
+ {
114
+ "hooks": [
115
+ {
116
+ "type": "command",
117
+ "command": ".claude/hooks/skill-forced-eval-hook.sh"
118
+ }
119
+ ]
120
+ }
121
+ ]
122
+ }
123
+ }
124
+ ```
125
+
126
+ **`CLAUDE.md`** — Appends the evaluation rule:
127
+
128
+ ```yaml
129
+ skill_evaluation:
130
+ mandatory: true
131
+ rule: |
132
+ BEFORE writing ANY code, you MUST:
133
+ 1. List EVERY skill from the system-reminder's available skills section
134
+ 2. For each skill, write: [skill-name] → ACTIVATE / SKIP — [one-line reason]
135
+ 3. Call Skill(name) for every skill marked ACTIVATE
136
+ 4. Only THEN proceed to implementation
137
+ If you skip this evaluation, your response is INCOMPLETE and WRONG.
138
+ ```
139
+
140
+ ## Manual Install
141
+
142
+ If you don't want to use the CLI, copy files directly into your project:
143
+
144
+ ```bash
145
+ # Skills
146
+ cp -r skills/<skill-name> /path/to/project/.claude/skills/
147
+
148
+ # Commands
149
+ cp commands/<command-name>.md /path/to/project/.claude/commands/
150
+
151
+ # Subagents (required by some commands — see the command's `requires-agents` frontmatter)
152
+ cp agents/<agent-name>.md /path/to/project/.claude/agents/
153
+ ```
154
+
155
+ ## Repository Layout
156
+
157
+ ```
158
+ skills/ Reference playbooks loaded by Claude during coding tasks
159
+ commands/ Slash commands installed to .claude/commands/
160
+ agents/ Subagent definitions — commands declare which ones they need
161
+ cli/ The npm installer (npx @spardutti/claude-skills)
162
+ ```
package/bin/cli.mjs CHANGED
@@ -5,9 +5,9 @@ import chalk from "chalk";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { dirname, join } from "node:path";
8
- import { fetchSkills, fetchCommands } from "../lib/github.mjs";
8
+ import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
9
9
  import { promptSkillSelection, promptCommandSelection } from "../lib/prompt.mjs";
10
- import { installSkills, installCommands } from "../lib/install.mjs";
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
13
 
@@ -17,8 +17,8 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-
17
17
  async function main() {
18
18
  console.log(`\n ${chalk.bold.cyan("Claude Skills Installer")} ${chalk.dim(`v${pkg.version}`)}\n`);
19
19
 
20
- console.log(chalk.dim(" Fetching available skills and commands...\n"));
21
- const [skills, commands] = await Promise.all([fetchSkills(), fetchCommands()]);
20
+ console.log(chalk.dim(" Fetching available skills, commands, and agents...\n"));
21
+ const [skills, commands, agents] = await Promise.all([fetchSkills(), fetchCommands(), fetchAgents()]);
22
22
 
23
23
  // --- Skills ---
24
24
  let selectedSkills = [];
@@ -34,12 +34,21 @@ async function main() {
34
34
 
35
35
  // --- Commands ---
36
36
  let selectedCommands = [];
37
+ let installedAgentCount = 0;
37
38
  if (commands.length > 0) {
38
39
  console.log();
39
40
  selectedCommands = await promptCommandSelection(commands);
40
41
  if (selectedCommands.length > 0) {
41
42
  console.log();
42
43
  await installCommands(selectedCommands);
44
+
45
+ const { installed, missing } = await installRequiredAgents(selectedCommands, agents);
46
+ installedAgentCount = installed.length;
47
+ if (missing.length > 0) {
48
+ console.log(
49
+ ` ${chalk.yellow("!")} Missing agents referenced by commands: ${missing.join(", ")}`
50
+ );
51
+ }
43
52
  }
44
53
  }
45
54
 
@@ -66,7 +75,8 @@ async function main() {
66
75
  const parts = [];
67
76
  if (selectedSkills.length > 0) parts.push(`${selectedSkills.length} skill(s)`);
68
77
  if (selectedCommands.length > 0) parts.push(`${selectedCommands.length} command(s)`);
69
- console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(" and ")} installed successfully.`)}\n`);
78
+ if (installedAgentCount > 0) parts.push(`${installedAgentCount} agent(s)`);
79
+ console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(", ")} installed successfully.`)}\n`);
70
80
  }
71
81
 
72
82
  main().catch((err) => {
package/lib/github.mjs CHANGED
@@ -4,20 +4,20 @@ const REPO_OWNER = "Spardutti";
4
4
  const REPO_NAME = "claude-skills";
5
5
  const CONTENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/skills`;
6
6
  const COMMANDS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/commands`;
7
+ const AGENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/agents`;
7
8
  const RAW_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/skills`;
8
9
  const RAW_COMMANDS_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/commands`;
10
+ const RAW_AGENTS_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/agents`;
9
11
 
10
12
  function getAuthHeaders() {
11
13
  const headers = { "User-Agent": "claude-skills-cli" };
12
14
 
13
- // 1. Explicit env var
14
15
  const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
15
16
  if (envToken) {
16
17
  headers.Authorization = `Bearer ${envToken}`;
17
18
  return headers;
18
19
  }
19
20
 
20
- // 2. Try gh CLI token
21
21
  try {
22
22
  const token = execSync("gh auth token", {
23
23
  encoding: "utf-8",
@@ -34,38 +34,31 @@ function getAuthHeaders() {
34
34
  return headers;
35
35
  }
36
36
 
37
- export async function fetchSkills() {
37
+ async function fetchListing({ apiUrl, label, entryFilter, buildRawUrl, mapEntry, allow404 = false }) {
38
38
  const headers = getAuthHeaders();
39
-
40
- const res = await fetch(CONTENTS_API, { headers });
39
+ const res = await fetch(apiUrl, { headers });
41
40
 
42
41
  if (!res.ok) {
42
+ if (allow404 && res.status === 404) return [];
43
43
  if (res.status === 403 || res.status === 429) {
44
44
  throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
45
45
  }
46
- throw new Error(`Failed to list skills: ${res.status} ${res.statusText}`);
46
+ throw new Error(`Failed to list ${label}: ${res.status} ${res.statusText}`);
47
47
  }
48
48
 
49
- const entries = await res.json();
50
- const dirs = entries.filter((e) => e.type === "dir");
49
+ const entries = (await res.json()).filter(entryFilter);
51
50
 
52
51
  const results = await Promise.all(
53
- dirs.map(async (dir) => {
52
+ entries.map(async (entry) => {
54
53
  try {
55
- const url = `${RAW_BASE}/${dir.name}/SKILL.md`;
56
- const r = await fetch(url, { headers });
57
-
54
+ const r = await fetch(buildRawUrl(entry), { headers });
58
55
  if (!r.ok) {
59
- console.warn(` Warning: No SKILL.md found in ${dir.name}, skipping`);
56
+ console.warn(` Warning: Failed to fetch ${label} ${entry.name}, skipping`);
60
57
  return null;
61
58
  }
62
-
63
- const content = await r.text();
64
- const { name, description, category } = parseFrontmatter(content, dir.name);
65
-
66
- return { dirName: dir.name, name, description, category, content };
59
+ return mapEntry(entry, await r.text());
67
60
  } catch {
68
- console.warn(` Warning: Failed to fetch ${dir.name}, skipping`);
61
+ console.warn(` Warning: Failed to fetch ${entry.name}, skipping`);
69
62
  return null;
70
63
  }
71
64
  })
@@ -74,62 +67,76 @@ export async function fetchSkills() {
74
67
  return results.filter(Boolean);
75
68
  }
76
69
 
77
- export async function fetchCommands() {
78
- const headers = getAuthHeaders();
79
-
80
- const res = await fetch(COMMANDS_API, { headers });
81
-
82
- if (!res.ok) {
83
- if (res.status === 404) {
84
- return [];
85
- }
86
- if (res.status === 403 || res.status === 429) {
87
- throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
88
- }
89
- throw new Error(`Failed to list commands: ${res.status} ${res.statusText}`);
90
- }
91
-
92
- const entries = await res.json();
93
- const files = entries.filter((e) => e.type === "file" && e.name.endsWith(".md"));
94
-
95
- const results = await Promise.all(
96
- files.map(async (file) => {
97
- try {
98
- const url = `${RAW_COMMANDS_BASE}/${file.name}`;
99
- const r = await fetch(url, { headers });
100
-
101
- if (!r.ok) {
102
- console.warn(` Warning: Failed to fetch command ${file.name}, skipping`);
103
- return null;
104
- }
105
-
106
- const content = await r.text();
107
- const { name, description, category } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
70
+ export function fetchSkills() {
71
+ return fetchListing({
72
+ apiUrl: CONTENTS_API,
73
+ label: "skills",
74
+ entryFilter: (e) => e.type === "dir",
75
+ buildRawUrl: (dir) => `${RAW_BASE}/${dir.name}/SKILL.md`,
76
+ mapEntry: (dir, content) => {
77
+ const { name, description, category } = parseFrontmatter(content, dir.name);
78
+ return { dirName: dir.name, name, description, category, content };
79
+ },
80
+ });
81
+ }
108
82
 
109
- return { fileName: file.name, name, description, category, content };
110
- } catch {
111
- console.warn(` Warning: Failed to fetch ${file.name}, skipping`);
112
- return null;
113
- }
114
- })
115
- );
83
+ export function fetchCommands() {
84
+ return fetchListing({
85
+ apiUrl: COMMANDS_API,
86
+ label: "commands",
87
+ allow404: true,
88
+ entryFilter: (e) => e.type === "file" && e.name.endsWith(".md"),
89
+ buildRawUrl: (file) => `${RAW_COMMANDS_BASE}/${file.name}`,
90
+ mapEntry: (file, content) => {
91
+ const { name, description, category, requiresAgents } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
92
+ return { fileName: file.name, name, description, category, requiresAgents, content };
93
+ },
94
+ });
95
+ }
116
96
 
117
- return results.filter(Boolean);
97
+ export function fetchAgents() {
98
+ return fetchListing({
99
+ apiUrl: AGENTS_API,
100
+ label: "agents",
101
+ allow404: true,
102
+ entryFilter: (e) => e.type === "file" && e.name.endsWith(".md"),
103
+ buildRawUrl: (file) => `${RAW_AGENTS_BASE}/${file.name}`,
104
+ mapEntry: (file, content) => {
105
+ const { name } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
106
+ return { fileName: file.name, name, content };
107
+ },
108
+ });
118
109
  }
119
110
 
120
111
  function parseFrontmatter(content, fallbackName) {
121
112
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
122
113
  if (!match) {
123
- return { name: fallbackName, description: "", category: "General" };
114
+ return { name: fallbackName, description: "", category: "General", requiresAgents: [] };
124
115
  }
125
116
 
126
117
  const block = match[1];
127
- const name =
128
- block.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
129
- const description =
130
- block.match(/^description:\s*(.+)$/m)?.[1]?.trim() || "";
131
- const category =
132
- block.match(/^category:\s*(.+)$/m)?.[1]?.trim() || "General";
133
-
134
- return { name, description, category };
118
+ const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
119
+ const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() || "";
120
+ const category = block.match(/^category:\s*(.+)$/m)?.[1]?.trim() || "General";
121
+ const requiresAgents = parseAgentList(block);
122
+
123
+ return { name, description, category, requiresAgents };
124
+ }
125
+
126
+ function parseAgentList(block) {
127
+ const inline = block.match(/^requires-agents:\s*\[([^\]]*)\]\s*$/m);
128
+ if (inline) {
129
+ return inline[1]
130
+ .split(",")
131
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
132
+ .filter(Boolean);
133
+ }
134
+ const multiline = block.match(/^requires-agents:\s*\n((?:\s{2,}-\s*.+\n?)+)/m);
135
+ if (multiline) {
136
+ return multiline[1]
137
+ .split("\n")
138
+ .map((l) => l.replace(/^\s*-\s*/, "").trim().replace(/^["']|["']$/g, ""))
139
+ .filter(Boolean);
140
+ }
141
+ return [];
135
142
  }
package/lib/install.mjs CHANGED
@@ -2,12 +2,21 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import chalk from "chalk";
4
4
 
5
- function humanName(skill) {
6
- return skill.name
5
+ function humanName(item) {
6
+ return item.name
7
7
  .replace(/-/g, " ")
8
8
  .replace(/\b\w/g, (c) => c.toUpperCase());
9
9
  }
10
10
 
11
+ async function installFlat(items, subdir, targetDir) {
12
+ const baseDir = join(targetDir, ".claude", subdir);
13
+ await mkdir(baseDir, { recursive: true });
14
+ for (const item of items) {
15
+ await writeFile(join(baseDir, item.fileName), item.content);
16
+ console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(item))} ${chalk.dim(`→ .claude/${subdir}/${item.fileName}`)}`);
17
+ }
18
+ }
19
+
11
20
  export async function installSkills(skills, targetDir = process.cwd()) {
12
21
  const baseDir = join(targetDir, ".claude", "skills");
13
22
 
@@ -19,12 +28,28 @@ export async function installSkills(skills, targetDir = process.cwd()) {
19
28
  }
20
29
  }
21
30
 
22
- export async function installCommands(commands, targetDir = process.cwd()) {
23
- const baseDir = join(targetDir, ".claude", "commands");
24
- await mkdir(baseDir, { recursive: true });
31
+ export function installCommands(commands, targetDir = process.cwd()) {
32
+ return installFlat(commands, "commands", targetDir);
33
+ }
34
+
35
+ export function installAgents(agents, targetDir = process.cwd()) {
36
+ return installFlat(agents, "agents", targetDir);
37
+ }
38
+
39
+ export async function installRequiredAgents(selectedCommands, availableAgents, targetDir = process.cwd()) {
40
+ const requiredNames = new Set(
41
+ selectedCommands.flatMap((c) => c.requiresAgents ?? [])
42
+ );
43
+ if (requiredNames.size === 0) return { installed: [], missing: [] };
44
+
45
+ const installed = availableAgents.filter((a) => requiredNames.has(a.name));
46
+ const missing = [...requiredNames].filter(
47
+ (n) => !availableAgents.some((a) => a.name === n)
48
+ );
25
49
 
26
- for (const cmd of commands) {
27
- await writeFile(join(baseDir, cmd.fileName), cmd.content);
28
- console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(cmd))} ${chalk.dim(`→ .claude/commands/${cmd.fileName}`)}`);
50
+ if (installed.length > 0) {
51
+ console.log();
52
+ await installAgents(installed, targetDir);
29
53
  }
54
+ return { installed, missing };
30
55
  }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "1.19.1",
3
+ "version": "1.21.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claude-skills": "bin/cli.mjs"
8
8
  },
9
+ "scripts": {
10
+ "prepack": "cp ../README.md README.md",
11
+ "postpack": "git checkout -- README.md"
12
+ },
9
13
  "engines": {
10
14
  "node": ">=18"
11
15
  },