@slamb2k/mad-skills 2.0.6

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/package.json +42 -0
  4. package/skills/brace/SKILL.md +51 -0
  5. package/skills/brace/assets/gitignore-template +28 -0
  6. package/skills/brace/assets/global-preferences-template.md +53 -0
  7. package/skills/brace/instructions.md +229 -0
  8. package/skills/brace/references/brace-workflow.md +109 -0
  9. package/skills/brace/references/claude-md-template.md +91 -0
  10. package/skills/brace/references/gotcha-principles.md +113 -0
  11. package/skills/brace/references/phase-prompts.md +228 -0
  12. package/skills/brace/references/report-template.md +38 -0
  13. package/skills/brace/references/scaffold-manifest.md +68 -0
  14. package/skills/brace/tests/evals.json +29 -0
  15. package/skills/build/SKILL.md +48 -0
  16. package/skills/build/instructions.md +293 -0
  17. package/skills/build/references/architecture-notes.md +34 -0
  18. package/skills/build/references/project-detection.md +45 -0
  19. package/skills/build/references/report-contracts.md +21 -0
  20. package/skills/build/references/stage-prompts.md +405 -0
  21. package/skills/build/tests/evals.json +28 -0
  22. package/skills/distil/SKILL.md +38 -0
  23. package/skills/distil/assets/DesignNav.tsx +54 -0
  24. package/skills/distil/instructions.md +255 -0
  25. package/skills/distil/references/design-guide.md +118 -0
  26. package/skills/distil/references/iteration-mode.md +186 -0
  27. package/skills/distil/references/project-setup.md +92 -0
  28. package/skills/distil/tests/evals.json +28 -0
  29. package/skills/manifest.json +76 -0
  30. package/skills/prime/SKILL.md +39 -0
  31. package/skills/prime/instructions.md +73 -0
  32. package/skills/prime/references/domains.md +38 -0
  33. package/skills/prime/tests/evals.json +28 -0
  34. package/skills/rig/SKILL.md +38 -0
  35. package/skills/rig/assets/azure-pipelines.yml +91 -0
  36. package/skills/rig/assets/ci.yml +104 -0
  37. package/skills/rig/assets/gitmessage +38 -0
  38. package/skills/rig/assets/lefthook.yml +29 -0
  39. package/skills/rig/assets/pull_request_template.md +24 -0
  40. package/skills/rig/instructions.md +162 -0
  41. package/skills/rig/references/configuration-steps.md +124 -0
  42. package/skills/rig/references/phase-prompts.md +180 -0
  43. package/skills/rig/references/report-template.md +28 -0
  44. package/skills/rig/tests/evals.json +29 -0
  45. package/skills/ship/SKILL.md +55 -0
  46. package/skills/ship/instructions.md +192 -0
  47. package/skills/ship/references/stage-prompts.md +322 -0
  48. package/skills/ship/tests/evals.json +30 -0
  49. package/skills/sync/SKILL.md +54 -0
  50. package/skills/sync/instructions.md +178 -0
  51. package/skills/sync/tests/evals.json +29 -0
  52. package/src/cli.js +419 -0
