skillrepo 1.6.1 → 1.6.2
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
|
@@ -37,7 +37,7 @@ export function writeFileSafe(filePath, content) {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Write a file and mark it executable (0o755).
|
|
40
|
-
* Used for
|
|
40
|
+
* Used for the Cursor session hook which is invoked directly via shebang.
|
|
41
41
|
*/
|
|
42
42
|
export function writeExecutable(filePath, content) {
|
|
43
43
|
writeFileSafe(filePath, content);
|
|
@@ -8,70 +8,59 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { unlinkSync } from "node:fs";
|
|
11
|
-
import { readFileSafe, writeFileSafe
|
|
11
|
+
import { readFileSafe, writeFileSafe } 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
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
]);
|
|
15
|
+
// The three hook script paths (relative to project root).
|
|
16
|
+
const HOOK_SCRIPTS = {
|
|
17
|
+
sync: ".claude/hooks/skillrepo-sync.mjs",
|
|
18
|
+
prompt: ".claude/hooks/skillrepo-prompt-match.mjs",
|
|
19
|
+
pretool: ".claude/hooks/skillrepo-pretool-match.mjs",
|
|
20
|
+
};
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @param {string} fallback - default command if extraction fails
|
|
32
|
-
* @returns {string}
|
|
23
|
+
* Build the full command string for a hook script.
|
|
24
|
+
* Uses process.execPath (the absolute path to the current Node binary)
|
|
25
|
+
* so hooks work on nvm/fnm/mise systems where `node` is not on /bin/sh's PATH.
|
|
33
26
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
return hooksConfig[eventName][0].hooks[0].command || fallback;
|
|
37
|
-
} catch {
|
|
38
|
-
return fallback;
|
|
39
|
-
}
|
|
27
|
+
function buildCommand(scriptPath) {
|
|
28
|
+
return `"${process.execPath}" ${scriptPath}`;
|
|
40
29
|
}
|
|
41
30
|
|
|
42
31
|
/**
|
|
43
|
-
* Check if a
|
|
44
|
-
*
|
|
45
|
-
* @param {string} command
|
|
46
|
-
* @returns {boolean}
|
|
32
|
+
* Check if a SkillRepo hook for the given script path is already installed,
|
|
33
|
+
* regardless of how node was referenced (bare `node`, shebang, or full path).
|
|
47
34
|
*/
|
|
48
|
-
function
|
|
35
|
+
function hasSkillRepoHook(groups, scriptPath) {
|
|
49
36
|
if (!Array.isArray(groups)) return false;
|
|
50
37
|
return groups.some(
|
|
51
38
|
(group) =>
|
|
52
39
|
Array.isArray(group.hooks) &&
|
|
53
|
-
group.hooks.some((h) => h.command
|
|
40
|
+
group.hooks.some((h) => h.command?.includes(scriptPath))
|
|
54
41
|
);
|
|
55
42
|
}
|
|
56
43
|
|
|
57
44
|
/**
|
|
58
|
-
* Replace
|
|
59
|
-
*
|
|
45
|
+
* Replace any existing SkillRepo hook command with the current node path.
|
|
46
|
+
* Handles all legacy formats: `node .claude/hooks/...`, `.claude/hooks/...`,
|
|
47
|
+
* or `/old/path/to/node .claude/hooks/...`.
|
|
48
|
+
* Returns true if any replacement was made.
|
|
60
49
|
*/
|
|
61
|
-
function
|
|
50
|
+
function updateExistingCommands(groups, scriptPath) {
|
|
62
51
|
if (!Array.isArray(groups)) return false;
|
|
63
|
-
|
|
52
|
+
const newCommand = buildCommand(scriptPath);
|
|
53
|
+
let updated = false;
|
|
64
54
|
for (const group of groups) {
|
|
65
55
|
if (!Array.isArray(group.hooks)) continue;
|
|
66
56
|
for (const hook of group.hooks) {
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
replaced = true;
|
|
57
|
+
if (hook.command?.includes(scriptPath) && hook.command !== newCommand) {
|
|
58
|
+
hook.command = newCommand;
|
|
59
|
+
updated = true;
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
|
-
return
|
|
63
|
+
return updated;
|
|
75
64
|
}
|
|
76
65
|
|
|
77
66
|
/**
|
|
@@ -82,33 +71,32 @@ function replaceLegacyCommands(groups) {
|
|
|
82
71
|
* @param {string} [preToolHookContent] - The pretool-match hook script content (optional for backward compat)
|
|
83
72
|
* @returns {{ results: { path: string; action: string }[] }}
|
|
84
73
|
*/
|
|
74
|
+
// NOTE: The hooksConfig parameter is accepted for backward compatibility but
|
|
75
|
+
// its command strings are not used. All commands are built locally using
|
|
76
|
+
// process.execPath to ensure hooks work on nvm/fnm/mise systems.
|
|
85
77
|
export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent, preToolHookContent) {
|
|
86
|
-
const syncCommand = extractCommand(hooksConfig, "SessionStart", DEFAULT_SYNC_COMMAND);
|
|
87
|
-
const promptCommand = extractCommand(hooksConfig, "UserPromptSubmit", DEFAULT_PROMPT_COMMAND);
|
|
88
|
-
const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
|
|
89
78
|
const results = [];
|
|
90
79
|
|
|
91
|
-
// Always write hook scripts (latest version from server)
|
|
92
|
-
// Scripts have #!/usr/bin/env node shebangs and are invoked directly (not via `node`).
|
|
80
|
+
// Always write hook scripts (latest version from server).
|
|
93
81
|
const syncExisted = readFileSafe(claudeSyncHook()) !== null;
|
|
94
|
-
|
|
82
|
+
writeFileSafe(claudeSyncHook(), syncHookContent);
|
|
95
83
|
results.push({
|
|
96
|
-
path:
|
|
84
|
+
path: HOOK_SCRIPTS.sync,
|
|
97
85
|
action: syncExisted ? "updated" : "created",
|
|
98
86
|
});
|
|
99
87
|
|
|
100
88
|
const promptExisted = readFileSafe(claudePromptHook()) !== null;
|
|
101
|
-
|
|
89
|
+
writeFileSafe(claudePromptHook(), promptHookContent);
|
|
102
90
|
results.push({
|
|
103
|
-
path:
|
|
91
|
+
path: HOOK_SCRIPTS.prompt,
|
|
104
92
|
action: promptExisted ? "updated" : "created",
|
|
105
93
|
});
|
|
106
94
|
|
|
107
95
|
if (preToolHookContent) {
|
|
108
96
|
const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
|
|
109
|
-
|
|
97
|
+
writeFileSafe(claudePreToolHook(), preToolHookContent);
|
|
110
98
|
results.push({
|
|
111
|
-
path:
|
|
99
|
+
path: HOOK_SCRIPTS.pretool,
|
|
112
100
|
action: preToolExisted ? "updated" : "created",
|
|
113
101
|
});
|
|
114
102
|
}
|
|
@@ -140,16 +128,28 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
140
128
|
if (!config.hooks) config.hooks = {};
|
|
141
129
|
let changed = false;
|
|
142
130
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
// Build commands using the absolute node path from this machine.
|
|
132
|
+
// This ensures hooks work on nvm/fnm/mise systems where `node` is
|
|
133
|
+
// not on /bin/sh's PATH.
|
|
134
|
+
const syncCommand = buildCommand(HOOK_SCRIPTS.sync);
|
|
135
|
+
const promptCommand = buildCommand(HOOK_SCRIPTS.prompt);
|
|
136
|
+
const preToolCommand = buildCommand(HOOK_SCRIPTS.pretool);
|
|
137
|
+
|
|
138
|
+
// Update existing SkillRepo hook commands to use the current node path
|
|
139
|
+
// (handles all legacy formats: bare `node`, shebang paths, old full paths)
|
|
140
|
+
for (const [event, scriptPath] of [
|
|
141
|
+
["SessionStart", HOOK_SCRIPTS.sync],
|
|
142
|
+
["UserPromptSubmit", HOOK_SCRIPTS.prompt],
|
|
143
|
+
["PreToolUse", HOOK_SCRIPTS.pretool],
|
|
144
|
+
]) {
|
|
145
|
+
if (Array.isArray(config.hooks[event]) && updateExistingCommands(config.hooks[event], scriptPath)) {
|
|
146
146
|
changed = true;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Merge SessionStart
|
|
151
151
|
if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
|
|
152
|
-
if (!
|
|
152
|
+
if (!hasSkillRepoHook(config.hooks.SessionStart, HOOK_SCRIPTS.sync)) {
|
|
153
153
|
config.hooks.SessionStart.push({
|
|
154
154
|
matcher: "startup|resume",
|
|
155
155
|
hooks: [{ type: "command", command: syncCommand }],
|
|
@@ -159,7 +159,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
159
159
|
|
|
160
160
|
// Merge UserPromptSubmit
|
|
161
161
|
if (!Array.isArray(config.hooks.UserPromptSubmit)) config.hooks.UserPromptSubmit = [];
|
|
162
|
-
if (!
|
|
162
|
+
if (!hasSkillRepoHook(config.hooks.UserPromptSubmit, HOOK_SCRIPTS.prompt)) {
|
|
163
163
|
config.hooks.UserPromptSubmit.push({
|
|
164
164
|
hooks: [{ type: "command", command: promptCommand }],
|
|
165
165
|
});
|
|
@@ -169,7 +169,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
169
169
|
// Merge PreToolUse
|
|
170
170
|
if (preToolHookContent) {
|
|
171
171
|
if (!Array.isArray(config.hooks.PreToolUse)) config.hooks.PreToolUse = [];
|
|
172
|
-
if (!
|
|
172
|
+
if (!hasSkillRepoHook(config.hooks.PreToolUse, HOOK_SCRIPTS.pretool)) {
|
|
173
173
|
config.hooks.PreToolUse.push({
|
|
174
174
|
hooks: [{ type: "command", command: preToolCommand }],
|
|
175
175
|
});
|
|
@@ -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
|
|
190
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
191
191
|
import { join, dirname } from "path";
|
|
192
192
|
|
|
193
193
|
const SETUP_URL = "${setupUrl}";
|
|
@@ -223,11 +223,6 @@ 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
|
-
|
|
231
226
|
try {
|
|
232
227
|
const res = await fetch(SETUP_URL, {
|
|
233
228
|
headers: { Authorization: \`Bearer \${API_KEY}\`, Accept: "application/json" },
|
|
@@ -239,8 +234,8 @@ try {
|
|
|
239
234
|
if (cc.skillrepoMd?.content) safeWrite(cc.skillrepoMd.path, cc.skillrepoMd.content);
|
|
240
235
|
if (cc.skillIndex?.content) safeWrite(cc.skillIndex.path, cc.skillIndex.content);
|
|
241
236
|
if (cc.skillrepoConfig?.content) safeWrite(cc.skillrepoConfig.path, cc.skillrepoConfig.content);
|
|
242
|
-
if (cc.promptHook?.content)
|
|
243
|
-
if (cc.preToolHook?.content)
|
|
237
|
+
if (cc.promptHook?.content) safeWrite(cc.promptHook.path, cc.promptHook.content);
|
|
238
|
+
if (cc.preToolHook?.content) safeWrite(cc.preToolHook.path, cc.preToolHook.content);
|
|
244
239
|
} catch { /* silently fail */ }
|
|
245
240
|
|
|
246
241
|
// Repo profiling
|
|
@@ -22,6 +22,7 @@ const SYNC_CONTENT = "// sync hook";
|
|
|
22
22
|
const PROMPT_CONTENT = "// prompt hook";
|
|
23
23
|
const PRETOOL_CONTENT = "// pretool hook";
|
|
24
24
|
|
|
25
|
+
// Server payload hook commands (bare script paths — the CLI builds full commands locally)
|
|
25
26
|
const HOOKS_CONFIG = {
|
|
26
27
|
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
27
28
|
UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
|
|
@@ -80,14 +81,15 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
80
81
|
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
81
82
|
});
|
|
82
83
|
|
|
83
|
-
it("skips merge when hooks are already installed", async () => {
|
|
84
|
+
it("skips merge when hooks are already installed with current node path", async () => {
|
|
84
85
|
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
85
86
|
|
|
86
|
-
// Pre-existing with
|
|
87
|
+
// Pre-existing with hooks using the CURRENT node path (already up to date)
|
|
88
|
+
const nodeCmd = (script) => `"${process.execPath}" ${script}`;
|
|
87
89
|
const existing = {
|
|
88
90
|
hooks: {
|
|
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
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-sync.mjs") }] }],
|
|
92
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-prompt-match.mjs") }] }],
|
|
91
93
|
},
|
|
92
94
|
};
|
|
93
95
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
@@ -119,21 +121,22 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
119
121
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
120
122
|
assert.equal(config.hooks.SessionStart.length, 1);
|
|
121
123
|
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
122
|
-
assert.
|
|
124
|
+
assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-prompt-match.mjs"), "Command should end with script path");
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
it("always updates hook scripts even when settings merge is skipped", async () => {
|
|
126
128
|
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
127
129
|
|
|
128
|
-
// Install once with old scripts
|
|
130
|
+
// Install once with old scripts but current-format commands
|
|
131
|
+
const nodeCmd = (script) => `"${process.execPath}" ${script}`;
|
|
129
132
|
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
130
133
|
writeFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "// old sync");
|
|
131
134
|
writeFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "// old prompt");
|
|
132
135
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
133
136
|
const existing = {
|
|
134
137
|
hooks: {
|
|
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" }] }],
|
|
138
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-sync.mjs") }] }],
|
|
139
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-prompt-match.mjs") }] }],
|
|
137
140
|
},
|
|
138
141
|
};
|
|
139
142
|
writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(existing, null, 2));
|
|
@@ -149,7 +152,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
149
152
|
assert.equal(promptResult.action, "updated");
|
|
150
153
|
assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "utf-8"), "// new prompt");
|
|
151
154
|
|
|
152
|
-
// Settings skipped
|
|
155
|
+
// Settings skipped (commands already use current node path)
|
|
153
156
|
const settingsResult = results.find((r) => r.path.includes("settings.local.json"));
|
|
154
157
|
assert.equal(settingsResult.action, "skipped");
|
|
155
158
|
});
|
|
@@ -203,7 +206,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
203
206
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
204
207
|
assert.ok(Array.isArray(config.hooks.SessionStart), "SessionStart should be an array");
|
|
205
208
|
assert.equal(config.hooks.SessionStart.length, 1);
|
|
206
|
-
assert.
|
|
209
|
+
assert.ok(config.hooks.SessionStart[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Command should end with script path");
|
|
207
210
|
});
|
|
208
211
|
|
|
209
212
|
it("documents partial write when JSON parse fails (hook scripts written before throw)", async () => {
|
|
@@ -244,7 +247,7 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
244
247
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
245
248
|
assert.ok(Array.isArray(config.hooks.PreToolUse), "PreToolUse should be an array");
|
|
246
249
|
assert.equal(config.hooks.PreToolUse.length, 1);
|
|
247
|
-
assert.
|
|
250
|
+
assert.ok(config.hooks.PreToolUse[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-pretool-match.mjs"), "Command should end with script path");
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
it("skips pretool hook when preToolHookContent is not provided", async () => {
|
|
@@ -271,10 +274,10 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
271
274
|
assert.equal(config.hooks.PreToolUse.length, 1, "should not duplicate PreToolUse entry");
|
|
272
275
|
});
|
|
273
276
|
|
|
274
|
-
it("migrates
|
|
277
|
+
it("migrates any non-current-node-path commands to use process.execPath", async () => {
|
|
275
278
|
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
276
279
|
|
|
277
|
-
// Pre-existing settings with
|
|
280
|
+
// Pre-existing settings with bare `node` command format
|
|
278
281
|
const oldSettings = {
|
|
279
282
|
hooks: {
|
|
280
283
|
SessionStart: [{
|
|
@@ -303,10 +306,55 @@ describe("Hooks settings.local.json merger", () => {
|
|
|
303
306
|
// Read the result and verify: ONE entry per event, with NEW command format
|
|
304
307
|
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
305
308
|
assert.equal(config.hooks.SessionStart.length, 1);
|
|
306
|
-
assert.
|
|
309
|
+
assert.ok(config.hooks.SessionStart[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
|
|
310
|
+
assert.ok(config.hooks.SessionStart[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Command should end with script path");
|
|
307
311
|
assert.equal(config.hooks.UserPromptSubmit.length, 1);
|
|
308
|
-
assert.
|
|
312
|
+
assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
|
|
313
|
+
assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-prompt-match.mjs"), "Command should end with script path");
|
|
309
314
|
assert.equal(config.hooks.PreToolUse.length, 1);
|
|
310
|
-
assert.
|
|
315
|
+
assert.ok(config.hooks.PreToolUse[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
|
|
316
|
+
assert.ok(config.hooks.PreToolUse[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-pretool-match.mjs"), "Command should end with script path");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("migrates bare shebang-style commands (v1.6.1 format)", async () => {
|
|
320
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
321
|
+
|
|
322
|
+
// v1.6.1 used bare script paths relying on shebang execution
|
|
323
|
+
const oldSettings = {
|
|
324
|
+
hooks: {
|
|
325
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
329
|
+
writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
|
|
330
|
+
|
|
331
|
+
const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT);
|
|
332
|
+
|
|
333
|
+
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
334
|
+
assert.equal(config.hooks.SessionStart.length, 1, "Should not duplicate");
|
|
335
|
+
const cmd = config.hooks.SessionStart[0].hooks[0].command;
|
|
336
|
+
assert.ok(cmd.includes(process.execPath), "Should contain full node path");
|
|
337
|
+
assert.ok(cmd.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Should end with script path");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("migrates old full-path-to-node commands (nvm version change)", async () => {
|
|
341
|
+
const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
|
|
342
|
+
|
|
343
|
+
// User ran init with an older nvm node version
|
|
344
|
+
const oldSettings = {
|
|
345
|
+
hooks: {
|
|
346
|
+
SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "/usr/local/bin/node .claude/hooks/skillrepo-sync.mjs" }] }],
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
|
|
350
|
+
writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
|
|
351
|
+
|
|
352
|
+
const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT);
|
|
353
|
+
|
|
354
|
+
const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
|
|
355
|
+
assert.equal(config.hooks.SessionStart.length, 1, "Should not duplicate");
|
|
356
|
+
const cmd = config.hooks.SessionStart[0].hooks[0].command;
|
|
357
|
+
assert.ok(cmd.includes(process.execPath), "Should contain CURRENT node path");
|
|
358
|
+
assert.ok(!cmd.includes("/usr/local/bin/node"), "Should NOT contain old node path");
|
|
311
359
|
});
|
|
312
360
|
});
|