skillrepo 1.6.0 → 1.6.1
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/package.json
CHANGED
package/src/lib/fs-utils.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Creates directories as needed, handles errors cleanly.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from "node:fs";
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -35,6 +35,15 @@ export function writeFileSafe(filePath, content) {
|
|
|
35
35
|
writeFileSync(filePath, content, "utf-8");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Write a file and mark it executable (0o755).
|
|
40
|
+
* Used for hook scripts that have shebangs and are invoked directly.
|
|
41
|
+
*/
|
|
42
|
+
export function writeExecutable(filePath, content) {
|
|
43
|
+
writeFileSafe(filePath, content);
|
|
44
|
+
chmodSync(filePath, 0o755);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* Check if a path exists (file or directory).
|
|
40
49
|
*/
|
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { unlinkSync } from "node:fs";
|
|
11
|
-
import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
|
|
11
|
+
import { readFileSafe, writeFileSafe, writeExecutable } from "../fs-utils.mjs";
|
|
12
12
|
import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudePreToolHook, claudeHooksDir } from "../paths.mjs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
-
const DEFAULT_SYNC_COMMAND = "
|
|
16
|
-
const DEFAULT_PROMPT_COMMAND = "
|
|
17
|
-
const DEFAULT_PRETOOL_COMMAND = "
|
|
15
|
+
const DEFAULT_SYNC_COMMAND = ".claude/hooks/skillrepo-sync.mjs";
|
|
16
|
+
const DEFAULT_PROMPT_COMMAND = ".claude/hooks/skillrepo-prompt-match.mjs";
|
|
17
|
+
const DEFAULT_PRETOOL_COMMAND = ".claude/hooks/skillrepo-pretool-match.mjs";
|
|
18
|
+
|
|
19
|
+
// Legacy commands used before v1.6.1 — recognized during migration so
|
|
20
|
+
// re-running init replaces them rather than duplicating.
|
|
21
|
+
const LEGACY_COMMANDS = new Set([
|
|
22
|
+
"node .claude/hooks/skillrepo-sync.mjs",
|
|
23
|
+
"node .claude/hooks/skillrepo-prompt-match.mjs",
|
|
24
|
+
"node .claude/hooks/skillrepo-pretool-match.mjs",
|
|
25
|
+
]);
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
28
|
* Extract a hook command from a hooks config group, falling back to a default.
|
|
@@ -46,6 +54,26 @@ function hasCommand(groups, command) {
|
|
|
46
54
|
);
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Replace legacy `node .claude/hooks/...` commands with the new direct-execution
|
|
59
|
+
* format. Returns true if any replacement was made.
|
|
60
|
+
*/
|
|
61
|
+
function replaceLegacyCommands(groups) {
|
|
62
|
+
if (!Array.isArray(groups)) return false;
|
|
63
|
+
let replaced = false;
|
|
64
|
+
for (const group of groups) {
|
|
65
|
+
if (!Array.isArray(group.hooks)) continue;
|
|
66
|
+
for (const hook of group.hooks) {
|
|
67
|
+
if (LEGACY_COMMANDS.has(hook.command)) {
|
|
68
|
+
// Strip the "node " prefix → ".claude/hooks/..."
|
|
69
|
+
hook.command = hook.command.replace(/^node /, "");
|
|
70
|
+
replaced = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return replaced;
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
/**
|
|
50
78
|
* Merge SkillRepo hooks into .claude/settings.local.json and write hook scripts.
|
|
51
79
|
* @param {object} hooksConfig - The hooks config object from the server payload (settingsHooks.hooks)
|
|
@@ -60,16 +88,17 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
60
88
|
const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
|
|
61
89
|
const results = [];
|
|
62
90
|
|
|
63
|
-
// Always write hook scripts (latest version from server)
|
|
91
|
+
// Always write hook scripts (latest version from server) and mark executable.
|
|
92
|
+
// Scripts have #!/usr/bin/env node shebangs and are invoked directly (not via `node`).
|
|
64
93
|
const syncExisted = readFileSafe(claudeSyncHook()) !== null;
|
|
65
|
-
|
|
94
|
+
writeExecutable(claudeSyncHook(), syncHookContent);
|
|
66
95
|
results.push({
|
|
67
96
|
path: ".claude/hooks/skillrepo-sync.mjs",
|
|
68
97
|
action: syncExisted ? "updated" : "created",
|
|
69
98
|
});
|
|
70
99
|
|
|
71
100
|
const promptExisted = readFileSafe(claudePromptHook()) !== null;
|
|
72
|
-
|
|
101
|
+
writeExecutable(claudePromptHook(), promptHookContent);
|
|
73
102
|
results.push({
|
|
74
103
|
path: ".claude/hooks/skillrepo-prompt-match.mjs",
|
|
75
104
|
action: promptExisted ? "updated" : "created",
|
|
@@ -77,7 +106,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
77
106
|
|
|
78
107
|
if (preToolHookContent) {
|
|
79
108
|
const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
|
|
80
|
-
|
|
109
|
+
writeExecutable(claudePreToolHook(), preToolHookContent);
|
|
81
110
|
results.push({
|
|
82
111
|
path: ".claude/hooks/skillrepo-pretool-match.mjs",
|
|
83
112
|
action: preToolExisted ? "updated" : "created",
|
|
@@ -111,6 +140,13 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
111
140
|
if (!config.hooks) config.hooks = {};
|
|
112
141
|
let changed = false;
|
|
113
142
|
|
|
143
|
+
// Migrate legacy "node .claude/hooks/..." commands to direct execution
|
|
144
|
+
for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse"]) {
|
|
145
|
+
if (Array.isArray(config.hooks[event]) && replaceLegacyCommands(config.hooks[event])) {
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
114
150
|
// Merge SessionStart
|
|
115
151
|
if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
|
|
116
152
|
if (!hasCommand(config.hooks.SessionStart, syncCommand)) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
|
|
7
|
+
import { readFileSafe, writeFileSafe, writeExecutable } from "./fs-utils.mjs";
|
|
8
8
|
import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig } from "./paths.mjs";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -95,7 +95,7 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
|
|
|
95
95
|
}
|
|
96
96
|
if (payload.cursor?.sessionHook) {
|
|
97
97
|
const existed = readFileSafe(payload.cursor.sessionHook.path) !== null;
|
|
98
|
-
|
|
98
|
+
writeExecutable(payload.cursor.sessionHook.path, payload.cursor.sessionHook.content);
|
|
99
99
|
results.push({ path: payload.cursor.sessionHook.path, action: existed ? "updated" : "created" });
|
|
100
100
|
}
|
|
101
101
|
if (payload.cursor?.skillIndex) {
|
|
@@ -187,7 +187,7 @@ function buildSyncHook(baseUrl) {
|
|
|
187
187
|
return `#!/usr/bin/env node
|
|
188
188
|
// SkillRepo SessionStart hook — auto-refreshes skill config files.
|
|
189
189
|
// Installed by npx skillrepo init. Commit this file to your repo.
|
|
190
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
190
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
191
191
|
import { join, dirname } from "path";
|
|
192
192
|
|
|
193
193
|
const SETUP_URL = "${setupUrl}";
|
|
@@ -223,6 +223,11 @@ function safeWrite(relPath, content) {
|
|
|
223
223
|
writeFileSync(p, content, "utf-8");
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
function writeExecutable(relPath, content) {
|
|
227
|
+
safeWrite(relPath, content);
|
|
228
|
+
chmodSync(join(process.cwd(), relPath), 0o755);
|
|
229
|
+
}
|
|
230
|
+
|
|
226
231
|
try {
|
|
227
232
|
const res = await fetch(SETUP_URL, {
|
|
228
233
|
headers: { Authorization: \`Bearer \${API_KEY}\`, Accept: "application/json" },
|
|
@@ -234,8 +239,8 @@ try {
|
|
|
234
239
|
if (cc.skillrepoMd?.content) safeWrite(cc.skillrepoMd.path, cc.skillrepoMd.content);
|
|
235
240
|
if (cc.skillIndex?.content) safeWrite(cc.skillIndex.path, cc.skillIndex.content);
|
|
236
241
|
if (cc.skillrepoConfig?.content) safeWrite(cc.skillrepoConfig.path, cc.skillrepoConfig.content);
|
|
237
|
-
if (cc.promptHook?.content)
|
|
238
|
-
if (cc.preToolHook?.content)
|
|
242
|
+
if (cc.promptHook?.content) writeExecutable(cc.promptHook.path, cc.promptHook.content);
|
|
243
|
+
if (cc.preToolHook?.content) writeExecutable(cc.preToolHook.path, cc.preToolHook.content);
|
|
239
244
|
} catch { /* silently fail */ }
|
|
240
245
|
|
|
241
246
|
// Repo profiling
|
|
@@ -473,12 +478,13 @@ import { tmpdir } from "os";
|
|
|
473
478
|
|
|
474
479
|
const CONTENT_URL = "${contentUrl}";
|
|
475
480
|
|
|
476
|
-
let hookConfig = { maxTotalBytes: 10000, reinjectionLineThreshold: 50 };
|
|
481
|
+
let hookConfig = { maxTotalBytes: 10000, reinjectionLineThreshold: 50, maxSkillsPerPrompt: 3 };
|
|
477
482
|
try {
|
|
478
483
|
const cfg = JSON.parse(readFileSync(join(process.cwd(), ".claude", "skillrepo-config.json"), "utf-8"));
|
|
479
484
|
if (cfg.hookInjection) hookConfig = { ...hookConfig, ...cfg.hookInjection };
|
|
480
485
|
} catch {}
|
|
481
486
|
const MAX_BYTES = hookConfig.maxTotalBytes;
|
|
487
|
+
const MAX_SKILLS = hookConfig.maxSkillsPerPrompt;
|
|
482
488
|
const REINJECT_THRESHOLD = hookConfig.reinjectionLineThreshold ?? 50;
|
|
483
489
|
|
|
484
490
|
let dynamicSignals = [];
|
|
@@ -489,13 +495,8 @@ try {
|
|
|
489
495
|
}
|
|
490
496
|
} catch {}
|
|
491
497
|
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
{ toolName: "skill__affaan-m__e2e-testing", files: ["**/*.test.*", "**/*.spec.*"], project: [], tasks: [] },
|
|
495
|
-
{ toolName: "skill__atxpace__gh-issue-workflow", files: [], project: [], tasks: ["github issue", "pull request"] },
|
|
496
|
-
{ toolName: "skill__affaan-m__api-routes", files: ["**/api/**/route.ts", "**/api/**/route.tsx"], project: [], tasks: [] },
|
|
497
|
-
];
|
|
498
|
-
const SIGNALS = dynamicSignals.length > 0 ? dynamicSignals : FALLBACK_SIGNALS;
|
|
498
|
+
const SIGNALS = dynamicSignals;
|
|
499
|
+
if (SIGNALS.length === 0) { process.stdout.write("{}"); process.exit(0); }
|
|
499
500
|
|
|
500
501
|
// Minimal glob matcher — self-contained, no npm deps.
|
|
501
502
|
function matchGlob(filePath, pattern) {
|
|
@@ -621,7 +622,7 @@ if (matchedSkills.length === 0) { process.stdout.write("{}"); process.exit(0); }
|
|
|
621
622
|
|
|
622
623
|
const { path: statePath, state } = readState(sessionId);
|
|
623
624
|
const currentLines = countTranscriptLines(transcriptPath);
|
|
624
|
-
const candidates = matchedSkills.filter((s) => shouldReinject(s.toolName, state, currentLines));
|
|
625
|
+
const candidates = matchedSkills.filter((s) => shouldReinject(s.toolName, state, currentLines)).slice(0, MAX_SKILLS);
|
|
625
626
|
if (candidates.length === 0) { process.stdout.write("{}"); process.exit(0); }
|
|
626
627
|
|
|
627
628
|
const contents = [];
|
|
@@ -675,21 +676,21 @@ function buildSettingsHooks() {
|
|
|
675
676
|
{
|
|
676
677
|
matcher: "startup|resume",
|
|
677
678
|
hooks: [
|
|
678
|
-
{ type: "command", command: "
|
|
679
|
+
{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" },
|
|
679
680
|
],
|
|
680
681
|
},
|
|
681
682
|
],
|
|
682
683
|
UserPromptSubmit: [
|
|
683
684
|
{
|
|
684
685
|
hooks: [
|
|
685
|
-
{ type: "command", command: "
|
|
686
|
+
{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" },
|
|
686
687
|
],
|
|
687
688
|
},
|
|
688
689
|
],
|
|
689
690
|
PreToolUse: [
|
|
690
691
|
{
|
|
691
692
|
hooks: [
|
|
692
|
-
{ type: "command", command: "
|
|
693
|
+
{ type: "command", command: ".claude/hooks/skillrepo-pretool-match.mjs" },
|
|
693
694
|
],
|
|
694
695
|
},
|
|
695
696
|
],
|
|
@@ -766,7 +767,7 @@ function buildCursorHooksJson() {
|
|
|
766
767
|
hooks: [
|
|
767
768
|
{
|
|
768
769
|
event: "sessionStart",
|
|
769
|
-
command: "
|
|
770
|
+
command: ".cursor/hooks/skillrepo-session.mjs",
|
|
770
771
|
},
|
|
771
772
|
],
|
|
772
773
|
}, null, 2) + "\n";
|
|
@@ -23,9 +23,9 @@ const PROMPT_CONTENT = "// prompt hook";
|
|
|
23
23
|
const PRETOOL_CONTENT = "// pretool hook";
|
|
24
24
|
|
|
25
25
|
const HOOKS_CONFIG = {
|
|
26
|
-
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "
|
|
27
|
-
UserPromptSubmit: [{ hooks: [{ type: "command", command: "
|
|
28
|
-
PreToolUse: [{ hooks: [{ type: "command", command: "
|
|
26
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
27
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
28
|
+
PreToolUse: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-pretool-match.mjs" }] }],
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
describe("Hooks settings.local.json merger", () => {
|
|
@@ -86,8 +86,8 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
86
86
|
// Pre-existing with both hooks already
|
|
87
87
|
const existing = {
|
|
88
88
|
hooks: {
|
|
89
|
-
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "
|
|
90
|
-
UserPromptSubmit: [{ hooks: [{ type: "command", command: "
|
|
89
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
90
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
@@ -105,7 +105,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
105
105
|
// Only SessionStart installed
|
|
106
106
|
const existing = {
|
|
107
107
|
hooks: {
|
|
108
|
-
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "
|
|
108
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
@@ -119,7 +119,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
119
119
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
120
120
|
assert.equal(config.hooks.SessionStart.length, 1);
|
|
121
121
|
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
122
|
-
assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, "
|
|
122
|
+
assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it("always updates hook scripts even when settings merge is skipped", async () => {
|
|
@@ -132,8 +132,8 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
132
132
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
133
133
|
const existing = {
|
|
134
134
|
hooks: {
|
|
135
|
-
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "
|
|
136
|
-
UserPromptSubmit: [{ hooks: [{ type: "command", command: "
|
|
135
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
136
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(existing, null, 2));
|
|
@@ -203,7 +203,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
203
203
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
204
204
|
assert.ok(Array.isArray(config.hooks.SessionStart), "SessionStart should be an array");
|
|
205
205
|
assert.equal(config.hooks.SessionStart.length, 1);
|
|
206
|
-
assert.equal(config.hooks.SessionStart[0].hooks[0].command, "
|
|
206
|
+
assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it("documents partial write when JSON parse fails (hook scripts written before throw)", async () => {
|
|
@@ -244,7 +244,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
244
244
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
245
245
|
assert.ok(Array.isArray(config.hooks.PreToolUse), "PreToolUse should be an array");
|
|
246
246
|
assert.equal(config.hooks.PreToolUse.length, 1);
|
|
247
|
-
assert.equal(config.hooks.PreToolUse[0].hooks[0].command, "
|
|
247
|
+
assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
it("skips pretool hook when preToolHookContent is not provided", async () => {
|
|
@@ -270,4 +270,43 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
270
270
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
271
271
|
assert.equal(config.hooks.PreToolUse.length, 1, "should not duplicate PreToolUse entry");
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
it("migrates legacy 'node .claude/hooks/...' commands to direct execution", async () => {
|
|
275
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
276
|
+
|
|
277
|
+
// Pre-existing settings with OLD command format (node .claude/hooks/...)
|
|
278
|
+
const oldSettings = {
|
|
279
|
+
hooks: {
|
|
280
|
+
SessionStart: [{
|
|
281
|
+
matcher: "startup|resume",
|
|
282
|
+
hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }]
|
|
283
|
+
}],
|
|
284
|
+
UserPromptSubmit: [{
|
|
285
|
+
hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }]
|
|
286
|
+
}],
|
|
287
|
+
PreToolUse: [{
|
|
288
|
+
hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-pretool-match.mjs" }]
|
|
289
|
+
}]
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
293
|
+
writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
|
|
294
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
295
|
+
|
|
296
|
+
// Run merger with NEW hook config (HOOKS_CONFIG uses .claude/hooks/... without "node" prefix)
|
|
297
|
+
const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
|
|
298
|
+
|
|
299
|
+
// Settings should show "merged" (not "created" or "skipped")
|
|
300
|
+
const settingsResult = results.find((r) => r.path.includes("settings.local.json"));
|
|
301
|
+
assert.equal(settingsResult.action, "merged");
|
|
302
|
+
|
|
303
|
+
// Read the result and verify: ONE entry per event, with NEW command format
|
|
304
|
+
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
305
|
+
assert.equal(config.hooks.SessionStart.length, 1);
|
|
306
|
+
assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
|
|
307
|
+
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
308
|
+
assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
|
|
309
|
+
assert.equal(config.hooks.PreToolUse.length, 1);
|
|
310
|
+
assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
|
|
311
|
+
});
|
|
273
312
|
});
|