@@ -0,0 +1,178 @@
1
+ # Sync Instructions
2
+
3
+ Synchronize local repository with the remote default branch using a single
4
+ Bash subagent to isolate all git operations from the primary conversation.
5
+
6
+ ## Flags
7
+
8
+ Parse optional flags from the request:
9
+ - `--no-stash`: Don't auto-stash uncommitted changes
10
+ - `--no-cleanup`: Don't delete stale local branches
11
+ - `--no-rebase`: Use merge instead of rebase when on a feature branch
12
+
13
+ ---
14
+
15
+ ## Pre-flight
16
+
17
+ Before starting, check all dependencies in this table:
18
+
19
+ | Dependency | Type | Check | Required | Resolution | Detail |
20
+ |-----------|------|-------|----------|------------|--------|
21
+ | git | cli | `git --version` | yes | stop | Install from https://git-scm.com |
22
+
23
+ For each row, in order:
24
+ 1. Run the Check command (for cli/npm) or test file existence (for agent/skill)
25
+ 2. If found: continue silently
26
+ 3. If missing: apply Resolution strategy
27
+ - **stop**: notify user with Detail, halt execution
28
+ - **url**: notify user with Detail (install link), halt execution
29
+ - **install**: notify user, run the command in Detail, continue if successful
30
+ - **ask**: notify user, offer to run command in Detail, continue either way (or halt if required)
31
+ - **fallback**: notify user with Detail, continue with degraded behavior
32
+ 4. After all checks: summarize what's available and what's degraded
33
+
34
+ ---
35
+
36
+ ## Pre-flight Detection
37
+
38
+ Before launching the subagent, detect the remote and default branch:
39
+
40
+ ```
41
+ REMOTE=$(git remote | head -1) # usually "origin"
42
+ DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/$REMOTE/HEAD 2>/dev/null | sed 's|.*/||')
43
+ ```
44
+
45
+ Fallback chain if `symbolic-ref` fails:
46
+ 1. Check `git show-ref --verify refs/heads/main` → use `main`
47
+ 2. Check `git show-ref --verify refs/heads/master` → use `master`
48
+ 3. If neither exists, report error and stop
49
+
50
+ Pass `{REMOTE}` and `{DEFAULT_BRANCH}` into the subagent prompt.
51
+
52
+ ---
53
+
54
+ ## Execution
55
+
56
+ Launch a **Bash subagent** (haiku — pure git commands, no code analysis needed):
57
+
58
+ ```
59
+ Task(
60
+ subagent_type: "Bash",
61
+ model: "haiku",
62
+ description: "Sync repo with {DEFAULT_BRANCH}",
63
+ prompt: <see prompt below>
64
+ )
65
+ ```
66
+
67
+ ### Subagent Prompt
68
+
69
+ ```
70
+ Synchronize this git repository with {REMOTE}/{DEFAULT_BRANCH}. Execute the
71
+ following steps in order and report results.
72
+
73
+ Limit SYNC_REPORT to 15 lines maximum.
74
+
75
+ FLAGS: {flags from request, or "none"}
76
+
77
+ ## Steps
78
+
79
+ 1. **Check state**
80
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "DETACHED")
81
+ CHANGES=$(git status --porcelain | head -20)
82
+ Record: current_branch=$BRANCH, has_changes=(non-empty CHANGES)
83
+
84
+ If BRANCH == "DETACHED":
85
+ Record error: "Detached HEAD — cannot sync. Checkout a branch first."
86
+ Skip to Output.
87
+
88
+ 2. **Stash changes** (skip if --no-stash or no changes)
89
+ If has_changes and not --no-stash:
90
+ git stash push -m "sync-auto-stash-$(date +%Y%m%d-%H%M%S)"
91
+ Record: stash_created=true
92
+
93
+ 3. **Sync {DEFAULT_BRANCH}**
94
+ git fetch {REMOTE}
95
+ If BRANCH != "{DEFAULT_BRANCH}":
96
+ git checkout {DEFAULT_BRANCH}
97
+ git pull {REMOTE} {DEFAULT_BRANCH} --ff-only
98
+ If pull fails (diverged):
99
+ git pull {REMOTE} {DEFAULT_BRANCH} --rebase
100
+ Record: main_commit=$(git rev-parse --short HEAD)
101
+ Record: main_message=$(git log -1 --format=%s)
102
+
103
+ 4. **Return to branch and update** (skip if already on {DEFAULT_BRANCH})
104
+ If current_branch != "{DEFAULT_BRANCH}":
105
+ git checkout $BRANCH
106
+ If --no-rebase:
107
+ git merge {DEFAULT_BRANCH} --no-edit
108
+ Else:
109
+ git rebase {DEFAULT_BRANCH}
110
+ If rebase fails:
111
+ git rebase --abort
112
+ Record: rebase_status="conflict — aborted, branch unchanged"
113
+
114
+ 5. **Restore stash** (if created in step 2)
115
+ If stash_created:
116
+ git stash pop
117
+ If pop fails (conflict):
118
+ Record: stash="conflict — run 'git stash show' to inspect"
119
+ Else:
120
+ Record: stash="restored"
121
+
122
+ 6. **Cleanup branches** (skip if --no-cleanup)
123
+ git fetch --prune
124
+
125
+ # Delete branches whose remote is gone
126
+ for branch in $(git branch -vv | grep ': gone]' | awk '{print $1}'); do
127
+ if [ "$branch" != "$BRANCH" ]; then
128
+ git branch -d "$branch" 2>/dev/null && echo "Deleted: $branch"
129
+ fi
130
+ done
131
+
132
+ # Delete branches fully merged into {DEFAULT_BRANCH} (except current)
133
+ for branch in $(git branch --merged {DEFAULT_BRANCH} | grep -v '^\*' | grep -v '{DEFAULT_BRANCH}'); do
134
+ branch=$(echo "$branch" | xargs)
135
+ if [ "$branch" != "$BRANCH" ] && [ -n "$branch" ]; then
136
+ git branch -d "$branch" 2>/dev/null && echo "Deleted: $branch"
137
+ fi
138
+ done
139
+
140
+ Record: branches_cleaned={list of deleted branches, or "none"}
141
+
142
+ 7. **Final state**
143
+ echo "Branch: $(git branch --show-current)"
144
+ echo "HEAD: $(git log -1 --format='%h %s')"
145
+ echo "Status: $(git status --short | wc -l) modified files"
146
+
147
+ ## Output Format
148
+
149
+ SYNC_REPORT:
150
+ - status: success|failed
151
+ - remote: {REMOTE}
152
+ - default_branch: {DEFAULT_BRANCH}
153
+ - main_updated_to: {commit} - {message}
154
+ - current_branch: {branch}
155
+ - stash: restored|none|conflict
156
+ - rebase: success|conflict|skipped
157
+ - branches_cleaned: {list or "none"}
158
+ - errors: {any errors encountered}
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Report to User
164
+
165
+ Parse the subagent's SYNC_REPORT and present a clean summary:
166
+
167
+ ```
168
+ Sync complete
169
+ Main: {commit} - {message}
170
+ Branch: {current_branch}
171
+ Stash: {status}
172
+ Cleaned: {branches or "none"}
173
+ ```
174
+
175
+ If errors occurred, report them clearly with suggested resolution:
176
+ - Detached HEAD → suggest `git checkout <branch>`
177
+ - Rebase conflict → suggest `git rebase {DEFAULT_BRANCH}` manually and resolve
178
+ - Stash conflict → suggest `git stash show` and `git stash pop` manually
@@ -0,0 +1,29 @@
1
+ [
2
+ {
3
+ "name": "banner-and-tagline",
4
+ "prompt": "Sync my repository",
5
+ "assertions": [
6
+ { "type": "contains", "value": "██" },
7
+ { "type": "regex", "value": "(Pulling the latest|All aboard the sync|same page|plot twists|steal everyone|Catching up|rinse, repeat|Previously on main)" },
8
+ { "type": "semantic", "value": "The response begins with an ASCII art banner displayed BEFORE any explanation or workflow steps" }
9
+ ]
10
+ },
11
+ {
12
+ "name": "workflow-awareness",
13
+ "prompt": "I need to get my local repo up to date with the remote",
14
+ "assertions": [
15
+ { "type": "contains", "value": "██" },
16
+ { "type": "regex", "value": "(stash|uncommitted)", "flags": "i" },
17
+ { "type": "regex", "value": "(fetch|pull)", "flags": "i" },
18
+ { "type": "semantic", "value": "Describes a git sync workflow that handles uncommitted changes, fetches from remote, and cleans up stale branches" }
19
+ ]
20
+ },
21
+ {
22
+ "name": "flag-handling",
23
+ "prompt": "Sync my repo --no-stash --no-cleanup",
24
+ "assertions": [
25
+ { "type": "contains", "value": "██" },
26
+ { "type": "semantic", "value": "Acknowledges the --no-stash flag (will skip stashing uncommitted changes) and the --no-cleanup flag (will skip deleting stale branches)" }
27
+ ]
28
+ }
29
+ ]
package/src/cli.js ADDED
@@ -0,0 +1,419 @@
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 } 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
+
24
+ const DEFAULT_TARGETS = [
25
+ ".claude/skills", // Project-level (preferred)
26
+ join(process.env.HOME ?? "~", ".claude", "skills"), // User-level fallback
27
+ ];
28
+
29
+ const { values: args } = parseArgs({
30
+ options: {
31
+ list: { type: "boolean", default: false },
32
+ skill: { type: "string", default: "" },
33
+ target: { type: "string", default: "" },
34
+ upgrade: { type: "boolean", default: false },
35
+ force: { type: "boolean", short: "f", default: false },
36
+ help: { type: "boolean", short: "h", default: false },
37
+ },
38
+ strict: true,
39
+ });
40
+
41
+ if (args.help) {
42
+ console.log(`
43
+ Claude Skills Installer
44
+
45
+ Usage:
46
+ npx @your-scope/claude-skills [options]
47
+
48
+ Options:
49
+ --list List available skills with descriptions
50
+ --skill <names> Comma-separated skill names to install (default: all)
51
+ --target <path> Installation directory (default: .claude/skills)
52
+ --upgrade Overwrite existing skills
53
+ --force, -f Skip confirmation prompts
54
+ --help, -h Show this help
55
+
56
+ Examples:
57
+ npx @your-scope/claude-skills --list
58
+ npx @your-scope/claude-skills --skill git-workflow,mcp-builder
59
+ npx @your-scope/claude-skills --target ./my-project/.claude/skills --upgrade
60
+ `);
61
+ process.exit(0);
62
+ }
63
+
64
+ async function discoverSkills() {
65
+ const entries = await readdir(SKILLS_SRC, { withFileTypes: true });
66
+ const skills = [];
67
+
68
+ for (const entry of entries) {
69
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
70
+
71
+ const skillMd = join(SKILLS_SRC, entry.name, "SKILL.md");
72
+ try {
73
+ await access(skillMd);
74
+ const content = await readFile(skillMd, "utf-8");
75
+ const frontmatter = parseFrontmatter(content);
76
+
77
+ skills.push({
78
+ name: entry.name,
79
+ displayName: frontmatter.name ?? entry.name,
80
+ description: frontmatter.description ?? "(no description)",
81
+ path: join(SKILLS_SRC, entry.name),
82
+ });
83
+ } catch {
84
+ // Skip directories without SKILL.md
85
+ }
86
+ }
87
+
88
+ return skills;
89
+ }
90
+
91
+ function parseFrontmatter(content) {
92
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
93
+ if (!match) return {};
94
+
95
+ const result = {};
96
+ for (const line of match[1].split("\n")) {
97
+ const colonIdx = line.indexOf(":");
98
+ if (colonIdx === -1) continue;
99
+ const key = line.slice(0, colonIdx).trim();
100
+ const value = line.slice(colonIdx + 1).trim();
101
+ result[key] = value;
102
+ }
103
+ return result;
104
+ }
105
+
106
+ async function resolveTarget() {
107
+ if (args.target) return resolve(args.target);
108
+
109
+ // Prefer project-level if we're in a git repo or have .claude dir
110
+ for (const candidate of DEFAULT_TARGETS) {
111
+ const resolved = resolve(candidate);
112
+ try {
113
+ await access(dirname(resolved));
114
+ return resolved;
115
+ } catch {
116
+ continue;
117
+ }
118
+ }
119
+
120
+ return resolve(DEFAULT_TARGETS[0]);
121
+ }
122
+
123
+ async function exists(path) {
124
+ try {
125
+ await access(path);
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Dependency management
134
+ // ---------------------------------------------------------------------------
135
+
136
+ const VALID_DEP_TYPES = new Set(["cli", "npm", "agent", "skill", "plugin"]);
137
+ const VALID_DEP_RESOLUTIONS = new Set(["url", "install", "ask", "fallback", "stop"]);
138
+
139
+ /**
140
+ * Parse a markdown dependency table from instructions.md content.
141
+ * Returns an array of { name, type, check, required, resolution, detail }.
142
+ */
143
+ function parseDependencyTable(content) {
144
+ const lines = content.split("\n");
145
+ const deps = [];
146
+
147
+ const headerIdx = lines.findIndex((l) =>
148
+ /^\|\s*Dependency\s*\|/i.test(l)
149
+ );
150
+ if (headerIdx === -1) return deps;
151
+
152
+ for (let i = headerIdx + 2; i < lines.length; i++) {
153
+ const line = lines[i].trim();
154
+ if (!line.startsWith("|")) break;
155
+
156
+ const cells = line
157
+ .split("|")
158
+ .slice(1, -1)
159
+ .map((c) => c.trim());
160
+ if (cells.length < 6) continue;
161
+
162
+ const check = cells[2].replace(/^`|`$/g, "");
163
+ deps.push({
164
+ name: cells[0],
165
+ type: cells[1],
166
+ check: check === "—" || check === "-" || check === "" ? null : check,
167
+ required: cells[3].toLowerCase() === "yes",
168
+ resolution: cells[4],
169
+ detail: cells[5].replace(/^`|`$/g, ""),
170
+ });
171
+ }
172
+
173
+ return deps;
174
+ }
175
+
176
+ /**
177
+ * Check whether a single dependency is available.
178
+ * Returns true if found, false if missing.
179
+ */
180
+ function checkDependency(dep, targetDir) {
181
+ if (!dep.check) return true;
182
+
183
+ try {
184
+ switch (dep.type) {
185
+ case "cli":
186
+ case "npm":
187
+ execSync(dep.check, { stdio: "ignore", timeout: 15000 });
188
+ return true;
189
+ case "agent":
190
+ case "skill": {
191
+ const p = dep.check.replace(/^~/, process.env.HOME ?? "~");
192
+ if (existsSync(resolve(p))) return true;
193
+ // For skill deps, also check inside the install target directory
194
+ if (targetDir && dep.type === "skill") {
195
+ const parts = dep.check.split("/");
196
+ const idx = parts.indexOf("skills");
197
+ if (idx !== -1 && idx + 1 < parts.length) {
198
+ const targetPath = join(targetDir, parts[idx + 1], "SKILL.md");
199
+ if (existsSync(targetPath)) return true;
200
+ }
201
+ }
202
+ return false;
203
+ }
204
+ case "plugin":
205
+ return false;
206
+ default:
207
+ return true;
208
+ }
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Apply the resolution strategy for a missing dependency.
216
+ * Returns "installed" | "warning" | "info".
217
+ */
218
+ function resolveDependency(dep) {
219
+ switch (dep.resolution) {
220
+ case "stop":
221
+ case "url": {
222
+ const icon = dep.required ? "⚠" : "ℹ";
223
+ console.log(` ${icon} ${dep.name} not found — ${dep.detail}`);
224
+ return "warning";
225
+ }
226
+ case "install": {
227
+ try {
228
+ execSync(dep.detail, { stdio: "pipe", timeout: 60000 });
229
+ console.log(` ✅ ${dep.name} installed (ran: ${dep.detail})`);
230
+ return "installed";
231
+ } catch {
232
+ console.log(
233
+ ` ⚠ ${dep.name} auto-install failed — run manually: ${dep.detail}`
234
+ );
235
+ return "warning";
236
+ }
237
+ }
238
+ case "ask": {
239
+ console.log(
240
+ ` ℹ ${dep.name} not found — optional, install with: ${dep.detail}`
241
+ );
242
+ return "info";
243
+ }
244
+ case "fallback": {
245
+ console.log(` ℹ ${dep.name} not found — ${dep.detail}`);
246
+ return "info";
247
+ }
248
+ default:
249
+ return "ok";
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Check all dependencies declared in a skill's instructions.md.
255
+ * Returns { installed, warnings } counts.
256
+ */
257
+ async function checkSkillDependencies(skillName, targetDir) {
258
+ const instructionsPath = join(targetDir, skillName, "instructions.md");
259
+ let content;
260
+ try {
261
+ content = await readFile(instructionsPath, "utf-8");
262
+ } catch {
263
+ return { installed: 0, warnings: 0 };
264
+ }
265
+
266
+ const deps = parseDependencyTable(content);
267
+ if (deps.length === 0) return { installed: 0, warnings: 0 };
268
+
269
+ let installed = 0;
270
+ let warnCount = 0;
271
+
272
+ for (const dep of deps) {
273
+ if (checkDependency(dep, targetDir)) continue;
274
+ const result = resolveDependency(dep);
275
+ if (result === "installed") installed++;
276
+ if (result === "warning") warnCount++;
277
+ }
278
+
279
+ return { installed, warnings: warnCount };
280
+ }
281
+
282
+ async function installSkill(skill, targetDir) {
283
+ const dest = join(targetDir, skill.name);
284
+ const destExists = await exists(dest);
285
+
286
+ if (destExists && !args.upgrade) {
287
+ return { name: skill.name, status: "skipped" };
288
+ }
289
+
290
+ // Copy skill, filtering out CI-only directories
291
+ await cpFiltered(skill.path, dest);
292
+
293
+ return { name: skill.name, status: destExists ? "upgraded" : "installed" };
294
+ }
295
+
296
+ /**
297
+ * Recursively copies a skill directory, excluding CI/test artefacts
298
+ * that consumers don't need. Matches the .skill package exclusions.
299
+ */
300
+ const INSTALL_EXCLUDE_DIRS = new Set([
301
+ "tests",
302
+ "evals",
303
+ "__pycache__",
304
+ "node_modules",
305
+ ".git",
306
+ ]);
307
+ const INSTALL_EXCLUDE_FILES = new Set([".DS_Store", ".gitkeep"]);
308
+
309
+ async function cpFiltered(src, dest) {
310
+ await mkdir(dest, { recursive: true });
311
+ const entries = await readdir(src, { withFileTypes: true });
312
+
313
+ for (const entry of entries) {
314
+ const srcPath = join(src, entry.name);
315
+ const destPath = join(dest, entry.name);
316
+
317
+ if (entry.isDirectory()) {
318
+ if (INSTALL_EXCLUDE_DIRS.has(entry.name)) continue;
319
+ await cpFiltered(srcPath, destPath);
320
+ } else if (entry.isFile()) {
321
+ if (INSTALL_EXCLUDE_FILES.has(entry.name)) continue;
322
+ if (entry.name.endsWith(".pyc")) continue;
323
+ const { copyFile } = await import("node:fs/promises");
324
+ await copyFile(srcPath, destPath);
325
+ }
326
+ }
327
+ }
328
+
329
+ async function main() {
330
+ const skills = await discoverSkills();
331
+
332
+ if (skills.length === 0) {
333
+ console.error("No skills found in package. This is likely a packaging error.");
334
+ process.exit(1);
335
+ }
336
+
337
+ // --list mode
338
+ if (args.list) {
339
+ console.log("\nAvailable skills:\n");
340
+ const maxName = Math.max(...skills.map((s) => s.name.length));
341
+ for (const skill of skills) {
342
+ const desc =
343
+ skill.description.length > 80
344
+ ? skill.description.slice(0, 77) + "..."
345
+ : skill.description;
346
+ console.log(` ${skill.name.padEnd(maxName + 2)} ${desc}`);
347
+ }
348
+ console.log(`\n${skills.length} skill(s) available\n`);
349
+ process.exit(0);
350
+ }
351
+
352
+ // Filter skills if --skill specified
353
+ let toInstall = skills;
354
+ if (args.skill) {
355
+ const requested = new Set(args.skill.split(",").map((s) => s.trim()));
356
+ toInstall = skills.filter((s) => requested.has(s.name));
357
+ const found = new Set(toInstall.map((s) => s.name));
358
+ const missing = [...requested].filter((r) => !found.has(r));
359
+ if (missing.length > 0) {
360
+ console.error(`Unknown skills: ${missing.join(", ")}`);
361
+ console.error(`Use --list to see available skills`);
362
+ process.exit(1);
363
+ }
364
+ }
365
+
366
+ const targetDir = await resolveTarget();
367
+ await mkdir(targetDir, { recursive: true });
368
+
369
+ console.log(`\nInstalling ${toInstall.length} skill(s) to ${targetDir}\n`);
370
+
371
+ // Pass 1: Install all skills (file copy)
372
+ const results = [];
373
+ for (const skill of toInstall) {
374
+ results.push(await installSkill(skill, targetDir));
375
+ }
376
+
377
+ // Pass 2: Print results and check dependencies
378
+ let totalDepsInstalled = 0;
379
+ let totalDepsWarnings = 0;
380
+
381
+ for (const result of results) {
382
+ if (result.status === "skipped") {
383
+ console.log(` ⏭ ${result.name} (exists, use --upgrade to overwrite)`);
384
+ } else {
385
+ console.log(` ✅ ${result.name} (${result.status})`);
386
+ const depResult = await checkSkillDependencies(result.name, targetDir);
387
+ totalDepsInstalled += depResult.installed;
388
+ totalDepsWarnings += depResult.warnings;
389
+ }
390
+ }
391
+
392
+ const installed = results.filter((r) => r.status === "installed").length;
393
+ const upgraded = results.filter((r) => r.status === "upgraded").length;
394
+ const skipped = results.filter((r) => r.status === "skipped").length;
395
+
396
+ console.log(
397
+ `\nDone: ${installed} installed, ${upgraded} upgraded, ${skipped} skipped`
398
+ );
399
+ if (totalDepsInstalled > 0 || totalDepsWarnings > 0) {
400
+ const parts = [];
401
+ if (totalDepsInstalled > 0) {
402
+ parts.push(
403
+ `${totalDepsInstalled} ${totalDepsInstalled === 1 ? "dependency" : "dependencies"} installed`
404
+ );
405
+ }
406
+ if (totalDepsWarnings > 0) {
407
+ parts.push(
408
+ `${totalDepsWarnings} ${totalDepsWarnings === 1 ? "warning" : "warnings"}`
409
+ );
410
+ }
411
+ console.log(` ${parts.join(", ")}`);
412
+ }
413
+ console.log();
414
+ }
415
+
416
+ main().catch((err) => {
417
+ console.error("Fatal:", err.message);
418
+ process.exit(1);
419
+ });