skillrepo 1.0.0 → 1.2.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 +216 -0
- package/bin/skillrepo.mjs +1 -1
- package/package.json +1 -1
- package/src/commands/init.mjs +3 -3
- package/src/lib/mergers/claude-mcp.mjs +1 -1
- package/src/lib/mergers/cursor-mcp.mjs +1 -1
- package/src/lib/mergers/env-local.mjs +2 -2
- package/src/lib/mergers/hooks-json.mjs +63 -28
- package/src/lib/mergers/windsurf-mcp.mjs +1 -1
- package/src/lib/paths.mjs +2 -0
- package/src/lib/write-configs.mjs +9 -3
- package/src/test/env-local.test.mjs +5 -5
- package/src/test/mergers/claude-mcp.test.mjs +1 -1
- package/src/test/mergers/hooks-json.test.mjs +151 -0
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# skillrepo
|
|
2
|
+
|
|
3
|
+
Set up SkillRepo in any IDE -- one command.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx skillrepo init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Auto-detects your IDEs, validates your access key, and writes the correct MCP
|
|
10
|
+
configuration for each one. Safe to run multiple times.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
1. **Create an access key** at [skillrepo.dev/app/settings](https://skillrepo.dev/app/settings) (Settings > Access Keys).
|
|
15
|
+
2. **Run the CLI** in your project directory:
|
|
16
|
+
```
|
|
17
|
+
npx skillrepo init
|
|
18
|
+
```
|
|
19
|
+
3. **Done.** Your IDE can now discover and activate skills from your SkillRepo library.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npx skillrepo init [options]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Options
|
|
28
|
+
|
|
29
|
+
| Flag | Short | Description |
|
|
30
|
+
|------|-------|-------------|
|
|
31
|
+
| `--key <key>` | `-k` | Access key. If omitted, the CLI reads `SKILLREPO_ACCESS_KEY` from the environment, then prompts interactively. |
|
|
32
|
+
| `--url <url>` | `-u` | SkillRepo server URL. Defaults to `https://skillrepo.dev`. Use this for self-hosted instances. |
|
|
33
|
+
| `--yes` | `-y` | Non-interactive mode. Skips confirmation prompts. Useful for CI or scripted setups. |
|
|
34
|
+
|
|
35
|
+
### Examples
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
# Interactive setup (prompts for key and confirms detected IDEs)
|
|
39
|
+
npx skillrepo init
|
|
40
|
+
|
|
41
|
+
# Pass the key directly
|
|
42
|
+
npx skillrepo init --key sk_live_abc123
|
|
43
|
+
|
|
44
|
+
# Self-hosted instance, non-interactive
|
|
45
|
+
npx skillrepo init --url https://skillrepo.internal.company.com --yes
|
|
46
|
+
|
|
47
|
+
# Key from environment variable
|
|
48
|
+
export SKILLREPO_ACCESS_KEY=sk_live_abc123
|
|
49
|
+
npx skillrepo init --yes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What It Does
|
|
53
|
+
|
|
54
|
+
The CLI performs four steps:
|
|
55
|
+
|
|
56
|
+
1. **Detects installed IDEs** by checking for IDE-specific directories in the
|
|
57
|
+
current project (`.claude/`, `.cursor/`, `.vscode/`) and global paths
|
|
58
|
+
(`~/.codeium/windsurf/`). If no IDE is detected, it defaults to Claude Code
|
|
59
|
+
and Cursor.
|
|
60
|
+
|
|
61
|
+
2. **Validates the access key** against the SkillRepo API and fetches the
|
|
62
|
+
current skill mapping data for your library.
|
|
63
|
+
|
|
64
|
+
3. **Writes MCP configuration files** for each detected IDE, using the correct
|
|
65
|
+
format and environment variable syntax for that IDE.
|
|
66
|
+
|
|
67
|
+
4. **Writes skill mapping files** that let agents match user requests to the
|
|
68
|
+
right skill without a network round-trip. For Claude Code, it also installs a
|
|
69
|
+
SessionStart hook that auto-refreshes the mapping on each session start.
|
|
70
|
+
|
|
71
|
+
### Files Created or Modified
|
|
72
|
+
|
|
73
|
+
| File | IDE | Purpose |
|
|
74
|
+
|------|-----|---------|
|
|
75
|
+
| `.mcp.json` | Claude Code | MCP server connection config |
|
|
76
|
+
| `.claude/skillrepo.md` | Claude Code | Skill-to-tool mapping |
|
|
77
|
+
| `.claude/hooks/hooks.json` | Claude Code | SessionStart hook registration |
|
|
78
|
+
| `.claude/hooks/skillrepo-sync.mjs` | Claude Code | Auto-refresh script for skill mappings |
|
|
79
|
+
| `.cursor/mcp.json` | Cursor | MCP server connection config |
|
|
80
|
+
| `.cursor/rules/skillrepo.mdc` | Cursor | Skill-to-tool mapping |
|
|
81
|
+
| `~/.codeium/windsurf/mcp_config.json` | Windsurf | MCP server connection config (global) |
|
|
82
|
+
| `.vscode/mcp.json` | VS Code + Copilot | MCP server connection config |
|
|
83
|
+
| `.env.local` | All | Stores `SKILLREPO_ACCESS_KEY` (gitignored) |
|
|
84
|
+
|
|
85
|
+
All writes are merge-safe. The CLI reads existing files, adds or updates only
|
|
86
|
+
the `skillrepo` entry, and leaves all other entries untouched. Running `init`
|
|
87
|
+
again with a new key updates the key in `.env.local` without affecting anything
|
|
88
|
+
else.
|
|
89
|
+
|
|
90
|
+
## IDE-Specific Details
|
|
91
|
+
|
|
92
|
+
### Claude Code
|
|
93
|
+
|
|
94
|
+
- Config file: `.mcp.json` with `mcpServers.skillrepo` entry
|
|
95
|
+
- Uses `${SKILLREPO_ACCESS_KEY}` syntax for environment variable expansion
|
|
96
|
+
- Includes `"type": "http"` in the server entry
|
|
97
|
+
- Installs a SessionStart hook that refreshes the skill mapping file
|
|
98
|
+
(`.claude/skillrepo.md`) automatically on every session start
|
|
99
|
+
- Skill mappings are always current without manual intervention
|
|
100
|
+
|
|
101
|
+
### Cursor
|
|
102
|
+
|
|
103
|
+
- Config file: `.cursor/mcp.json` with `mcpServers.skillrepo` entry
|
|
104
|
+
- Uses `${env:SKILLREPO_ACCESS_KEY}` syntax for environment variable expansion
|
|
105
|
+
- Skill mapping file (`.cursor/rules/skillrepo.mdc`) is static; re-run
|
|
106
|
+
`npx skillrepo init` or ask your agent to run `update skills from skillrepo`
|
|
107
|
+
to refresh it after adding or removing skills
|
|
108
|
+
|
|
109
|
+
### Windsurf
|
|
110
|
+
|
|
111
|
+
- Config file: `~/.codeium/windsurf/mcp_config.json` (global, not per-project)
|
|
112
|
+
- Uses `serverUrl` instead of `url` in the server entry
|
|
113
|
+
- Uses `${env:SKILLREPO_ACCESS_KEY}` syntax for environment variable expansion
|
|
114
|
+
|
|
115
|
+
### VS Code + Copilot
|
|
116
|
+
|
|
117
|
+
- Config file: `.vscode/mcp.json` with `servers.skillrepo` entry (note:
|
|
118
|
+
`servers`, not `mcpServers`)
|
|
119
|
+
- VS Code does not support environment variable interpolation in MCP config;
|
|
120
|
+
instead, the CLI creates an `inputs` entry that prompts for the access key on
|
|
121
|
+
first use
|
|
122
|
+
- Uses `${input:skillrepo-api-key}` syntax to reference the input prompt
|
|
123
|
+
|
|
124
|
+
## Team Setup
|
|
125
|
+
|
|
126
|
+
For teams sharing a repository, an admin runs the setup once and commits the
|
|
127
|
+
generated config files. Each developer then clones and adds their own key.
|
|
128
|
+
|
|
129
|
+
### Admin (one-time)
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
cd your-team-repo
|
|
133
|
+
npx skillrepo init
|
|
134
|
+
git add .mcp.json .claude/ .cursor/ .vscode/
|
|
135
|
+
git commit -m "chore: add SkillRepo integration"
|
|
136
|
+
git push
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `.env.local` file is not committed -- it contains the access key and should
|
|
140
|
+
remain gitignored.
|
|
141
|
+
|
|
142
|
+
### Each Developer
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
git pull
|
|
146
|
+
npx skillrepo init
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The CLI detects the existing config files, merges in any updates, and writes the
|
|
150
|
+
developer's own access key to `.env.local`. Developers can also skip the CLI
|
|
151
|
+
and set the environment variable manually:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
echo 'SKILLREPO_ACCESS_KEY=sk_live_their_key' >> .env.local
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Self-Hosted Usage
|
|
158
|
+
|
|
159
|
+
If you run a self-hosted SkillRepo instance, pass the `--url` flag:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
npx skillrepo init --url https://skillrepo.internal.company.com
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The CLI writes the custom URL into all MCP config files. The access key is
|
|
166
|
+
validated against your self-hosted instance instead of skillrepo.dev.
|
|
167
|
+
|
|
168
|
+
You can also set the URL via the `SKILLREPO_URL` environment variable to avoid
|
|
169
|
+
passing `--url` every time.
|
|
170
|
+
|
|
171
|
+
## Troubleshooting
|
|
172
|
+
|
|
173
|
+
### "Invalid key format. Keys start with sk_live_"
|
|
174
|
+
|
|
175
|
+
Access keys always begin with `sk_live_`. Verify you copied the full key from
|
|
176
|
+
Settings > Access Keys. If the key was created with a different prefix, it may
|
|
177
|
+
be a test key that is not valid for production use.
|
|
178
|
+
|
|
179
|
+
### "Cannot reach https://skillrepo.dev"
|
|
180
|
+
|
|
181
|
+
Check your network connection. If you are behind a corporate proxy or firewall,
|
|
182
|
+
ensure HTTPS traffic to `skillrepo.dev` (or your self-hosted URL) is allowed.
|
|
183
|
+
For self-hosted instances, verify the `--url` flag points to the correct address.
|
|
184
|
+
|
|
185
|
+
### "Cannot parse .mcp.json -- invalid JSON"
|
|
186
|
+
|
|
187
|
+
The CLI cannot merge into a malformed JSON file. Open the file, fix the syntax
|
|
188
|
+
error (or delete it to start fresh), then run `npx skillrepo init` again.
|
|
189
|
+
|
|
190
|
+
### "No IDEs detected"
|
|
191
|
+
|
|
192
|
+
The CLI looks for IDE-specific directories in the current working directory. If
|
|
193
|
+
you are not in a project root, `cd` into your project first. If no IDE
|
|
194
|
+
directories exist yet, the CLI defaults to configuring Claude Code and Cursor.
|
|
195
|
+
|
|
196
|
+
### VS Code prompts for the access key every time
|
|
197
|
+
|
|
198
|
+
This is expected. VS Code does not support environment variable expansion in
|
|
199
|
+
MCP config files, so it uses an input prompt instead. The key is cached for the
|
|
200
|
+
duration of your VS Code session. Enter the same key from your `.env.local`
|
|
201
|
+
file.
|
|
202
|
+
|
|
203
|
+
### Skill mappings are stale
|
|
204
|
+
|
|
205
|
+
For Claude Code, the SessionStart hook refreshes mappings automatically. For
|
|
206
|
+
Cursor and VS Code + Copilot, re-run `npx skillrepo init` or ask your agent to
|
|
207
|
+
run `update skills from skillrepo` to regenerate the mapping file.
|
|
208
|
+
|
|
209
|
+
## Requirements
|
|
210
|
+
|
|
211
|
+
- Node.js 18 or later
|
|
212
|
+
- Zero runtime dependencies (uses built-in `fetch` and `fs`)
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
AGPL-3.0
|
package/bin/skillrepo.mjs
CHANGED
|
@@ -25,7 +25,7 @@ if (!command || command === "init") {
|
|
|
25
25
|
npx skillrepo init [options]
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
|
-
--key, -k <key> Access key (or set
|
|
28
|
+
--key, -k <key> Access key (or set SKILLREPO_ACCESS_KEY env var)
|
|
29
29
|
--url, -u <url> SkillRepo URL (default: https://skillrepo.dev)
|
|
30
30
|
--yes, -y Non-interactive mode
|
|
31
31
|
|
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -32,7 +32,7 @@ const DEFAULT_URL = "https://skillrepo.dev";
|
|
|
32
32
|
*/
|
|
33
33
|
function parseFlags(argv) {
|
|
34
34
|
const flags = {
|
|
35
|
-
key: process.env.
|
|
35
|
+
key: process.env.SKILLREPO_ACCESS_KEY || null,
|
|
36
36
|
url: process.env.SKILLREPO_URL || DEFAULT_URL,
|
|
37
37
|
yes: false,
|
|
38
38
|
};
|
|
@@ -167,8 +167,8 @@ export async function runInit(argv) {
|
|
|
167
167
|
printBlank();
|
|
168
168
|
console.log(" Next steps:");
|
|
169
169
|
console.log(" • Commit the generated config files to git");
|
|
170
|
-
console.log(" • Each team member runs: npx skillrepo
|
|
171
|
-
console.log(" •
|
|
170
|
+
console.log(" • Each team member runs: npx skillrepo init");
|
|
171
|
+
console.log(" • SKILLREPO_ACCESS_KEY is in .env.local (gitignored)");
|
|
172
172
|
|
|
173
173
|
if (detectedKeys.includes("vscode")) {
|
|
174
174
|
printBlank();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Merge
|
|
2
|
+
* Merge SKILLREPO_ACCESS_KEY into .env.local
|
|
3
3
|
* Append-only — never overwrite existing keys unless the value differs.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
|
|
7
7
|
import { envLocal } from "../paths.mjs";
|
|
8
8
|
|
|
9
|
-
const KEY_NAME = "
|
|
9
|
+
const KEY_NAME = "SKILLREPO_ACCESS_KEY";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {string} apiKey
|
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Merger for Claude Code .claude/hooks/hooks.json
|
|
3
|
-
*
|
|
3
|
+
* Writes hook scripts and merges hook entries into hooks.json.
|
|
4
4
|
*
|
|
5
|
-
* Merge strategy: add
|
|
6
|
-
* Idempotent — if
|
|
5
|
+
* Merge strategy: add SessionStart and UserPromptSubmit entries
|
|
6
|
+
* without destroying existing hooks. Idempotent — skips if already installed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
|
|
10
|
-
import { claudeHooksJson, claudeSyncHook } from "../paths.mjs";
|
|
10
|
+
import { claudeHooksJson, claudeSyncHook, claudePromptHook } from "../paths.mjs";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const SYNC_COMMAND = "node .claude/hooks/skillrepo-sync.mjs";
|
|
13
|
+
const PROMPT_COMMAND = "node .claude/hooks/skillrepo-prompt-match.mjs";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Check if a command is already installed in a hook group array.
|
|
17
|
+
* @param {Array} groups
|
|
18
|
+
* @param {string} command
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function hasCommand(groups, command) {
|
|
22
|
+
if (!Array.isArray(groups)) return false;
|
|
23
|
+
return groups.some(
|
|
24
|
+
(group) =>
|
|
25
|
+
Array.isArray(group.hooks) &&
|
|
26
|
+
group.hooks.some((h) => h.command === command)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge SkillRepo hooks into hooks.json and write hook scripts.
|
|
16
32
|
* @param {string} hooksJsonContent - The canonical hooks.json content from the API
|
|
17
|
-
* @param {string} syncHookContent - The sync hook script content
|
|
33
|
+
* @param {string} syncHookContent - The sync hook script content
|
|
34
|
+
* @param {string} promptHookContent - The prompt-match hook script content
|
|
18
35
|
* @returns {{ results: { path: string; action: string }[] }}
|
|
19
36
|
*/
|
|
20
|
-
export function mergeHooksConfig(hooksJsonContent, syncHookContent) {
|
|
37
|
+
export function mergeHooksConfig(hooksJsonContent, syncHookContent, promptHookContent) {
|
|
21
38
|
const results = [];
|
|
22
39
|
|
|
23
|
-
// Always write
|
|
24
|
-
const
|
|
40
|
+
// Always write hook scripts (latest version from server)
|
|
41
|
+
const syncExisted = readFileSafe(claudeSyncHook()) !== null;
|
|
25
42
|
writeFileSafe(claudeSyncHook(), syncHookContent);
|
|
26
|
-
results.push({
|
|
43
|
+
results.push({
|
|
44
|
+
path: ".claude/hooks/skillrepo-sync.mjs",
|
|
45
|
+
action: syncExisted ? "updated" : "created",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const promptExisted = readFileSafe(claudePromptHook()) !== null;
|
|
49
|
+
writeFileSafe(claudePromptHook(), promptHookContent);
|
|
50
|
+
results.push({
|
|
51
|
+
path: ".claude/hooks/skillrepo-prompt-match.mjs",
|
|
52
|
+
action: promptExisted ? "updated" : "created",
|
|
53
|
+
});
|
|
27
54
|
|
|
28
55
|
// Merge hooks.json
|
|
29
56
|
const filePath = claudeHooksJson();
|
|
@@ -39,31 +66,39 @@ export function mergeHooksConfig(hooksJsonContent, syncHookContent) {
|
|
|
39
66
|
try {
|
|
40
67
|
config = JSON.parse(existing);
|
|
41
68
|
} catch {
|
|
42
|
-
throw new Error(
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Cannot parse .claude/hooks/hooks.json — invalid JSON. Fix or delete it, then run setup again."
|
|
71
|
+
);
|
|
43
72
|
}
|
|
44
73
|
|
|
45
|
-
// Navigate to hooks.SessionStart
|
|
46
74
|
if (!config.hooks) config.hooks = {};
|
|
75
|
+
let changed = false;
|
|
76
|
+
|
|
77
|
+
// Merge SessionStart
|
|
47
78
|
if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
|
|
79
|
+
if (!hasCommand(config.hooks.SessionStart, SYNC_COMMAND)) {
|
|
80
|
+
config.hooks.SessionStart.push({
|
|
81
|
+
matcher: "startup|resume",
|
|
82
|
+
hooks: [{ type: "command", command: SYNC_COMMAND }],
|
|
83
|
+
});
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
48
86
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
// Merge UserPromptSubmit
|
|
88
|
+
if (!Array.isArray(config.hooks.UserPromptSubmit)) config.hooks.UserPromptSubmit = [];
|
|
89
|
+
if (!hasCommand(config.hooks.UserPromptSubmit, PROMPT_COMMAND)) {
|
|
90
|
+
config.hooks.UserPromptSubmit.push({
|
|
91
|
+
hooks: [{ type: "command", command: PROMPT_COMMAND }],
|
|
92
|
+
});
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
54
95
|
|
|
55
|
-
if (
|
|
96
|
+
if (changed) {
|
|
97
|
+
writeFileSafe(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
98
|
+
results.push({ path: ".claude/hooks/hooks.json", action: "merged" });
|
|
99
|
+
} else {
|
|
56
100
|
results.push({ path: ".claude/hooks/hooks.json", action: "skipped" });
|
|
57
|
-
return { results };
|
|
58
101
|
}
|
|
59
102
|
|
|
60
|
-
// Append new entry
|
|
61
|
-
config.hooks.SessionStart.push({
|
|
62
|
-
matcher: "startup|resume",
|
|
63
|
-
hooks: [{ type: "command", command: TARGET_COMMAND }],
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
writeFileSafe(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
67
|
-
results.push({ path: ".claude/hooks/hooks.json", action: "merged" });
|
|
68
103
|
return { results };
|
|
69
104
|
}
|
package/src/lib/paths.mjs
CHANGED
|
@@ -14,7 +14,9 @@ export const claudeDir = () => join(cwd(), ".claude");
|
|
|
14
14
|
export const claudeHooksDir = () => join(cwd(), ".claude", "hooks");
|
|
15
15
|
export const claudeHooksJson = () => join(cwd(), ".claude", "hooks", "hooks.json");
|
|
16
16
|
export const claudeSyncHook = () => join(cwd(), ".claude", "hooks", "skillrepo-sync.mjs");
|
|
17
|
+
export const claudePromptHook = () => join(cwd(), ".claude", "hooks", "skillrepo-prompt-match.mjs");
|
|
17
18
|
export const claudeSkillrepoMd = () => join(cwd(), ".claude", "skillrepo.md");
|
|
19
|
+
export const claudeSkillrepoIndex = () => join(cwd(), ".claude", "skillrepo-index.json");
|
|
18
20
|
|
|
19
21
|
// Cursor
|
|
20
22
|
export const cursorDir = () => join(cwd(), ".cursor");
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
|
|
7
|
-
import { claudeSkillrepoMd, cursorSkillrepoMdc } from "./paths.mjs";
|
|
7
|
+
import { claudeSkillrepoMd, claudeSkillrepoIndex, cursorSkillrepoMdc } from "./paths.mjs";
|
|
8
8
|
import { mergeClaudeMcpConfig } from "./mergers/claude-mcp.mjs";
|
|
9
9
|
import { mergeCursorMcpConfig } from "./mergers/cursor-mcp.mjs";
|
|
10
10
|
import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
|
|
@@ -33,10 +33,16 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
|
|
|
33
33
|
writeFileSafe(claudeSkillrepoMd(), payload.claudeCode.skillrepoMd.content);
|
|
34
34
|
results.push({ path: ".claude/skillrepo.md", action: claudeMdExisted ? "updated" : "created" });
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// JSON skill index for prompt matching
|
|
37
|
+
const indexExisted = readFileSafe(claudeSkillrepoIndex()) !== null;
|
|
38
|
+
writeFileSafe(claudeSkillrepoIndex(), payload.claudeCode.skillIndex.content);
|
|
39
|
+
results.push({ path: ".claude/skillrepo-index.json", action: indexExisted ? "updated" : "created" });
|
|
40
|
+
|
|
41
|
+
// Hooks (SessionStart sync + UserPromptSubmit prompt-match)
|
|
37
42
|
const hookResults = mergeHooksConfig(
|
|
38
43
|
payload.claudeCode.hooksJson.content,
|
|
39
|
-
payload.claudeCode.syncHook.content
|
|
44
|
+
payload.claudeCode.syncHook.content,
|
|
45
|
+
payload.claudeCode.promptHook.content
|
|
40
46
|
);
|
|
41
47
|
results.push(...hookResults.results);
|
|
42
48
|
}
|
|
@@ -25,12 +25,12 @@ describe(".env.local merger", () => {
|
|
|
25
25
|
|
|
26
26
|
assert.equal(result.action, "created");
|
|
27
27
|
const content = readFileSync(join(tempDir, ".env.local"), "utf-8");
|
|
28
|
-
assert.equal(content, "
|
|
28
|
+
assert.equal(content, "SKILLREPO_ACCESS_KEY=sk_live_test123\n");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("skips when key already exists with same value", async () => {
|
|
32
32
|
const { mergeEnvLocal } = await import("../lib/mergers/env-local.mjs");
|
|
33
|
-
writeFileSync(join(tempDir, ".env.local"), "
|
|
33
|
+
writeFileSync(join(tempDir, ".env.local"), "SKILLREPO_ACCESS_KEY=sk_live_test123\n");
|
|
34
34
|
|
|
35
35
|
const result = mergeEnvLocal("sk_live_test123");
|
|
36
36
|
assert.equal(result.action, "skipped");
|
|
@@ -38,13 +38,13 @@ describe(".env.local merger", () => {
|
|
|
38
38
|
|
|
39
39
|
it("updates when key exists with different value", async () => {
|
|
40
40
|
const { mergeEnvLocal } = await import("../lib/mergers/env-local.mjs");
|
|
41
|
-
writeFileSync(join(tempDir, ".env.local"), "
|
|
41
|
+
writeFileSync(join(tempDir, ".env.local"), "SKILLREPO_ACCESS_KEY=sk_live_old\n");
|
|
42
42
|
|
|
43
43
|
const result = mergeEnvLocal("sk_live_new");
|
|
44
44
|
assert.equal(result.action, "updated");
|
|
45
45
|
|
|
46
46
|
const content = readFileSync(join(tempDir, ".env.local"), "utf-8");
|
|
47
|
-
assert.ok(content.includes("
|
|
47
|
+
assert.ok(content.includes("SKILLREPO_ACCESS_KEY=sk_live_new"));
|
|
48
48
|
assert.ok(!content.includes("sk_live_old"));
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -57,7 +57,7 @@ describe(".env.local merger", () => {
|
|
|
57
57
|
|
|
58
58
|
const content = readFileSync(join(tempDir, ".env.local"), "utf-8");
|
|
59
59
|
assert.ok(content.includes("OTHER_VAR=value"));
|
|
60
|
-
assert.ok(content.includes("
|
|
60
|
+
assert.ok(content.includes("SKILLREPO_ACCESS_KEY=sk_live_test123"));
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
it("preserves CRLF line endings", async () => {
|
|
@@ -30,7 +30,7 @@ describe("Claude Code MCP config merger", () => {
|
|
|
30
30
|
const content = JSON.parse(readFileSync(join(tempDir, ".mcp.json"), "utf-8"));
|
|
31
31
|
assert.equal(content.mcpServers.skillrepo.type, "http");
|
|
32
32
|
assert.equal(content.mcpServers.skillrepo.url, "https://skillrepo.dev/api/mcp");
|
|
33
|
-
assert.equal(content.mcpServers.skillrepo.headers.Authorization, "Bearer ${
|
|
33
|
+
assert.equal(content.mcpServers.skillrepo.headers.Authorization, "Bearer ${SKILLREPO_ACCESS_KEY}");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("merges into existing config without destroying other servers", async () => {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
let originalCwd;
|
|
8
|
+
let tempDir;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalCwd = process.cwd;
|
|
12
|
+
tempDir = mkdtempSync(join(tmpdir(), "cli-hooks-test-"));
|
|
13
|
+
process.cwd = () => tempDir;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
process.cwd = originalCwd;
|
|
18
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const SYNC_CONTENT = "// sync hook";
|
|
22
|
+
const PROMPT_CONTENT = "// prompt hook";
|
|
23
|
+
const HOOKS_JSON = JSON.stringify({
|
|
24
|
+
hooks: {
|
|
25
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
|
|
26
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
27
|
+
},
|
|
28
|
+
}, null, 2) + "\n";
|
|
29
|
+
|
|
30
|
+
describe("Hooks JSON merger", () => {
|
|
31
|
+
it("creates all files when nothing exists", async () => {
|
|
32
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
33
|
+
const { results } = mergeHooksConfig(HOOKS_JSON, SYNC_CONTENT, PROMPT_CONTENT);
|
|
34
|
+
|
|
35
|
+
// Check sync hook written
|
|
36
|
+
const syncResult = results.find((r) => r.path.includes("sync"));
|
|
37
|
+
assert.equal(syncResult.action, "created");
|
|
38
|
+
assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "utf-8"), SYNC_CONTENT);
|
|
39
|
+
|
|
40
|
+
// Check prompt hook written
|
|
41
|
+
const promptResult = results.find((r) => r.path.includes("prompt"));
|
|
42
|
+
assert.equal(promptResult.action, "created");
|
|
43
|
+
assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "utf-8"), PROMPT_CONTENT);
|
|
44
|
+
|
|
45
|
+
// Check hooks.json created
|
|
46
|
+
const jsonResult = results.find((r) => r.path.includes("hooks.json"));
|
|
47
|
+
assert.equal(jsonResult.action, "created");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("merges into existing hooks.json without destroying other hooks", async () => {
|
|
51
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
52
|
+
|
|
53
|
+
// Pre-existing hooks.json with a different hook
|
|
54
|
+
const existing = {
|
|
55
|
+
hooks: {
|
|
56
|
+
PreToolUse: [{ hooks: [{ type: "command", command: "node other-hook.mjs" }] }],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
60
|
+
writeFileSync(join(tempDir, ".claude/hooks/hooks.json"), JSON.stringify(existing, null, 2));
|
|
61
|
+
|
|
62
|
+
const { results } = mergeHooksConfig(HOOKS_JSON, SYNC_CONTENT, PROMPT_CONTENT);
|
|
63
|
+
|
|
64
|
+
const jsonResult = results.find((r) => r.path.includes("hooks.json"));
|
|
65
|
+
assert.equal(jsonResult.action, "merged");
|
|
66
|
+
|
|
67
|
+
const config = JSON.parse(readFileSync(join(tempDir, ".claude/hooks/hooks.json"), "utf-8"));
|
|
68
|
+
// Original hook preserved
|
|
69
|
+
assert.equal(config.hooks.PreToolUse.length, 1);
|
|
70
|
+
// New hooks added
|
|
71
|
+
assert.equal(config.hooks.SessionStart.length, 1);
|
|
72
|
+
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("skips merge when hooks are already installed", async () => {
|
|
76
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
77
|
+
|
|
78
|
+
// Pre-existing with both hooks already
|
|
79
|
+
const existing = {
|
|
80
|
+
hooks: {
|
|
81
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
|
|
82
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
86
|
+
writeFileSync(join(tempDir, ".claude/hooks/hooks.json"), JSON.stringify(existing, null, 2));
|
|
87
|
+
|
|
88
|
+
const { results } = mergeHooksConfig(HOOKS_JSON, SYNC_CONTENT, PROMPT_CONTENT);
|
|
89
|
+
|
|
90
|
+
const jsonResult = results.find((r) => r.path.includes("hooks.json"));
|
|
91
|
+
assert.equal(jsonResult.action, "skipped");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("adds missing UserPromptSubmit when only SessionStart exists", async () => {
|
|
95
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
96
|
+
|
|
97
|
+
// Only SessionStart installed (pre-existing v1.1 setup)
|
|
98
|
+
const existing = {
|
|
99
|
+
hooks: {
|
|
100
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
104
|
+
writeFileSync(join(tempDir, ".claude/hooks/hooks.json"), JSON.stringify(existing, null, 2));
|
|
105
|
+
|
|
106
|
+
const { results } = mergeHooksConfig(HOOKS_JSON, SYNC_CONTENT, PROMPT_CONTENT);
|
|
107
|
+
|
|
108
|
+
const jsonResult = results.find((r) => r.path.includes("hooks.json"));
|
|
109
|
+
assert.equal(jsonResult.action, "merged");
|
|
110
|
+
|
|
111
|
+
const config = JSON.parse(readFileSync(join(tempDir, ".claude/hooks/hooks.json"), "utf-8"));
|
|
112
|
+
// SessionStart unchanged
|
|
113
|
+
assert.equal(config.hooks.SessionStart.length, 1);
|
|
114
|
+
// UserPromptSubmit added
|
|
115
|
+
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
116
|
+
assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, "node .claude/hooks/skillrepo-prompt-match.mjs");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("always updates hook scripts even when hooks.json is skipped", async () => {
|
|
120
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
121
|
+
|
|
122
|
+
// Install once
|
|
123
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
124
|
+
writeFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "// old sync");
|
|
125
|
+
writeFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "// old prompt");
|
|
126
|
+
writeFileSync(join(tempDir, ".claude/hooks/hooks.json"), HOOKS_JSON);
|
|
127
|
+
|
|
128
|
+
const { results } = mergeHooksConfig(HOOKS_JSON, "// new sync", "// new prompt");
|
|
129
|
+
|
|
130
|
+
// Scripts updated
|
|
131
|
+
const syncResult = results.find((r) => r.path.includes("sync.mjs"));
|
|
132
|
+
assert.equal(syncResult.action, "updated");
|
|
133
|
+
assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "utf-8"), "// new sync");
|
|
134
|
+
|
|
135
|
+
const promptResult = results.find((r) => r.path.includes("prompt-match"));
|
|
136
|
+
assert.equal(promptResult.action, "updated");
|
|
137
|
+
assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "utf-8"), "// new prompt");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("throws on invalid JSON in hooks.json", async () => {
|
|
141
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
142
|
+
|
|
143
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
144
|
+
writeFileSync(join(tempDir, ".claude/hooks/hooks.json"), "not json{");
|
|
145
|
+
|
|
146
|
+
assert.throws(
|
|
147
|
+
() => mergeHooksConfig(HOOKS_JSON, SYNC_CONTENT, PROMPT_CONTENT),
|
|
148
|
+
/Cannot parse/
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|