ronds_ai 0.1.13 → 0.1.17
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 +41 -21
- package/bin/ronds_ai.js +60 -8
- package/lib/code_record.js +177 -317
- package/lib/doctor.js +4 -2
- package/lib/hooks_deploy.js +604 -396
- package/package.json +1 -1
package/lib/hooks_deploy.js
CHANGED
|
@@ -1,397 +1,605 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const CLAUDE_MATCHER = 'Edit|Write|MultiEdit';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
|
|
6
|
+
const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
|
|
7
|
+
const CODEX_COMMAND = 'npx ronds_ai@latest record codex';
|
|
8
|
+
const CURSOR_OLD_COMMAND = 'node .cursor/hooks/cursor_hook_request.cjs';
|
|
9
|
+
const CURSOR_OLD_JS_COMMAND = 'node .cursor/hooks/cursor_hook_request.js';
|
|
10
|
+
const CLAUDE_OLD_COMMAND = 'node .claude/hooks/claude_hook_request.cjs';
|
|
11
|
+
const CLAUDE_OLD_JS_COMMAND = 'node .claude/hooks/claude_hook_request.js';
|
|
12
|
+
const CLAUDE_MATCHER = 'Edit|Write|MultiEdit';
|
|
13
|
+
const CODEX_HOOK_NAMES = new Set(['UserPromptSubmit', 'Stop', 'PostToolUse']);
|
|
14
|
+
const CODEX_MATCHER = 'apply_patch';
|
|
15
|
+
const CURSOR_MANAGED_COMMANDS = new Set([
|
|
16
|
+
CURSOR_COMMAND,
|
|
17
|
+
CURSOR_OLD_COMMAND,
|
|
18
|
+
CURSOR_OLD_JS_COMMAND,
|
|
19
|
+
]);
|
|
20
|
+
const CLAUDE_MANAGED_COMMANDS = new Set([
|
|
21
|
+
CLAUDE_COMMAND,
|
|
22
|
+
CLAUDE_OLD_COMMAND,
|
|
23
|
+
CLAUDE_OLD_JS_COMMAND,
|
|
24
|
+
]);
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureDir(dirPath) {
|
|
30
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJsonFile(filePath, fallback) {
|
|
34
|
+
if (!fs.existsSync(filePath)) {
|
|
35
|
+
return { data: fallback, exists: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
39
|
+
if (!raw) {
|
|
40
|
+
return { data: fallback, exists: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let data;
|
|
44
|
+
try {
|
|
45
|
+
data = JSON.parse(raw);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
48
|
+
throw new Error(`Invalid JSON in ${filePath}: ${detail}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isPlainObject(data)) {
|
|
52
|
+
throw new Error(`Expected top-level object in ${filePath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { data, exists: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeJsonFile(filePath, data) {
|
|
59
|
+
const nextContent = `${JSON.stringify(data, null, 2)}\n`;
|
|
60
|
+
if (fs.existsSync(filePath)) {
|
|
61
|
+
const currentContent = fs.readFileSync(filePath, 'utf8');
|
|
62
|
+
if (currentContent === nextContent) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ensureDir(path.dirname(filePath));
|
|
68
|
+
fs.writeFileSync(filePath, nextContent, 'utf8');
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeTextFileIfChanged(filePath, nextContent) {
|
|
73
|
+
if (fs.existsSync(filePath)) {
|
|
74
|
+
const currentContent = fs.readFileSync(filePath, 'utf8');
|
|
75
|
+
if (currentContent === nextContent) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ensureDir(path.dirname(filePath));
|
|
81
|
+
fs.writeFileSync(filePath, nextContent, 'utf8');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function removeFileIfExists(filePath, removedFiles) {
|
|
86
|
+
if (!fs.existsSync(filePath)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.rmSync(filePath, { force: true });
|
|
91
|
+
removedFiles.push(filePath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function removeKnownCommands(entries, commands) {
|
|
95
|
+
const input = Array.isArray(entries) ? entries : [];
|
|
96
|
+
return input.filter((entry) => {
|
|
97
|
+
return !(isPlainObject(entry) && entry.type === 'command' && commands.has(entry.command));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ensureCursorHook(config) {
|
|
102
|
+
const next = isPlainObject(config) ? { ...config } : {};
|
|
103
|
+
const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
|
|
104
|
+
const afterFileEdit = Array.isArray(hooks.afterFileEdit) ? hooks.afterFileEdit.slice() : [];
|
|
105
|
+
const filtered = removeKnownCommands(afterFileEdit, CURSOR_MANAGED_COMMANDS);
|
|
106
|
+
|
|
107
|
+
filtered.push({
|
|
108
|
+
type: 'command',
|
|
109
|
+
command: CURSOR_COMMAND,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
hooks.afterFileEdit = filtered;
|
|
113
|
+
next.version = next.version === undefined ? 1 : next.version;
|
|
114
|
+
next.hooks = hooks;
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function splitClaudeHooks(hooksConfig) {
|
|
119
|
+
const nextHooks = isPlainObject(hooksConfig) ? { ...hooksConfig } : {};
|
|
120
|
+
const postToolUse = Array.isArray(nextHooks.PostToolUse) ? nextHooks.PostToolUse : [];
|
|
121
|
+
const keepEntries = [];
|
|
122
|
+
const desiredHooks = [];
|
|
123
|
+
|
|
124
|
+
for (const entry of postToolUse) {
|
|
125
|
+
if (!isPlainObject(entry)) {
|
|
126
|
+
keepEntries.push(entry);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (entry.matcher !== CLAUDE_MATCHER) {
|
|
131
|
+
keepEntries.push(entry);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existingHooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
136
|
+
for (const hook of existingHooks) {
|
|
137
|
+
if (!(isPlainObject(hook) && hook.type === 'command' && CLAUDE_MANAGED_COMMANDS.has(hook.command))) {
|
|
138
|
+
desiredHooks.push(hook);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
desiredHooks.push({
|
|
144
|
+
type: 'command',
|
|
145
|
+
command: CLAUDE_COMMAND,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
withoutManagedHook: keepEntries,
|
|
150
|
+
desiredEntry: {
|
|
151
|
+
matcher: CLAUDE_MATCHER,
|
|
152
|
+
hooks: desiredHooks,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ensureClaudeSettings(config) {
|
|
158
|
+
const next = isPlainObject(config) ? { ...config } : {};
|
|
159
|
+
const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
|
|
160
|
+
const { withoutManagedHook, desiredEntry } = splitClaudeHooks(hooks);
|
|
161
|
+
|
|
162
|
+
hooks.PostToolUse = withoutManagedHook.concat([desiredEntry]);
|
|
163
|
+
next.hooks = hooks;
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function removeClaudeHookFromConfig(config) {
|
|
168
|
+
const next = isPlainObject(config) ? { ...config } : {};
|
|
169
|
+
if (!isPlainObject(next.hooks)) {
|
|
170
|
+
return next;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hooks = { ...next.hooks };
|
|
174
|
+
const postToolUse = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
|
|
175
|
+
const cleaned = [];
|
|
176
|
+
|
|
177
|
+
for (const entry of postToolUse) {
|
|
178
|
+
if (!isPlainObject(entry)) {
|
|
179
|
+
cleaned.push(entry);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (entry.matcher !== CLAUDE_MATCHER) {
|
|
184
|
+
cleaned.push(entry);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const existingHooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
189
|
+
const filteredHooks = removeKnownCommands(existingHooks, CLAUDE_MANAGED_COMMANDS);
|
|
190
|
+
|
|
191
|
+
if (filteredHooks.length > 0) {
|
|
192
|
+
cleaned.push({
|
|
193
|
+
...entry,
|
|
194
|
+
hooks: filteredHooks,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (cleaned.length > 0) {
|
|
200
|
+
hooks.PostToolUse = cleaned;
|
|
201
|
+
} else {
|
|
202
|
+
delete hooks.PostToolUse;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (Object.keys(hooks).length > 0) {
|
|
206
|
+
next.hooks = hooks;
|
|
207
|
+
} else {
|
|
208
|
+
delete next.hooks;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return next;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ensureCodexHooksFeatureFlag(toml) {
|
|
215
|
+
const newline = toml.includes('\r\n') ? '\r\n' : '\n';
|
|
216
|
+
const desiredLine = `hooks = true${newline}`;
|
|
217
|
+
const desiredSection = `[features]${newline}${desiredLine}`;
|
|
218
|
+
|
|
219
|
+
if (!toml.trim()) {
|
|
220
|
+
return desiredSection;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const featuresHeaderPattern = /^[ \t]*\[features\][ \t]*(?:#.*)?$/m;
|
|
224
|
+
const featuresHeaderMatch = featuresHeaderPattern.exec(toml);
|
|
225
|
+
if (!featuresHeaderMatch) {
|
|
226
|
+
const trimmed = toml.replace(/[ \t\r\n]*$/, '');
|
|
227
|
+
return `${trimmed}${newline}${newline}${desiredSection}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const headerStart = featuresHeaderMatch.index;
|
|
231
|
+
const headerEnd = headerStart + featuresHeaderMatch[0].length;
|
|
232
|
+
const sectionAfterHeaderStart = toml.indexOf('\n', headerEnd) === -1
|
|
233
|
+
? toml.length
|
|
234
|
+
: toml.indexOf('\n', headerEnd) + 1;
|
|
235
|
+
const afterHeader = toml.slice(sectionAfterHeaderStart);
|
|
236
|
+
const nextHeaderMatch = /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/m.exec(afterHeader);
|
|
237
|
+
const sectionEnd = nextHeaderMatch
|
|
238
|
+
? sectionAfterHeaderStart + nextHeaderMatch.index
|
|
239
|
+
: toml.length;
|
|
240
|
+
const section = toml.slice(headerStart, sectionEnd);
|
|
241
|
+
const flagPattern = /^([ \t]*hooks[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?$)/m;
|
|
242
|
+
const flagMatch = flagPattern.exec(section);
|
|
243
|
+
|
|
244
|
+
if (flagMatch) {
|
|
245
|
+
if (flagMatch[2] === 'true') {
|
|
246
|
+
return toml;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const nextSection = section.replace(flagPattern, '$1true$3');
|
|
250
|
+
return `${toml.slice(0, headerStart)}${nextSection}${toml.slice(sectionEnd)}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return `${toml.slice(0, sectionAfterHeaderStart)}${desiredLine}${toml.slice(sectionAfterHeaderStart)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function ensureCodexManagedDir(toml, reqPath) {
|
|
257
|
+
const newline = toml.includes('\r\n') ? '\r\n' : '\n';
|
|
258
|
+
const codexDir = path.dirname(path.resolve(reqPath));
|
|
259
|
+
const managedDirLine = 'managed_dir = "~/.codex/"';
|
|
260
|
+
const winManagedDirLine = `windows_managed_dir = '${codexDir}'`;
|
|
261
|
+
const desiredLines = `${managedDirLine}${newline}${winManagedDirLine}${newline}`;
|
|
262
|
+
const desiredSection = `[hooks]${newline}${desiredLines}`;
|
|
263
|
+
|
|
264
|
+
if (!toml.trim()) {
|
|
265
|
+
return desiredSection;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const hooksHeaderPattern = /^[ \t]*\[hooks\][ \t]*(?:#.*)?$/m;
|
|
269
|
+
const hooksHeaderMatch = hooksHeaderPattern.exec(toml);
|
|
270
|
+
|
|
271
|
+
if (!hooksHeaderMatch) {
|
|
272
|
+
const trimmed = toml.replace(/[ \t\r\n]*$/, '');
|
|
273
|
+
return `${trimmed}${newline}${newline}${desiredSection}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const headerStart = hooksHeaderMatch.index;
|
|
277
|
+
const headerEnd = headerStart + hooksHeaderMatch[0].length;
|
|
278
|
+
const sectionAfterHeaderStart = toml.indexOf('\n', headerEnd) === -1
|
|
279
|
+
? toml.length
|
|
280
|
+
: toml.indexOf('\n', headerEnd) + 1;
|
|
281
|
+
const afterHeader = toml.slice(sectionAfterHeaderStart);
|
|
282
|
+
const nextHeaderMatch = /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/m.exec(afterHeader);
|
|
283
|
+
const sectionEnd = nextHeaderMatch
|
|
284
|
+
? sectionAfterHeaderStart + nextHeaderMatch.index
|
|
285
|
+
: toml.length;
|
|
286
|
+
|
|
287
|
+
let section = toml.slice(headerStart, sectionEnd);
|
|
288
|
+
|
|
289
|
+
const managedDirPattern = /^([ \t]*managed_dir[ \t]*=[ \t]*).*$/m;
|
|
290
|
+
if (managedDirPattern.test(section)) {
|
|
291
|
+
section = section.replace(managedDirPattern, `$1"~/.codex/"`);
|
|
292
|
+
} else {
|
|
293
|
+
section = section.replace(/(\[hooks\][^\n]*\n)/, `$1${managedDirLine}${newline}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const winManagedDirPattern = /^([ \t]*windows_managed_dir[ \t]*=[ \t]*).*$/m;
|
|
297
|
+
if (winManagedDirPattern.test(section)) {
|
|
298
|
+
section = section.replace(winManagedDirPattern, (_, prefix) => `${prefix}'${codexDir}'`);
|
|
299
|
+
} else {
|
|
300
|
+
section = section.replace(
|
|
301
|
+
new RegExp(`(${managedDirLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\n]*\\n)`),
|
|
302
|
+
`$1${winManagedDirLine}${newline}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return `${toml.slice(0, headerStart)}${section}${toml.slice(sectionEnd)}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 清理 Codex requirements.toml 中由本工具管理的 hook command,保留同一 hook 下的用户自定义命令。
|
|
310
|
+
function removeRondsBlocksFromToml(content) {
|
|
311
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
312
|
+
const lines = content.split(/\r?\n/);
|
|
313
|
+
let i = 0;
|
|
314
|
+
const result = [];
|
|
315
|
+
|
|
316
|
+
while (i < lines.length) {
|
|
317
|
+
const entryMatch = /^\[\[hooks\.([^\]]+)\]\]/.exec(lines[i]);
|
|
318
|
+
|
|
319
|
+
if (!entryMatch || !CODEX_HOOK_NAMES.has(entryMatch[1])) {
|
|
320
|
+
result.push(lines[i]);
|
|
321
|
+
i += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const blockStart = i;
|
|
326
|
+
i += 1;
|
|
327
|
+
|
|
328
|
+
while (i < lines.length) {
|
|
329
|
+
const sectionMatch = /^\[+([^\]]+)\]/.exec(lines[i]);
|
|
330
|
+
if (sectionMatch) {
|
|
331
|
+
const header = sectionMatch[1];
|
|
332
|
+
const isSubSection = header.startsWith(`hooks.${entryMatch[1]}.`);
|
|
333
|
+
if (!isSubSection) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
i += 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const cleanedBlock = removeRondsCommandsFromTomlHookBlock(lines.slice(blockStart, i), entryMatch[1]);
|
|
341
|
+
for (const line of cleanedBlock) {
|
|
342
|
+
result.push(line);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let output = result.join(newline);
|
|
347
|
+
if (content.endsWith('\n') && !output.endsWith('\n')) {
|
|
348
|
+
output += newline;
|
|
349
|
+
}
|
|
350
|
+
return output;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 判断 TOML command 行是否精确匹配本工具管理的 Codex 采集命令。
|
|
354
|
+
function isCodexManagedCommandLine(line) {
|
|
355
|
+
const escapedCommand = CODEX_COMMAND.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
356
|
+
const commandPattern = new RegExp(`^[ \\t]*command[ \\t]*=[ \\t]*(["'])${escapedCommand}\\1[ \\t]*(?:#.*)?$`);
|
|
357
|
+
return commandPattern.test(line);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 判断父级 hook block 是否只包含本工具生成的 matcher,便于安全删除空壳 block。
|
|
361
|
+
function isGeneratedCodexParentBlock(parentLines, hookName) {
|
|
362
|
+
return parentLines.every((line, index) => {
|
|
363
|
+
if (index === 0) {
|
|
364
|
+
return line === `[[hooks.${hookName}]]`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (/^[ \t]*$/.test(line)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (hookName === 'PostToolUse') {
|
|
372
|
+
return new RegExp(`^[ \\t]*matcher[ \\t]*=[ \\t]*(["'])${CODEX_MATCHER}\\1[ \\t]*(?:#.*)?$`).test(line);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return /^[ \t]*matcher[ \t]*=[ \t]*["'][ \t]*["'][ \t]*(?:#.*)?$/.test(line);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 从单个 Codex hook 父 block 中仅移除本工具管理的子 command block。
|
|
380
|
+
function removeRondsCommandsFromTomlHookBlock(blockLines, hookName) {
|
|
381
|
+
const childHeaderPattern = new RegExp(`^\\[\\[hooks\\.${hookName}\\.hooks\\]\\]`);
|
|
382
|
+
const parentLines = [];
|
|
383
|
+
const childBlocks = [];
|
|
384
|
+
let i = 0;
|
|
385
|
+
|
|
386
|
+
while (i < blockLines.length) {
|
|
387
|
+
if (childHeaderPattern.test(blockLines[i])) {
|
|
388
|
+
const childStart = i;
|
|
389
|
+
i += 1;
|
|
390
|
+
|
|
391
|
+
while (i < blockLines.length && !/^\[+([^\]]+)\]/.test(blockLines[i])) {
|
|
392
|
+
i += 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
childBlocks.push(blockLines.slice(childStart, i));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
parentLines.push(blockLines[i]);
|
|
400
|
+
i += 1;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const keptChildren = childBlocks.filter((childBlock) => {
|
|
404
|
+
return !childBlock.some((line) => isCodexManagedCommandLine(line));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (keptChildren.length === 0 && childBlocks.length !== keptChildren.length && isGeneratedCodexParentBlock(parentLines, hookName)) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return parentLines.concat(...keptChildren);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function generateRondsHookBlocks(newline) {
|
|
415
|
+
return [
|
|
416
|
+
'',
|
|
417
|
+
'[[hooks.PostToolUse]]',
|
|
418
|
+
`matcher = "${CODEX_MATCHER}"`,
|
|
419
|
+
'',
|
|
420
|
+
'[[hooks.PostToolUse.hooks]]',
|
|
421
|
+
'type = "command"',
|
|
422
|
+
`command = "${CODEX_COMMAND}"`,
|
|
423
|
+
'',
|
|
424
|
+
].join(newline);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function ensureCodexRequirementsToml(reqPath) {
|
|
428
|
+
const exists = fs.existsSync(reqPath);
|
|
429
|
+
let content = exists ? fs.readFileSync(reqPath, 'utf8') : '';
|
|
430
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
431
|
+
content = content.slice(1);
|
|
39
432
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
throw new Error(`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (cleaned.length > 0) {
|
|
214
|
-
hooks.PostToolUse = cleaned;
|
|
215
|
-
} else {
|
|
216
|
-
delete hooks.PostToolUse;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (Object.keys(hooks).length > 0) {
|
|
220
|
-
next.hooks = hooks;
|
|
221
|
-
} else {
|
|
222
|
-
delete next.hooks;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return next;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function cleanupCodexManagedHooks(hooks) {
|
|
229
|
-
const withoutManagedCommands = removeKnownCommands(hooks, CODEX_MANAGED_COMMANDS);
|
|
230
|
-
return removeCommandEntriesByMatcher(withoutManagedCommands, CODEX_OLD_COMMAND_MATCHERS);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function ensureCodexHooksFeatureFlag(toml) {
|
|
234
|
-
const newline = toml.includes('\r\n') ? '\r\n' : '\n';
|
|
235
|
-
const desiredLine = `codex_hooks = true${newline}`;
|
|
236
|
-
const desiredSection = `[features]${newline}${desiredLine}`;
|
|
237
|
-
|
|
238
|
-
if (!toml.trim()) {
|
|
239
|
-
return desiredSection;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const featuresHeaderPattern = /^[ \t]*\[features\][ \t]*(?:#.*)?$/m;
|
|
243
|
-
const featuresHeaderMatch = featuresHeaderPattern.exec(toml);
|
|
244
|
-
if (!featuresHeaderMatch) {
|
|
245
|
-
const trimmed = toml.replace(/[ \t\r\n]*$/, '');
|
|
246
|
-
return `${trimmed}${newline}${newline}${desiredSection}`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const headerStart = featuresHeaderMatch.index;
|
|
250
|
-
const headerEnd = headerStart + featuresHeaderMatch[0].length;
|
|
251
|
-
const sectionAfterHeaderStart = toml.indexOf('\n', headerEnd) === -1
|
|
252
|
-
? toml.length
|
|
253
|
-
: toml.indexOf('\n', headerEnd) + 1;
|
|
254
|
-
const afterHeader = toml.slice(sectionAfterHeaderStart);
|
|
255
|
-
const nextHeaderMatch = /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/m.exec(afterHeader);
|
|
256
|
-
const sectionEnd = nextHeaderMatch
|
|
257
|
-
? sectionAfterHeaderStart + nextHeaderMatch.index
|
|
258
|
-
: toml.length;
|
|
259
|
-
const section = toml.slice(headerStart, sectionEnd);
|
|
260
|
-
const flagPattern = /^([ \t]*codex_hooks[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?$)/m;
|
|
261
|
-
const flagMatch = flagPattern.exec(section);
|
|
262
|
-
|
|
263
|
-
if (flagMatch) {
|
|
264
|
-
if (flagMatch[2] === 'true') {
|
|
265
|
-
return toml;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const nextSection = section.replace(flagPattern, '$1true$3');
|
|
269
|
-
return `${toml.slice(0, headerStart)}${nextSection}${toml.slice(sectionEnd)}`;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return `${toml.slice(0, sectionAfterHeaderStart)}${desiredLine}${toml.slice(sectionAfterHeaderStart)}`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function ensureCodexConfigToml(configPath) {
|
|
276
|
-
const exists = fs.existsSync(configPath);
|
|
277
|
-
const currentContent = exists ? fs.readFileSync(configPath, 'utf8') : '';
|
|
278
|
-
const nextContent = ensureCodexHooksFeatureFlag(currentContent);
|
|
279
|
-
const changed = writeTextFileIfChanged(configPath, nextContent);
|
|
280
|
-
|
|
281
|
-
return { exists, changed };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function ensureCodexHook(config) {
|
|
285
|
-
const next = isPlainObject(config) ? { ...config } : {};
|
|
286
|
-
const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
|
|
287
|
-
|
|
288
|
-
for (const hookName of ['UserPromptSubmit', 'Stop']) {
|
|
289
|
-
const entries = Array.isArray(hooks[hookName]) ? hooks[hookName].slice() : [];
|
|
290
|
-
const normalizedEntries = [];
|
|
291
|
-
|
|
292
|
-
for (const entry of entries) {
|
|
293
|
-
if (!isPlainObject(entry)) {
|
|
294
|
-
normalizedEntries.push(entry);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const innerHooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
299
|
-
const filteredHooks = cleanupCodexManagedHooks(innerHooks);
|
|
300
|
-
if (filteredHooks.length > 0) {
|
|
301
|
-
normalizedEntries.push({
|
|
302
|
-
...entry,
|
|
303
|
-
hooks: filteredHooks,
|
|
304
|
-
});
|
|
305
|
-
} else if (!Array.isArray(entry.hooks)) {
|
|
306
|
-
normalizedEntries.push(entry);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
normalizedEntries.push({
|
|
311
|
-
hooks: [
|
|
312
|
-
{
|
|
313
|
-
type: 'command',
|
|
314
|
-
command: CODEX_COMMAND,
|
|
315
|
-
},
|
|
316
|
-
],
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
hooks[hookName] = normalizedEntries;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
next.hooks = hooks;
|
|
323
|
-
return next;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function deployHooks(targetDir = process.cwd()) {
|
|
327
|
-
const baseDir = path.resolve(targetDir);
|
|
328
|
-
const removedFiles = [];
|
|
329
|
-
const updatedFiles = [];
|
|
330
|
-
const createdFiles = [];
|
|
331
|
-
|
|
332
|
-
removeFileIfExists(path.join(baseDir, '.cursor', 'hooks', 'cursor_hook_request.cjs'), removedFiles);
|
|
333
|
-
removeFileIfExists(path.join(baseDir, '.cursor', 'hooks', 'cursor_hook_request.js'), removedFiles);
|
|
334
|
-
removeFileIfExists(path.join(baseDir, '.claude', 'hooks', 'claude_hook_request.cjs'), removedFiles);
|
|
335
|
-
removeFileIfExists(path.join(baseDir, '.claude', 'hooks', 'claude_hook_request.js'), removedFiles);
|
|
336
|
-
removeFileIfExists(path.join(baseDir, '.claude', 'hooks', 'claide_hook_request.cjs'), removedFiles);
|
|
337
|
-
removeFileIfExists(path.join(baseDir, '.claude', 'hooks', 'claide_hook_request.js'), removedFiles);
|
|
338
|
-
removeFileIfExists(path.join(baseDir, '.codex', 'hooks', 'codex_hook_request.js'), removedFiles);
|
|
339
|
-
removeFileIfExists(path.join(baseDir, '.codex', 'hooks', 'codex_hook_request.cjs'), removedFiles);
|
|
340
|
-
removeFileIfExists(path.join(baseDir, '.codex', 'hooks', 'codex_hook_request.mjs'), removedFiles);
|
|
341
|
-
|
|
342
|
-
const cursorPath = path.join(baseDir, '.cursor', 'hooks.json');
|
|
343
|
-
const cursorResult = readJsonFile(cursorPath, {});
|
|
344
|
-
const nextCursor = ensureCursorHook(cursorResult.data);
|
|
345
|
-
if (writeJsonFile(cursorPath, nextCursor)) {
|
|
346
|
-
(cursorResult.exists ? updatedFiles : createdFiles).push(cursorPath);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const claudeSettingsPath = path.join(baseDir, '.claude', 'settings.json');
|
|
350
|
-
const claudeSettingsResult = readJsonFile(claudeSettingsPath, {});
|
|
351
|
-
const nextClaudeSettings = ensureClaudeSettings(claudeSettingsResult.data);
|
|
352
|
-
if (writeJsonFile(claudeSettingsPath, nextClaudeSettings)) {
|
|
353
|
-
(claudeSettingsResult.exists ? updatedFiles : createdFiles).push(claudeSettingsPath);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const codexDir = path.join(baseDir, '.codex');
|
|
357
|
-
const codexHooksPath = path.join(codexDir, 'hooks.json');
|
|
358
|
-
const codexHooksResult = readJsonFile(codexHooksPath, {});
|
|
359
|
-
const nextCodexHooks = ensureCodexHook(codexHooksResult.data);
|
|
360
|
-
if (writeJsonFile(codexHooksPath, nextCodexHooks)) {
|
|
361
|
-
(codexHooksResult.exists ? updatedFiles : createdFiles).push(codexHooksPath);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const codexConfigPath = path.join(codexDir, 'config.toml');
|
|
365
|
-
const codexConfigResult = ensureCodexConfigToml(codexConfigPath);
|
|
366
|
-
if (codexConfigResult.changed) {
|
|
367
|
-
(codexConfigResult.exists ? updatedFiles : createdFiles).push(codexConfigPath);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const localCandidates = [
|
|
371
|
-
path.join(baseDir, '.claude', 'settings.local.json'),
|
|
372
|
-
path.join(baseDir, '.claude', 'setting.local.json'),
|
|
373
|
-
];
|
|
374
|
-
|
|
375
|
-
for (const localPath of localCandidates) {
|
|
376
|
-
if (!fs.existsSync(localPath)) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const localResult = readJsonFile(localPath, {});
|
|
381
|
-
const nextLocal = removeClaudeHookFromConfig(localResult.data);
|
|
382
|
-
if (writeJsonFile(localPath, nextLocal)) {
|
|
383
|
-
updatedFiles.push(localPath);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
targetDir: baseDir,
|
|
389
|
-
removedFiles,
|
|
390
|
-
updatedFiles,
|
|
391
|
-
createdFiles,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
module.exports = {
|
|
396
|
-
deployHooks,
|
|
397
|
-
};
|
|
433
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
434
|
+
|
|
435
|
+
content = ensureCodexHooksFeatureFlag(content);
|
|
436
|
+
content = ensureCodexManagedDir(content, reqPath);
|
|
437
|
+
content = removeRondsBlocksFromToml(content);
|
|
438
|
+
content = content.replace(/[\t ]*\r?\n*$/, newline);
|
|
439
|
+
content += generateRondsHookBlocks(newline);
|
|
440
|
+
|
|
441
|
+
const changed = writeTextFileIfChanged(reqPath, content);
|
|
442
|
+
return { exists, changed };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function normalizeDeployScope(scope) {
|
|
446
|
+
const normalizedScope = String(scope || 'project').trim().toLowerCase();
|
|
447
|
+
if (normalizedScope !== 'project' && normalizedScope !== 'user') {
|
|
448
|
+
throw new Error(`Unsupported hooks deploy scope: ${scope || ''}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return normalizedScope;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveDeployPaths(targetDir, scope) {
|
|
455
|
+
const baseDir = path.resolve(targetDir);
|
|
456
|
+
const configRoot = scope === 'user' ? os.homedir() : baseDir;
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
targetDir: baseDir,
|
|
460
|
+
configRoot,
|
|
461
|
+
cursorHooksPath: path.join(configRoot, '.cursor', 'hooks.json'),
|
|
462
|
+
claudeSettingsPath: path.join(configRoot, '.claude', 'settings.json'),
|
|
463
|
+
claudeLocalCandidates: [
|
|
464
|
+
path.join(baseDir, '.claude', 'settings.local.json'),
|
|
465
|
+
path.join(baseDir, '.claude', 'setting.local.json'),
|
|
466
|
+
],
|
|
467
|
+
codexRequirementsPath: process.platform === 'win32'
|
|
468
|
+
? 'C:\\ProgramData\\OpenAI\\Codex\\requirements.toml'
|
|
469
|
+
: '/etc/codex/requirements.toml',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function recordWriteResult(filePath, existed, changed, result) {
|
|
474
|
+
if (!changed) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
(existed ? result.updatedFiles : result.createdFiles).push(filePath);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 按工具范围清理旧版脚本,避免 --tool 过滤时误删其他工具仍在使用的 hook 脚本。
|
|
482
|
+
function cleanupLegacyHookScripts(configRoot, result, tool) {
|
|
483
|
+
if (!tool || tool === 'cursor') {
|
|
484
|
+
removeFileIfExists(path.join(configRoot, '.cursor', 'hooks', 'cursor_hook_request.cjs'), result.removedFiles);
|
|
485
|
+
removeFileIfExists(path.join(configRoot, '.cursor', 'hooks', 'cursor_hook_request.js'), result.removedFiles);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!tool || tool === 'claude') {
|
|
489
|
+
removeFileIfExists(path.join(configRoot, '.claude', 'hooks', 'claude_hook_request.cjs'), result.removedFiles);
|
|
490
|
+
removeFileIfExists(path.join(configRoot, '.claude', 'hooks', 'claude_hook_request.js'), result.removedFiles);
|
|
491
|
+
removeFileIfExists(path.join(configRoot, '.claude', 'hooks', 'claide_hook_request.cjs'), result.removedFiles);
|
|
492
|
+
removeFileIfExists(path.join(configRoot, '.claude', 'hooks', 'claide_hook_request.js'), result.removedFiles);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!tool || tool === 'codex') {
|
|
496
|
+
removeFileIfExists(path.join(configRoot, '.codex', 'hooks', 'codex_hook_request.js'), result.removedFiles);
|
|
497
|
+
removeFileIfExists(path.join(configRoot, '.codex', 'hooks', 'codex_hook_request.cjs'), result.removedFiles);
|
|
498
|
+
removeFileIfExists(path.join(configRoot, '.codex', 'hooks', 'codex_hook_request.mjs'), result.removedFiles);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function deployCursorHook(cursorPath, result) {
|
|
503
|
+
const cursorResult = readJsonFile(cursorPath, {});
|
|
504
|
+
const nextCursor = ensureCursorHook(cursorResult.data);
|
|
505
|
+
const changed = writeJsonFile(cursorPath, nextCursor);
|
|
506
|
+
recordWriteResult(cursorPath, cursorResult.exists, changed, result);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function deployClaudeSettings(claudeSettingsPath, result) {
|
|
510
|
+
const claudeSettingsResult = readJsonFile(claudeSettingsPath, {});
|
|
511
|
+
const nextClaudeSettings = ensureClaudeSettings(claudeSettingsResult.data);
|
|
512
|
+
const changed = writeJsonFile(claudeSettingsPath, nextClaudeSettings);
|
|
513
|
+
recordWriteResult(claudeSettingsPath, claudeSettingsResult.exists, changed, result);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function cleanupClaudeLocalSettings(localCandidates, result) {
|
|
517
|
+
for (const localPath of localCandidates) {
|
|
518
|
+
if (!fs.existsSync(localPath)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const localResult = readJsonFile(localPath, {});
|
|
523
|
+
const nextLocal = removeClaudeHookFromConfig(localResult.data);
|
|
524
|
+
if (writeJsonFile(localPath, nextLocal)) {
|
|
525
|
+
result.updatedFiles.push(localPath);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 将系统级 Codex requirements.toml 的权限错误转换成面向用户的部署提示。
|
|
531
|
+
function wrapCodexPermissionError(error, codexRequirementsPath) {
|
|
532
|
+
const code = isPlainObject(error) ? error.code : '';
|
|
533
|
+
if (code !== 'EACCES' && code !== 'EPERM') {
|
|
534
|
+
return error;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const wrapped = new Error(
|
|
538
|
+
`Codex 部署需要管理员/root 权限,无法写入 ${codexRequirementsPath}。请以管理员身份重新运行 hooks deploy --scope user。`,
|
|
539
|
+
);
|
|
540
|
+
wrapped.cause = error;
|
|
541
|
+
wrapped.code = code;
|
|
542
|
+
return wrapped;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function deployCodexUserHooks(codexRequirementsPath, result) {
|
|
546
|
+
try {
|
|
547
|
+
const reqResult = ensureCodexRequirementsToml(codexRequirementsPath);
|
|
548
|
+
recordWriteResult(codexRequirementsPath, reqResult.exists, reqResult.changed, result);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
throw wrapCodexPermissionError(error, codexRequirementsPath);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function skipCodexProjectDeploy(result) {
|
|
555
|
+
result.skipped.push({
|
|
556
|
+
tool: 'codex',
|
|
557
|
+
reason: 'Codex project-level .codex config is ignored unless the project is trusted. Use --scope user to deploy Codex hooks.',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function deployHooks(targetDir = process.cwd(), options = {}) {
|
|
562
|
+
const scope = normalizeDeployScope(options.scope);
|
|
563
|
+
const paths = resolveDeployPaths(targetDir, scope);
|
|
564
|
+
const result = {
|
|
565
|
+
scope,
|
|
566
|
+
targetDir: paths.targetDir,
|
|
567
|
+
configRoot: paths.configRoot,
|
|
568
|
+
removedFiles: [],
|
|
569
|
+
updatedFiles: [],
|
|
570
|
+
createdFiles: [],
|
|
571
|
+
skipped: [],
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
cleanupLegacyHookScripts(paths.configRoot, result, options.tool);
|
|
575
|
+
|
|
576
|
+
if (!options.tool || options.tool === 'cursor') {
|
|
577
|
+
deployCursorHook(paths.cursorHooksPath, result);
|
|
578
|
+
} else {
|
|
579
|
+
result.skipped.push({ tool: 'cursor', reason: 'Skipped by --tool filter' });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!options.tool || options.tool === 'claude') {
|
|
583
|
+
deployClaudeSettings(paths.claudeSettingsPath, result);
|
|
584
|
+
} else {
|
|
585
|
+
result.skipped.push({ tool: 'claude', reason: 'Skipped by --tool filter' });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (options.tool && options.tool !== 'codex') {
|
|
589
|
+
result.skipped.push({ tool: 'codex', reason: 'Skipped by --tool filter' });
|
|
590
|
+
} else if (scope === 'user') {
|
|
591
|
+
deployCodexUserHooks(paths.codexRequirementsPath, result);
|
|
592
|
+
} else {
|
|
593
|
+
skipCodexProjectDeploy(result);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!options.tool || options.tool === 'claude') {
|
|
597
|
+
cleanupClaudeLocalSettings(paths.claudeLocalCandidates, result);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
module.exports = {
|
|
604
|
+
deployHooks,
|
|
605
|
+
};
|