aicm 0.18.0 → 0.19.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 +222 -65
- package/dist/api.d.ts +1 -0
- package/dist/commands/clean.js +61 -0
- package/dist/commands/init.js +23 -3
- package/dist/commands/install-workspaces.js +45 -136
- package/dist/commands/install.d.ts +5 -2
- package/dist/commands/install.js +62 -53
- package/dist/utils/config.d.ts +17 -13
- package/dist/utils/config.js +127 -132
- package/dist/utils/hooks.d.ts +50 -0
- package/dist/utils/hooks.js +346 -0
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadHooksFromDirectory = loadHooksFromDirectory;
|
|
7
|
+
exports.mergeHooksConfigs = mergeHooksConfigs;
|
|
8
|
+
exports.rewriteHooksConfigToManagedDir = rewriteHooksConfigToManagedDir;
|
|
9
|
+
exports.countHooks = countHooks;
|
|
10
|
+
exports.dedupeHookFiles = dedupeHookFiles;
|
|
11
|
+
exports.writeHooksToCursor = writeHooksToCursor;
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
/**
|
|
17
|
+
* Validate that a command path points to a file within the hooks directory
|
|
18
|
+
* Commands should be relative paths starting with ./hooks/
|
|
19
|
+
*/
|
|
20
|
+
function validateHookPath(commandPath, rootDir, hooksDir) {
|
|
21
|
+
if (!commandPath.startsWith("./") && !commandPath.startsWith("../")) {
|
|
22
|
+
return { valid: false };
|
|
23
|
+
}
|
|
24
|
+
// Resolve path relative to rootDir (where hooks.json is located)
|
|
25
|
+
const resolvedPath = node_path_1.default.resolve(rootDir, commandPath);
|
|
26
|
+
const relativePath = node_path_1.default.relative(hooksDir, resolvedPath);
|
|
27
|
+
// Check if the file is within hooks directory (not using .. to escape)
|
|
28
|
+
if (relativePath.startsWith("..") || node_path_1.default.isAbsolute(relativePath)) {
|
|
29
|
+
return { valid: false };
|
|
30
|
+
}
|
|
31
|
+
return { valid: true, relativePath };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load all files from the hooks directory
|
|
35
|
+
*/
|
|
36
|
+
async function loadAllFilesFromDirectory(dir, baseDir = dir) {
|
|
37
|
+
const files = [];
|
|
38
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
// Recursively load files from subdirectories
|
|
43
|
+
const subFiles = await loadAllFilesFromDirectory(fullPath, baseDir);
|
|
44
|
+
files.push(...subFiles);
|
|
45
|
+
}
|
|
46
|
+
else if (entry.isFile() && entry.name !== "hooks.json") {
|
|
47
|
+
// Skip hooks.json, collect all other files
|
|
48
|
+
const relativePath = node_path_1.default.relative(baseDir, fullPath);
|
|
49
|
+
files.push({ relativePath, absolutePath: fullPath });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Load hooks configuration from a root directory
|
|
56
|
+
* The directory should contain hooks.json (sibling to hooks/ directory)
|
|
57
|
+
* All files in the hooks/ subdirectory are copied during installation
|
|
58
|
+
*/
|
|
59
|
+
async function loadHooksFromDirectory(rootDir, source, presetName) {
|
|
60
|
+
const hooksFilePath = node_path_1.default.join(rootDir, "hooks.json");
|
|
61
|
+
const hooksDir = node_path_1.default.join(rootDir, "hooks");
|
|
62
|
+
if (!fs_extra_1.default.existsSync(hooksFilePath)) {
|
|
63
|
+
return {
|
|
64
|
+
config: { version: 1, hooks: {} },
|
|
65
|
+
files: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const content = await fs_extra_1.default.readFile(hooksFilePath, "utf8");
|
|
69
|
+
const hooksConfig = JSON.parse(content);
|
|
70
|
+
// Load all files from hooks/ subdirectory
|
|
71
|
+
const hookFiles = [];
|
|
72
|
+
if (fs_extra_1.default.existsSync(hooksDir)) {
|
|
73
|
+
const allFiles = await loadAllFilesFromDirectory(hooksDir);
|
|
74
|
+
// Create a map of all files for validation
|
|
75
|
+
const filePathMap = new Map();
|
|
76
|
+
for (const file of allFiles) {
|
|
77
|
+
filePathMap.set(file.relativePath, file.absolutePath);
|
|
78
|
+
}
|
|
79
|
+
// Validate that all referenced commands point to files within hooks directory
|
|
80
|
+
if (hooksConfig.hooks) {
|
|
81
|
+
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
82
|
+
const hookCommands = hooksConfig.hooks[hookType];
|
|
83
|
+
if (hookCommands && Array.isArray(hookCommands)) {
|
|
84
|
+
for (const hookCommand of hookCommands) {
|
|
85
|
+
const commandPath = hookCommand.command;
|
|
86
|
+
if (commandPath && typeof commandPath === "string") {
|
|
87
|
+
const validation = validateHookPath(commandPath, rootDir, hooksDir);
|
|
88
|
+
if (!validation.valid || !validation.relativePath) {
|
|
89
|
+
console.warn(`Warning: Hook command "${commandPath}" in hooks.json must reference a file within the hooks/ directory. Skipping.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Copy all files from the hooks/ directory
|
|
97
|
+
for (const file of allFiles) {
|
|
98
|
+
const fileContent = await fs_extra_1.default.readFile(file.absolutePath);
|
|
99
|
+
const basename = node_path_1.default.basename(file.absolutePath);
|
|
100
|
+
// Namespace preset files
|
|
101
|
+
let namespacedPath;
|
|
102
|
+
if (source === "preset" && presetName) {
|
|
103
|
+
const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
|
|
104
|
+
// Use posix paths for consistent cross-platform behavior
|
|
105
|
+
namespacedPath = node_path_1.default.posix.join(...namespace, file.relativePath.split(node_path_1.default.sep).join(node_path_1.default.posix.sep));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// For local files, use the relative path as-is
|
|
109
|
+
namespacedPath = file.relativePath.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
|
|
110
|
+
}
|
|
111
|
+
hookFiles.push({
|
|
112
|
+
name: namespacedPath,
|
|
113
|
+
basename,
|
|
114
|
+
content: fileContent,
|
|
115
|
+
sourcePath: file.absolutePath,
|
|
116
|
+
source,
|
|
117
|
+
presetName,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Rewrite the config to use namespaced file names
|
|
122
|
+
const rewrittenConfig = rewriteHooksConfigForNamespace(hooksConfig, hookFiles, rootDir);
|
|
123
|
+
return { config: rewrittenConfig, files: hookFiles };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Rewrite hooks config to use the namespaced names from the hook files
|
|
127
|
+
*/
|
|
128
|
+
function rewriteHooksConfigForNamespace(hooksConfig, hookFiles, rootDir) {
|
|
129
|
+
// Create a map from sourcePath to the hookFile
|
|
130
|
+
const sourcePathToFile = new Map();
|
|
131
|
+
for (const hookFile of hookFiles) {
|
|
132
|
+
sourcePathToFile.set(hookFile.sourcePath, hookFile);
|
|
133
|
+
}
|
|
134
|
+
const rewritten = {
|
|
135
|
+
version: hooksConfig.version,
|
|
136
|
+
hooks: {},
|
|
137
|
+
};
|
|
138
|
+
if (hooksConfig.hooks) {
|
|
139
|
+
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
140
|
+
const hookCommands = hooksConfig.hooks[hookType];
|
|
141
|
+
if (hookCommands && Array.isArray(hookCommands)) {
|
|
142
|
+
rewritten.hooks[hookType] = hookCommands
|
|
143
|
+
.map((hookCommand) => {
|
|
144
|
+
const commandPath = hookCommand.command;
|
|
145
|
+
if (commandPath &&
|
|
146
|
+
typeof commandPath === "string" &&
|
|
147
|
+
(commandPath.startsWith("./") || commandPath.startsWith("../"))) {
|
|
148
|
+
// Resolve path relative to rootDir (where hooks.json is)
|
|
149
|
+
const resolvedPath = node_path_1.default.resolve(rootDir, commandPath);
|
|
150
|
+
const hookFile = sourcePathToFile.get(resolvedPath);
|
|
151
|
+
if (hookFile) {
|
|
152
|
+
// Use the namespaced name
|
|
153
|
+
return { command: hookFile.name };
|
|
154
|
+
}
|
|
155
|
+
// File was invalid or not found, filter it out
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return hookCommand;
|
|
159
|
+
})
|
|
160
|
+
.filter((cmd) => cmd !== null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return rewritten;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Merge multiple hooks configurations into one
|
|
168
|
+
* Later configurations override earlier ones for the same hook type
|
|
169
|
+
*/
|
|
170
|
+
function mergeHooksConfigs(configs) {
|
|
171
|
+
const merged = {
|
|
172
|
+
version: 1,
|
|
173
|
+
hooks: {},
|
|
174
|
+
};
|
|
175
|
+
for (const config of configs) {
|
|
176
|
+
// Use the latest version
|
|
177
|
+
if (config.version) {
|
|
178
|
+
merged.version = config.version;
|
|
179
|
+
}
|
|
180
|
+
// Merge hooks - concatenate arrays for each hook type
|
|
181
|
+
if (config.hooks) {
|
|
182
|
+
for (const hookType of Object.keys(config.hooks)) {
|
|
183
|
+
const hookCommands = config.hooks[hookType];
|
|
184
|
+
if (hookCommands && Array.isArray(hookCommands)) {
|
|
185
|
+
if (!merged.hooks[hookType]) {
|
|
186
|
+
merged.hooks[hookType] = [];
|
|
187
|
+
}
|
|
188
|
+
// Concatenate commands (later configs add to the list)
|
|
189
|
+
merged.hooks[hookType] = [
|
|
190
|
+
...(merged.hooks[hookType] || []),
|
|
191
|
+
...hookCommands,
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return merged;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Rewrite command paths to point to the managed hooks directory (hooks/aicm/)
|
|
201
|
+
* At this point, paths are already namespaced filenames from loadHooksFromDirectory
|
|
202
|
+
*/
|
|
203
|
+
function rewriteHooksConfigToManagedDir(hooksConfig) {
|
|
204
|
+
const rewritten = {
|
|
205
|
+
version: hooksConfig.version,
|
|
206
|
+
hooks: {},
|
|
207
|
+
};
|
|
208
|
+
if (hooksConfig.hooks) {
|
|
209
|
+
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
210
|
+
const hookCommands = hooksConfig.hooks[hookType];
|
|
211
|
+
if (hookCommands && Array.isArray(hookCommands)) {
|
|
212
|
+
rewritten.hooks[hookType] = hookCommands.map((hookCommand) => {
|
|
213
|
+
const commandPath = hookCommand.command;
|
|
214
|
+
if (commandPath && typeof commandPath === "string") {
|
|
215
|
+
return { command: `./hooks/aicm/${commandPath}` };
|
|
216
|
+
}
|
|
217
|
+
return hookCommand;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return rewritten;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Count the number of hook entries in a hooks configuration
|
|
226
|
+
*/
|
|
227
|
+
function countHooks(hooksConfig) {
|
|
228
|
+
let count = 0;
|
|
229
|
+
if (hooksConfig.hooks) {
|
|
230
|
+
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
231
|
+
const hookCommands = hooksConfig.hooks[hookType];
|
|
232
|
+
if (hookCommands && Array.isArray(hookCommands)) {
|
|
233
|
+
count += hookCommands.length;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return count;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Dedupe hook files by namespaced path, warn on content conflicts
|
|
241
|
+
* Presets are namespaced with directories, so same basename from different presets won't collide
|
|
242
|
+
*/
|
|
243
|
+
function dedupeHookFiles(hookFiles) {
|
|
244
|
+
const fileMap = new Map();
|
|
245
|
+
for (const hookFile of hookFiles) {
|
|
246
|
+
const namespacedPath = hookFile.name;
|
|
247
|
+
if (fileMap.has(namespacedPath)) {
|
|
248
|
+
const existing = fileMap.get(namespacedPath);
|
|
249
|
+
const existingHash = node_crypto_1.default
|
|
250
|
+
.createHash("md5")
|
|
251
|
+
.update(existing.content)
|
|
252
|
+
.digest("hex");
|
|
253
|
+
const currentHash = node_crypto_1.default
|
|
254
|
+
.createHash("md5")
|
|
255
|
+
.update(hookFile.content)
|
|
256
|
+
.digest("hex");
|
|
257
|
+
if (existingHash !== currentHash) {
|
|
258
|
+
const sourceInfo = hookFile.presetName
|
|
259
|
+
? `preset "${hookFile.presetName}"`
|
|
260
|
+
: hookFile.source;
|
|
261
|
+
const existingSourceInfo = existing.presetName
|
|
262
|
+
? `preset "${existing.presetName}"`
|
|
263
|
+
: existing.source;
|
|
264
|
+
console.warn(`Warning: Hook file "${namespacedPath}" has different content from ${existingSourceInfo} and ${sourceInfo}. Using last occurrence.`);
|
|
265
|
+
}
|
|
266
|
+
// Last writer wins
|
|
267
|
+
fileMap.set(namespacedPath, hookFile);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
fileMap.set(namespacedPath, hookFile);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Array.from(fileMap.values());
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Write hooks configuration and files to Cursor target
|
|
277
|
+
*/
|
|
278
|
+
function writeHooksToCursor(hooksConfig, hookFiles, cwd) {
|
|
279
|
+
const cursorRoot = node_path_1.default.join(cwd, ".cursor");
|
|
280
|
+
const hooksJsonPath = node_path_1.default.join(cursorRoot, "hooks.json");
|
|
281
|
+
const hooksDir = node_path_1.default.join(cursorRoot, "hooks", "aicm");
|
|
282
|
+
// Dedupe hook files
|
|
283
|
+
const dedupedHookFiles = dedupeHookFiles(hookFiles);
|
|
284
|
+
// Create hooks directory and clean it
|
|
285
|
+
fs_extra_1.default.emptyDirSync(hooksDir);
|
|
286
|
+
// Copy hook files to managed directory
|
|
287
|
+
for (const hookFile of dedupedHookFiles) {
|
|
288
|
+
const targetPath = node_path_1.default.join(hooksDir, hookFile.name);
|
|
289
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(targetPath));
|
|
290
|
+
fs_extra_1.default.writeFileSync(targetPath, hookFile.content);
|
|
291
|
+
}
|
|
292
|
+
// Rewrite paths to point to managed directory
|
|
293
|
+
const finalConfig = rewriteHooksConfigToManagedDir(hooksConfig);
|
|
294
|
+
// Read existing hooks.json and preserve user hooks
|
|
295
|
+
let existingConfig = null;
|
|
296
|
+
if (fs_extra_1.default.existsSync(hooksJsonPath)) {
|
|
297
|
+
try {
|
|
298
|
+
existingConfig = fs_extra_1.default.readJsonSync(hooksJsonPath);
|
|
299
|
+
}
|
|
300
|
+
catch (_a) {
|
|
301
|
+
existingConfig = null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Extract user hooks (non-aicm managed)
|
|
305
|
+
const userHooks = { version: 1, hooks: {} };
|
|
306
|
+
if (existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.hooks) {
|
|
307
|
+
for (const hookType of Object.keys(existingConfig.hooks)) {
|
|
308
|
+
const commands = existingConfig.hooks[hookType];
|
|
309
|
+
if (commands && Array.isArray(commands)) {
|
|
310
|
+
const userCommands = commands.filter((cmd) => { var _a; return !((_a = cmd.command) === null || _a === void 0 ? void 0 : _a.includes("hooks/aicm/")); });
|
|
311
|
+
if (userCommands.length > 0) {
|
|
312
|
+
userHooks.hooks[hookType] = userCommands;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Merge user hooks with aicm hooks
|
|
318
|
+
const mergedConfig = {
|
|
319
|
+
version: finalConfig.version,
|
|
320
|
+
hooks: {},
|
|
321
|
+
};
|
|
322
|
+
// Add user hooks first
|
|
323
|
+
if (userHooks.hooks) {
|
|
324
|
+
for (const hookType of Object.keys(userHooks.hooks)) {
|
|
325
|
+
const commands = userHooks.hooks[hookType];
|
|
326
|
+
if (commands) {
|
|
327
|
+
mergedConfig.hooks[hookType] = [...commands];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Then add aicm hooks
|
|
332
|
+
if (finalConfig.hooks) {
|
|
333
|
+
for (const hookType of Object.keys(finalConfig.hooks)) {
|
|
334
|
+
const commands = finalConfig.hooks[hookType];
|
|
335
|
+
if (commands) {
|
|
336
|
+
if (!mergedConfig.hooks[hookType]) {
|
|
337
|
+
mergedConfig.hooks[hookType] = [];
|
|
338
|
+
}
|
|
339
|
+
mergedConfig.hooks[hookType].push(...commands);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Write hooks.json
|
|
344
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(hooksJsonPath));
|
|
345
|
+
fs_extra_1.default.writeJsonSync(hooksJsonPath, mergedConfig, { spaces: 2 });
|
|
346
|
+
}
|