@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +42 -0
- package/skills/brace/SKILL.md +51 -0
- package/skills/brace/assets/gitignore-template +28 -0
- package/skills/brace/assets/global-preferences-template.md +53 -0
- package/skills/brace/instructions.md +229 -0
- package/skills/brace/references/brace-workflow.md +109 -0
- package/skills/brace/references/claude-md-template.md +91 -0
- package/skills/brace/references/gotcha-principles.md +113 -0
- package/skills/brace/references/phase-prompts.md +228 -0
- package/skills/brace/references/report-template.md +38 -0
- package/skills/brace/references/scaffold-manifest.md +68 -0
- package/skills/brace/tests/evals.json +29 -0
- package/skills/build/SKILL.md +48 -0
- package/skills/build/instructions.md +293 -0
- package/skills/build/references/architecture-notes.md +34 -0
- package/skills/build/references/project-detection.md +45 -0
- package/skills/build/references/report-contracts.md +21 -0
- package/skills/build/references/stage-prompts.md +405 -0
- package/skills/build/tests/evals.json +28 -0
- package/skills/distil/SKILL.md +38 -0
- package/skills/distil/assets/DesignNav.tsx +54 -0
- package/skills/distil/instructions.md +255 -0
- package/skills/distil/references/design-guide.md +118 -0
- package/skills/distil/references/iteration-mode.md +186 -0
- package/skills/distil/references/project-setup.md +92 -0
- package/skills/distil/tests/evals.json +28 -0
- package/skills/manifest.json +76 -0
- package/skills/prime/SKILL.md +39 -0
- package/skills/prime/instructions.md +73 -0
- package/skills/prime/references/domains.md +38 -0
- package/skills/prime/tests/evals.json +28 -0
- package/skills/rig/SKILL.md +38 -0
- package/skills/rig/assets/azure-pipelines.yml +91 -0
- package/skills/rig/assets/ci.yml +104 -0
- package/skills/rig/assets/gitmessage +38 -0
- package/skills/rig/assets/lefthook.yml +29 -0
- package/skills/rig/assets/pull_request_template.md +24 -0
- package/skills/rig/instructions.md +162 -0
- package/skills/rig/references/configuration-steps.md +124 -0
- package/skills/rig/references/phase-prompts.md +180 -0
- package/skills/rig/references/report-template.md +28 -0
- package/skills/rig/tests/evals.json +29 -0
- package/skills/ship/SKILL.md +55 -0
- package/skills/ship/instructions.md +192 -0
- package/skills/ship/references/stage-prompts.md +322 -0
- package/skills/ship/tests/evals.json +30 -0
- package/skills/sync/SKILL.md +54 -0
- package/skills/sync/instructions.md +178 -0
- package/skills/sync/tests/evals.json +29 -0
- 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
|
+
});
|