@slamb2k/mad-skills 2.0.7 → 2.0.9

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/src/cli.js DELETED
@@ -1,482 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Claude Skills Installer CLI
5
- *
6
- * Usage:
7
- * npx @your-scope/claude-skills # Install all skills
8
- * npx @your-scope/claude-skills --list # List available skills
9
- * npx @your-scope/claude-skills --skill foo,bar # Install specific skills
10
- * npx @your-scope/claude-skills --target ./path # Custom install path
11
- * npx @your-scope/claude-skills --upgrade # Upgrade existing skills
12
- */
13
-
14
- import { readdir, readFile, mkdir, access, stat, chmod } from "node:fs/promises";
15
- import { existsSync } from "node:fs";
16
- import { execSync } from "node:child_process";
17
- import { resolve, join, dirname } from "node:path";
18
- import { fileURLToPath } from "node:url";
19
- import { parseArgs } from "node:util";
20
-
21
- const __dirname = dirname(fileURLToPath(import.meta.url));
22
- const SKILLS_SRC = resolve(__dirname, "..", "skills");
23
- const SUPPLEMENTARY_DIRS = ["commands", "agents", "hooks"];
24
-
25
- const DEFAULT_TARGETS = [
26
- ".claude/skills", // Project-level (preferred)
27
- join(process.env.HOME ?? "~", ".claude", "skills"), // User-level fallback
28
- ];
29
-
30
- const { values: args } = parseArgs({
31
- options: {
32
- list: { type: "boolean", default: false },
33
- skill: { type: "string", default: "" },
34
- target: { type: "string", default: "" },
35
- upgrade: { type: "boolean", default: false },
36
- force: { type: "boolean", short: "f", default: false },
37
- help: { type: "boolean", short: "h", default: false },
38
- },
39
- strict: true,
40
- });
41
-
42
- if (args.help) {
43
- console.log(`
44
- Claude Skills Installer
45
-
46
- Installs skills, slash commands, agents, and hooks to your .claude directory.
47
-
48
- Usage:
49
- npx @slamb2k/mad-skills [options]
50
-
51
- Options:
52
- --list List available skills with descriptions
53
- --skill <names> Comma-separated skill names to install (default: all)
54
- --target <path> Installation directory (default: .claude/skills)
55
- --upgrade Overwrite existing skills and supplementary files
56
- --force, -f Skip confirmation prompts
57
- --help, -h Show this help
58
-
59
- Examples:
60
- npx @slamb2k/mad-skills --list
61
- npx @slamb2k/mad-skills --skill build,ship
62
- npx @slamb2k/mad-skills --target ./my-project/.claude/skills --upgrade
63
- `);
64
- process.exit(0);
65
- }
66
-
67
- async function discoverSkills() {
68
- const entries = await readdir(SKILLS_SRC, { withFileTypes: true });
69
- const skills = [];
70
-
71
- for (const entry of entries) {
72
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
73
-
74
- const skillMd = join(SKILLS_SRC, entry.name, "SKILL.md");
75
- try {
76
- await access(skillMd);
77
- const content = await readFile(skillMd, "utf-8");
78
- const frontmatter = parseFrontmatter(content);
79
-
80
- skills.push({
81
- name: entry.name,
82
- displayName: frontmatter.name ?? entry.name,
83
- description: frontmatter.description ?? "(no description)",
84
- path: join(SKILLS_SRC, entry.name),
85
- });
86
- } catch {
87
- // Skip directories without SKILL.md
88
- }
89
- }
90
-
91
- return skills;
92
- }
93
-
94
- function parseFrontmatter(content) {
95
- const match = content.match(/^---\n([\s\S]*?)\n---/);
96
- if (!match) return {};
97
-
98
- const result = {};
99
- for (const line of match[1].split("\n")) {
100
- const colonIdx = line.indexOf(":");
101
- if (colonIdx === -1) continue;
102
- const key = line.slice(0, colonIdx).trim();
103
- const value = line.slice(colonIdx + 1).trim();
104
- result[key] = value;
105
- }
106
- return result;
107
- }
108
-
109
- async function resolveTarget() {
110
- if (args.target) return resolve(args.target);
111
-
112
- // Prefer project-level if we're in a git repo or have .claude dir
113
- for (const candidate of DEFAULT_TARGETS) {
114
- const resolved = resolve(candidate);
115
- try {
116
- await access(dirname(resolved));
117
- return resolved;
118
- } catch {
119
- continue;
120
- }
121
- }
122
-
123
- return resolve(DEFAULT_TARGETS[0]);
124
- }
125
-
126
- async function exists(path) {
127
- try {
128
- await access(path);
129
- return true;
130
- } catch {
131
- return false;
132
- }
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // Dependency management
137
- // ---------------------------------------------------------------------------
138
-
139
- const VALID_DEP_TYPES = new Set(["cli", "npm", "agent", "skill", "plugin"]);
140
- const VALID_DEP_RESOLUTIONS = new Set(["url", "install", "ask", "fallback", "stop"]);
141
-
142
- /**
143
- * Parse a markdown dependency table from instructions.md content.
144
- * Returns an array of { name, type, check, required, resolution, detail }.
145
- */
146
- function parseDependencyTable(content) {
147
- const lines = content.split("\n");
148
- const deps = [];
149
-
150
- const headerIdx = lines.findIndex((l) =>
151
- /^\|\s*Dependency\s*\|/i.test(l)
152
- );
153
- if (headerIdx === -1) return deps;
154
-
155
- for (let i = headerIdx + 2; i < lines.length; i++) {
156
- const line = lines[i].trim();
157
- if (!line.startsWith("|")) break;
158
-
159
- const cells = line
160
- .split("|")
161
- .slice(1, -1)
162
- .map((c) => c.trim());
163
- if (cells.length < 6) continue;
164
-
165
- const check = cells[2].replace(/^`|`$/g, "");
166
- deps.push({
167
- name: cells[0],
168
- type: cells[1],
169
- check: check === "—" || check === "-" || check === "" ? null : check,
170
- required: cells[3].toLowerCase() === "yes",
171
- resolution: cells[4],
172
- detail: cells[5].replace(/^`|`$/g, ""),
173
- });
174
- }
175
-
176
- return deps;
177
- }
178
-
179
- /**
180
- * Check whether a single dependency is available.
181
- * Returns true if found, false if missing.
182
- */
183
- function checkDependency(dep, targetDir) {
184
- if (!dep.check) return true;
185
-
186
- try {
187
- switch (dep.type) {
188
- case "cli":
189
- case "npm":
190
- execSync(dep.check, { stdio: "ignore", timeout: 15000 });
191
- return true;
192
- case "agent":
193
- case "skill": {
194
- const p = dep.check.replace(/^~/, process.env.HOME ?? "~");
195
- if (existsSync(resolve(p))) return true;
196
- // For skill deps, also check inside the install target directory
197
- if (targetDir && dep.type === "skill") {
198
- const parts = dep.check.split("/");
199
- const idx = parts.indexOf("skills");
200
- if (idx !== -1 && idx + 1 < parts.length) {
201
- const targetPath = join(targetDir, parts[idx + 1], "SKILL.md");
202
- if (existsSync(targetPath)) return true;
203
- }
204
- }
205
- return false;
206
- }
207
- case "plugin":
208
- return false;
209
- default:
210
- return true;
211
- }
212
- } catch {
213
- return false;
214
- }
215
- }
216
-
217
- /**
218
- * Apply the resolution strategy for a missing dependency.
219
- * Returns "installed" | "warning" | "info".
220
- */
221
- function resolveDependency(dep) {
222
- switch (dep.resolution) {
223
- case "stop":
224
- case "url": {
225
- const icon = dep.required ? "⚠" : "ℹ";
226
- console.log(` ${icon} ${dep.name} not found — ${dep.detail}`);
227
- return "warning";
228
- }
229
- case "install": {
230
- try {
231
- execSync(dep.detail, { stdio: "pipe", timeout: 60000 });
232
- console.log(` ✅ ${dep.name} installed (ran: ${dep.detail})`);
233
- return "installed";
234
- } catch {
235
- console.log(
236
- ` ⚠ ${dep.name} auto-install failed — run manually: ${dep.detail}`
237
- );
238
- return "warning";
239
- }
240
- }
241
- case "ask": {
242
- console.log(
243
- ` ℹ ${dep.name} not found — optional, install with: ${dep.detail}`
244
- );
245
- return "info";
246
- }
247
- case "fallback": {
248
- console.log(` ℹ ${dep.name} not found — ${dep.detail}`);
249
- return "info";
250
- }
251
- default:
252
- return "ok";
253
- }
254
- }
255
-
256
- /**
257
- * Check all dependencies declared in a skill's instructions.md.
258
- * Returns { installed, warnings } counts.
259
- */
260
- async function checkSkillDependencies(skillName, targetDir) {
261
- const instructionsPath = join(targetDir, skillName, "instructions.md");
262
- let content;
263
- try {
264
- content = await readFile(instructionsPath, "utf-8");
265
- } catch {
266
- return { installed: 0, warnings: 0 };
267
- }
268
-
269
- const deps = parseDependencyTable(content);
270
- if (deps.length === 0) return { installed: 0, warnings: 0 };
271
-
272
- let installed = 0;
273
- let warnCount = 0;
274
-
275
- for (const dep of deps) {
276
- if (checkDependency(dep, targetDir)) continue;
277
- const result = resolveDependency(dep);
278
- if (result === "installed") installed++;
279
- if (result === "warning") warnCount++;
280
- }
281
-
282
- return { installed, warnings: warnCount };
283
- }
284
-
285
- async function installSkill(skill, targetDir) {
286
- const dest = join(targetDir, skill.name);
287
- const destExists = await exists(dest);
288
-
289
- if (destExists && !args.upgrade) {
290
- return { name: skill.name, status: "skipped" };
291
- }
292
-
293
- // Copy skill, filtering out CI-only directories
294
- await cpFiltered(skill.path, dest);
295
-
296
- return { name: skill.name, status: destExists ? "upgraded" : "installed" };
297
- }
298
-
299
- /**
300
- * Recursively copies a skill directory, excluding CI/test artefacts
301
- * that consumers don't need. Matches the .skill package exclusions.
302
- */
303
- const INSTALL_EXCLUDE_DIRS = new Set([
304
- "tests",
305
- "evals",
306
- "__pycache__",
307
- "node_modules",
308
- ".git",
309
- ]);
310
- const INSTALL_EXCLUDE_FILES = new Set([".DS_Store", ".gitkeep"]);
311
-
312
- async function cpFiltered(src, dest) {
313
- await mkdir(dest, { recursive: true });
314
- const entries = await readdir(src, { withFileTypes: true });
315
-
316
- for (const entry of entries) {
317
- const srcPath = join(src, entry.name);
318
- const destPath = join(dest, entry.name);
319
-
320
- if (entry.isDirectory()) {
321
- if (INSTALL_EXCLUDE_DIRS.has(entry.name)) continue;
322
- await cpFiltered(srcPath, destPath);
323
- } else if (entry.isFile()) {
324
- if (INSTALL_EXCLUDE_FILES.has(entry.name)) continue;
325
- if (entry.name.endsWith(".pyc")) continue;
326
- const { copyFile } = await import("node:fs/promises");
327
- await copyFile(srcPath, destPath);
328
- }
329
- }
330
- }
331
-
332
- /**
333
- * Install supplementary directories (commands, agents, hooks) alongside skills.
334
- * Target dirs are siblings of the skills target dir (e.g. ~/.claude/commands).
335
- */
336
- async function installSupplementary(targetDir) {
337
- const baseDir = dirname(targetDir);
338
- const results = { installed: 0, upgraded: 0, skipped: 0 };
339
-
340
- for (const dir of SUPPLEMENTARY_DIRS) {
341
- const src = resolve(__dirname, "..", dir);
342
- const dest = join(baseDir, dir);
343
-
344
- if (!existsSync(src)) continue;
345
-
346
- const entries = await readdir(src, { withFileTypes: true });
347
- await mkdir(dest, { recursive: true });
348
-
349
- for (const entry of entries) {
350
- if (!entry.isFile()) continue;
351
- if (entry.name.startsWith(".")) continue;
352
-
353
- const srcPath = join(src, entry.name);
354
- const destPath = join(dest, entry.name);
355
- const destExists = await exists(destPath);
356
-
357
- if (destExists && !args.upgrade) {
358
- results.skipped++;
359
- continue;
360
- }
361
-
362
- const { copyFile } = await import("node:fs/promises");
363
- await copyFile(srcPath, destPath);
364
-
365
- // Make shell scripts executable
366
- if (entry.name.endsWith(".sh")) {
367
- await chmod(destPath, 0o755);
368
- }
369
-
370
- if (destExists) {
371
- results.upgraded++;
372
- } else {
373
- results.installed++;
374
- }
375
- }
376
- }
377
-
378
- return results;
379
- }
380
-
381
- async function main() {
382
- const skills = await discoverSkills();
383
-
384
- if (skills.length === 0) {
385
- console.error("No skills found in package. This is likely a packaging error.");
386
- process.exit(1);
387
- }
388
-
389
- // --list mode
390
- if (args.list) {
391
- console.log("\nAvailable skills:\n");
392
- const maxName = Math.max(...skills.map((s) => s.name.length));
393
- for (const skill of skills) {
394
- const desc =
395
- skill.description.length > 80
396
- ? skill.description.slice(0, 77) + "..."
397
- : skill.description;
398
- console.log(` ${skill.name.padEnd(maxName + 2)} ${desc}`);
399
- }
400
- console.log(`\n${skills.length} skill(s) available\n`);
401
- process.exit(0);
402
- }
403
-
404
- // Filter skills if --skill specified
405
- let toInstall = skills;
406
- if (args.skill) {
407
- const requested = new Set(args.skill.split(",").map((s) => s.trim()));
408
- toInstall = skills.filter((s) => requested.has(s.name));
409
- const found = new Set(toInstall.map((s) => s.name));
410
- const missing = [...requested].filter((r) => !found.has(r));
411
- if (missing.length > 0) {
412
- console.error(`Unknown skills: ${missing.join(", ")}`);
413
- console.error(`Use --list to see available skills`);
414
- process.exit(1);
415
- }
416
- }
417
-
418
- const targetDir = await resolveTarget();
419
- await mkdir(targetDir, { recursive: true });
420
-
421
- console.log(`\nInstalling ${toInstall.length} skill(s) to ${targetDir}\n`);
422
-
423
- // Pass 1: Install all skills (file copy)
424
- const results = [];
425
- for (const skill of toInstall) {
426
- results.push(await installSkill(skill, targetDir));
427
- }
428
-
429
- // Pass 2: Print results and check dependencies
430
- let totalDepsInstalled = 0;
431
- let totalDepsWarnings = 0;
432
-
433
- for (const result of results) {
434
- if (result.status === "skipped") {
435
- console.log(` ⏭ ${result.name} (exists, use --upgrade to overwrite)`);
436
- } else {
437
- console.log(` ✅ ${result.name} (${result.status})`);
438
- const depResult = await checkSkillDependencies(result.name, targetDir);
439
- totalDepsInstalled += depResult.installed;
440
- totalDepsWarnings += depResult.warnings;
441
- }
442
- }
443
-
444
- const installed = results.filter((r) => r.status === "installed").length;
445
- const upgraded = results.filter((r) => r.status === "upgraded").length;
446
- const skipped = results.filter((r) => r.status === "skipped").length;
447
-
448
- console.log(
449
- `\nDone: ${installed} installed, ${upgraded} upgraded, ${skipped} skipped`
450
- );
451
- if (totalDepsInstalled > 0 || totalDepsWarnings > 0) {
452
- const parts = [];
453
- if (totalDepsInstalled > 0) {
454
- parts.push(
455
- `${totalDepsInstalled} ${totalDepsInstalled === 1 ? "dependency" : "dependencies"} installed`
456
- );
457
- }
458
- if (totalDepsWarnings > 0) {
459
- parts.push(
460
- `${totalDepsWarnings} ${totalDepsWarnings === 1 ? "warning" : "warnings"}`
461
- );
462
- }
463
- console.log(` ${parts.join(", ")}`);
464
- }
465
-
466
- // Pass 3: Install supplementary files (commands, agents, hooks)
467
- const suppResults = await installSupplementary(targetDir);
468
- const suppTotal = suppResults.installed + suppResults.upgraded + suppResults.skipped;
469
-
470
- if (suppTotal > 0) {
471
- console.log(
472
- `\nSupplementary files: ${suppResults.installed} installed, ${suppResults.upgraded} upgraded, ${suppResults.skipped} skipped`
473
- );
474
- }
475
-
476
- console.log();
477
- }
478
-
479
- main().catch((err) => {
480
- console.error("Fatal:", err.message);
481
- process.exit(1);
482
- });