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
package/dist/utils/config.js
CHANGED
|
@@ -11,10 +11,10 @@ exports.validateConfig = validateConfig;
|
|
|
11
11
|
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
12
|
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
13
13
|
exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
|
|
14
|
+
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
14
15
|
exports.resolvePresetPath = resolvePresetPath;
|
|
15
16
|
exports.loadPreset = loadPreset;
|
|
16
17
|
exports.loadAllRules = loadAllRules;
|
|
17
|
-
exports.applyOverrides = applyOverrides;
|
|
18
18
|
exports.loadConfigFile = loadConfigFile;
|
|
19
19
|
exports.loadConfig = loadConfig;
|
|
20
20
|
exports.saveConfig = saveConfig;
|
|
@@ -22,12 +22,11 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
const cosmiconfig_1 = require("cosmiconfig");
|
|
24
24
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
25
|
+
const hooks_1 = require("./hooks");
|
|
25
26
|
exports.ALLOWED_CONFIG_KEYS = [
|
|
26
|
-
"
|
|
27
|
-
"commandsDir",
|
|
27
|
+
"rootDir",
|
|
28
28
|
"targets",
|
|
29
29
|
"presets",
|
|
30
|
-
"overrides",
|
|
31
30
|
"mcpServers",
|
|
32
31
|
"workspaces",
|
|
33
32
|
"skipInstall",
|
|
@@ -63,11 +62,9 @@ function resolveWorkspaces(config, configFilePath, cwd) {
|
|
|
63
62
|
}
|
|
64
63
|
function applyDefaults(config, workspaces) {
|
|
65
64
|
return {
|
|
66
|
-
|
|
67
|
-
commandsDir: config.commandsDir,
|
|
65
|
+
rootDir: config.rootDir,
|
|
68
66
|
targets: config.targets || ["cursor"],
|
|
69
67
|
presets: config.presets || [],
|
|
70
|
-
overrides: config.overrides || {},
|
|
71
68
|
mcpServers: config.mcpServers || {},
|
|
72
69
|
workspaces,
|
|
73
70
|
skipInstall: config.skipInstall || false,
|
|
@@ -81,36 +78,37 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
81
78
|
if (unknownKeys.length > 0) {
|
|
82
79
|
throw new Error(`Invalid configuration at ${configFilePath}: unknown keys: ${unknownKeys.join(", ")}`);
|
|
83
80
|
}
|
|
84
|
-
// Validate
|
|
85
|
-
const
|
|
86
|
-
const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
|
|
81
|
+
// Validate rootDir
|
|
82
|
+
const hasRootDir = "rootDir" in config && typeof config.rootDir === "string";
|
|
87
83
|
const hasPresets = "presets" in config &&
|
|
88
84
|
Array.isArray(config.presets) &&
|
|
89
85
|
config.presets.length > 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
// Validate rulesDir if provided
|
|
96
|
-
if (hasRulesDir) {
|
|
97
|
-
const rulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
|
|
98
|
-
if (!fs_extra_1.default.existsSync(rulesPath)) {
|
|
99
|
-
throw new Error(`Rules directory does not exist: ${rulesPath}`);
|
|
100
|
-
}
|
|
101
|
-
if (!fs_extra_1.default.statSync(rulesPath).isDirectory()) {
|
|
102
|
-
throw new Error(`Rules path is not a directory: ${rulesPath}`);
|
|
86
|
+
if (hasRootDir) {
|
|
87
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
88
|
+
if (!fs_extra_1.default.existsSync(rootPath)) {
|
|
89
|
+
throw new Error(`Root directory does not exist: ${rootPath}`);
|
|
103
90
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const commandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
|
|
107
|
-
if (!fs_extra_1.default.existsSync(commandsPath)) {
|
|
108
|
-
throw new Error(`Commands directory does not exist: ${commandsPath}`);
|
|
91
|
+
if (!fs_extra_1.default.statSync(rootPath).isDirectory()) {
|
|
92
|
+
throw new Error(`Root path is not a directory: ${rootPath}`);
|
|
109
93
|
}
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
// Check for at least one valid subdirectory or file
|
|
95
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
96
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
97
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
|
|
98
|
+
// In workspace mode, root config doesn't need these directories
|
|
99
|
+
// since packages will have their own configurations
|
|
100
|
+
if (!isWorkspaceMode &&
|
|
101
|
+
!hasRules &&
|
|
102
|
+
!hasCommands &&
|
|
103
|
+
!hasHooks &&
|
|
104
|
+
!hasPresets) {
|
|
105
|
+
throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
|
|
112
106
|
}
|
|
113
107
|
}
|
|
108
|
+
else if (!isWorkspaceMode && !hasPresets) {
|
|
109
|
+
// If no rootDir specified and not in workspace mode, must have presets
|
|
110
|
+
throw new Error(`At least one of rootDir or presets must be specified in config at ${configFilePath}`);
|
|
111
|
+
}
|
|
114
112
|
if ("targets" in config) {
|
|
115
113
|
if (!Array.isArray(config.targets)) {
|
|
116
114
|
throw new Error(`targets must be an array in config at ${configFilePath}`);
|
|
@@ -124,22 +122,21 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
|
-
// Validate override rule names will be checked after rule resolution
|
|
128
125
|
}
|
|
129
|
-
async function loadRulesFromDirectory(
|
|
126
|
+
async function loadRulesFromDirectory(directoryPath, source, presetName) {
|
|
130
127
|
const rules = [];
|
|
131
|
-
if (!fs_extra_1.default.existsSync(
|
|
128
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
132
129
|
return rules;
|
|
133
130
|
}
|
|
134
|
-
const pattern = node_path_1.default.join(
|
|
131
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.mdc").replace(/\\/g, "/");
|
|
135
132
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
136
133
|
onlyFiles: true,
|
|
137
134
|
absolute: true,
|
|
138
135
|
});
|
|
139
136
|
for (const filePath of filePaths) {
|
|
140
137
|
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
141
|
-
// Preserve directory structure by using relative path from
|
|
142
|
-
const relativePath = node_path_1.default.relative(
|
|
138
|
+
// Preserve directory structure by using relative path from source directory
|
|
139
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
143
140
|
const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/");
|
|
144
141
|
rules.push({
|
|
145
142
|
name: ruleName,
|
|
@@ -151,12 +148,12 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
|
|
|
151
148
|
}
|
|
152
149
|
return rules;
|
|
153
150
|
}
|
|
154
|
-
async function loadCommandsFromDirectory(
|
|
151
|
+
async function loadCommandsFromDirectory(directoryPath, source, presetName) {
|
|
155
152
|
const commands = [];
|
|
156
|
-
if (!fs_extra_1.default.existsSync(
|
|
153
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
157
154
|
return commands;
|
|
158
155
|
}
|
|
159
|
-
const pattern = node_path_1.default.join(
|
|
156
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
|
|
160
157
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
161
158
|
onlyFiles: true,
|
|
162
159
|
absolute: true,
|
|
@@ -164,7 +161,7 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
|
|
|
164
161
|
filePaths.sort();
|
|
165
162
|
for (const filePath of filePaths) {
|
|
166
163
|
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
167
|
-
const relativePath = node_path_1.default.relative(
|
|
164
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
168
165
|
const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
169
166
|
commands.push({
|
|
170
167
|
name: commandName,
|
|
@@ -176,13 +173,13 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
|
|
|
176
173
|
}
|
|
177
174
|
return commands;
|
|
178
175
|
}
|
|
179
|
-
async function loadAssetsFromDirectory(
|
|
176
|
+
async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
180
177
|
const assets = [];
|
|
181
|
-
if (!fs_extra_1.default.existsSync(
|
|
178
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
182
179
|
return assets;
|
|
183
180
|
}
|
|
184
181
|
// Find all files except .mdc files and hidden files
|
|
185
|
-
const pattern = node_path_1.default.join(
|
|
182
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*").replace(/\\/g, "/");
|
|
186
183
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
187
184
|
onlyFiles: true,
|
|
188
185
|
absolute: true,
|
|
@@ -190,8 +187,8 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
|
|
|
190
187
|
});
|
|
191
188
|
for (const filePath of filePaths) {
|
|
192
189
|
const content = await fs_extra_1.default.readFile(filePath);
|
|
193
|
-
// Preserve directory structure by using relative path from
|
|
194
|
-
const relativePath = node_path_1.default.relative(
|
|
190
|
+
// Preserve directory structure by using relative path from source directory
|
|
191
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
195
192
|
// Keep extension for assets
|
|
196
193
|
const assetName = relativePath.replace(/\\/g, "/");
|
|
197
194
|
assets.push({
|
|
@@ -204,6 +201,19 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
|
|
|
204
201
|
}
|
|
205
202
|
return assets;
|
|
206
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract namespace from preset path for directory structure
|
|
206
|
+
* Handles both npm packages and local paths consistently
|
|
207
|
+
*/
|
|
208
|
+
function extractNamespaceFromPresetPath(presetPath) {
|
|
209
|
+
// Special case: npm package names always use forward slashes, regardless of platform
|
|
210
|
+
if (presetPath.startsWith("@")) {
|
|
211
|
+
// For scoped packages like @scope/package/subdir, create nested directories
|
|
212
|
+
return presetPath.split("/");
|
|
213
|
+
}
|
|
214
|
+
const parts = presetPath.split(node_path_1.default.sep);
|
|
215
|
+
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
216
|
+
}
|
|
207
217
|
function resolvePresetPath(presetPath, cwd) {
|
|
208
218
|
// Support specifying aicm.json directory and load the config from it
|
|
209
219
|
if (!presetPath.endsWith(".json")) {
|
|
@@ -241,99 +251,103 @@ async function loadPreset(presetPath, cwd) {
|
|
|
241
251
|
throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
242
252
|
}
|
|
243
253
|
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!
|
|
251
|
-
throw new Error(`Preset "${presetPath}" must have
|
|
254
|
+
const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
|
|
255
|
+
// Check for at least one valid subdirectory
|
|
256
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
|
|
257
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
258
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
259
|
+
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
260
|
+
if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
|
|
261
|
+
throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, hooks.json, or assets/`);
|
|
252
262
|
}
|
|
253
263
|
return {
|
|
254
264
|
config: presetConfig,
|
|
255
|
-
|
|
256
|
-
commandsDir: presetCommandsDir,
|
|
265
|
+
rootDir: presetRootDir,
|
|
257
266
|
};
|
|
258
267
|
}
|
|
259
268
|
async function loadAllRules(config, cwd) {
|
|
260
269
|
const allRules = [];
|
|
261
270
|
const allCommands = [];
|
|
262
271
|
const allAssets = [];
|
|
272
|
+
const allHookFiles = [];
|
|
273
|
+
const allHooksConfigs = [];
|
|
263
274
|
let mergedMcpServers = { ...config.mcpServers };
|
|
264
|
-
// Load local
|
|
265
|
-
if (config.
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
275
|
+
// Load local files from rootDir only if specified
|
|
276
|
+
if (config.rootDir) {
|
|
277
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
278
|
+
// Load rules from rules/ subdirectory
|
|
279
|
+
const rulesPath = node_path_1.default.join(rootPath, "rules");
|
|
280
|
+
if (fs_extra_1.default.existsSync(rulesPath)) {
|
|
281
|
+
const localRules = await loadRulesFromDirectory(rulesPath, "local");
|
|
282
|
+
allRules.push(...localRules);
|
|
283
|
+
}
|
|
284
|
+
// Load commands from commands/ subdirectory
|
|
285
|
+
const commandsPath = node_path_1.default.join(rootPath, "commands");
|
|
286
|
+
if (fs_extra_1.default.existsSync(commandsPath)) {
|
|
287
|
+
const localCommands = await loadCommandsFromDirectory(commandsPath, "local");
|
|
288
|
+
allCommands.push(...localCommands);
|
|
289
|
+
}
|
|
290
|
+
// Load hooks from hooks.json (sibling to hooks/ directory)
|
|
291
|
+
const hooksFilePath = node_path_1.default.join(rootPath, "hooks.json");
|
|
292
|
+
if (fs_extra_1.default.existsSync(hooksFilePath)) {
|
|
293
|
+
const { config: localHooksConfig, files: localHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(rootPath, "local");
|
|
294
|
+
allHooksConfigs.push(localHooksConfig);
|
|
295
|
+
allHookFiles.push(...localHookFiles);
|
|
296
|
+
}
|
|
297
|
+
// Load assets from assets/ subdirectory
|
|
298
|
+
const assetsPath = node_path_1.default.join(rootPath, "assets");
|
|
299
|
+
if (fs_extra_1.default.existsSync(assetsPath)) {
|
|
300
|
+
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
301
|
+
allAssets.push(...localAssets);
|
|
302
|
+
}
|
|
276
303
|
}
|
|
304
|
+
// Load presets
|
|
277
305
|
if (config.presets) {
|
|
278
306
|
for (const presetPath of config.presets) {
|
|
279
307
|
const preset = await loadPreset(presetPath, cwd);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
308
|
+
const presetRootDir = preset.rootDir;
|
|
309
|
+
// Load preset rules from rules/ subdirectory
|
|
310
|
+
const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
|
|
311
|
+
if (fs_extra_1.default.existsSync(presetRulesPath)) {
|
|
312
|
+
const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
|
|
283
313
|
allRules.push(...presetRules);
|
|
284
|
-
allAssets.push(...presetAssets);
|
|
285
314
|
}
|
|
286
|
-
|
|
287
|
-
|
|
315
|
+
// Load preset commands from commands/ subdirectory
|
|
316
|
+
const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
|
|
317
|
+
if (fs_extra_1.default.existsSync(presetCommandsPath)) {
|
|
318
|
+
const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
|
|
288
319
|
allCommands.push(...presetCommands);
|
|
289
320
|
}
|
|
321
|
+
// Load preset hooks from hooks.json (sibling to hooks/ directory)
|
|
322
|
+
const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
|
|
323
|
+
if (fs_extra_1.default.existsSync(presetHooksFile)) {
|
|
324
|
+
const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
|
|
325
|
+
allHooksConfigs.push(presetHooksConfig);
|
|
326
|
+
allHookFiles.push(...presetHookFiles);
|
|
327
|
+
}
|
|
328
|
+
// Load preset assets from assets/ subdirectory
|
|
329
|
+
const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
|
|
330
|
+
if (fs_extra_1.default.existsSync(presetAssetsPath)) {
|
|
331
|
+
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
332
|
+
allAssets.push(...presetAssets);
|
|
333
|
+
}
|
|
290
334
|
// Merge MCP servers from preset
|
|
291
335
|
if (preset.config.mcpServers) {
|
|
292
336
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
293
337
|
}
|
|
294
338
|
}
|
|
295
339
|
}
|
|
340
|
+
// Merge all hooks configurations
|
|
341
|
+
const mergedHooks = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
|
|
296
342
|
return {
|
|
297
343
|
rules: allRules,
|
|
298
344
|
commands: allCommands,
|
|
299
345
|
assets: allAssets,
|
|
300
346
|
mcpServers: mergedMcpServers,
|
|
347
|
+
hooks: mergedHooks,
|
|
348
|
+
hookFiles: allHookFiles,
|
|
301
349
|
};
|
|
302
350
|
}
|
|
303
|
-
function applyOverrides(files, overrides, cwd) {
|
|
304
|
-
// Validate that all override names exist in the resolved files
|
|
305
|
-
for (const name of Object.keys(overrides)) {
|
|
306
|
-
if (!files.some((file) => file.name === name)) {
|
|
307
|
-
throw new Error(`Override entry "${name}" does not exist in resolved files`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
const fileMap = new Map();
|
|
311
|
-
for (const file of files) {
|
|
312
|
-
fileMap.set(file.name, file);
|
|
313
|
-
}
|
|
314
|
-
for (const [name, override] of Object.entries(overrides)) {
|
|
315
|
-
if (override === false) {
|
|
316
|
-
fileMap.delete(name);
|
|
317
|
-
}
|
|
318
|
-
else if (typeof override === "string") {
|
|
319
|
-
const overridePath = node_path_1.default.resolve(cwd, override);
|
|
320
|
-
if (!fs_extra_1.default.existsSync(overridePath)) {
|
|
321
|
-
throw new Error(`Override file not found: ${override} in ${cwd}`);
|
|
322
|
-
}
|
|
323
|
-
const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
|
|
324
|
-
const existing = fileMap.get(name);
|
|
325
|
-
fileMap.set(name, {
|
|
326
|
-
...(existing !== null && existing !== void 0 ? existing : {}),
|
|
327
|
-
name,
|
|
328
|
-
content,
|
|
329
|
-
sourcePath: overridePath,
|
|
330
|
-
source: "local",
|
|
331
|
-
presetName: undefined,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return Array.from(fileMap.values());
|
|
336
|
-
}
|
|
337
351
|
/**
|
|
338
352
|
* Merge preset MCP servers with local config MCP servers
|
|
339
353
|
* Local config takes precedence over preset config
|
|
@@ -347,7 +361,7 @@ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
|
|
|
347
361
|
delete newMcpServers[serverName];
|
|
348
362
|
continue;
|
|
349
363
|
}
|
|
350
|
-
// Only add if not already defined in config (
|
|
364
|
+
// Only add if not already defined in config (local config takes precedence)
|
|
351
365
|
if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
|
|
352
366
|
newMcpServers[serverName] = serverConfig;
|
|
353
367
|
}
|
|
@@ -376,34 +390,15 @@ async function loadConfig(cwd) {
|
|
|
376
390
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
377
391
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
378
392
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
379
|
-
const { rules, commands, assets, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
380
|
-
let rulesWithOverrides = rules;
|
|
381
|
-
let commandsWithOverrides = commands;
|
|
382
|
-
// Note: Assets are not currently supported in overrides as they are binary/varied files
|
|
383
|
-
if (configWithDefaults.overrides) {
|
|
384
|
-
const overrides = configWithDefaults.overrides;
|
|
385
|
-
const ruleNames = new Set(rules.map((rule) => rule.name));
|
|
386
|
-
const commandNames = new Set(commands.map((command) => command.name));
|
|
387
|
-
for (const overrideName of Object.keys(overrides)) {
|
|
388
|
-
if (!ruleNames.has(overrideName) && !commandNames.has(overrideName)) {
|
|
389
|
-
throw new Error(`Override entry "${overrideName}" does not exist in resolved rules or commands`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const ruleOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => ruleNames.has(name)));
|
|
393
|
-
const commandOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => commandNames.has(name)));
|
|
394
|
-
if (Object.keys(ruleOverrides).length > 0) {
|
|
395
|
-
rulesWithOverrides = applyOverrides(rules, ruleOverrides, workingDir);
|
|
396
|
-
}
|
|
397
|
-
if (Object.keys(commandOverrides).length > 0) {
|
|
398
|
-
commandsWithOverrides = applyOverrides(commands, commandOverrides, workingDir);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
393
|
+
const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
401
394
|
return {
|
|
402
395
|
config: configWithDefaults,
|
|
403
|
-
rules
|
|
404
|
-
commands
|
|
396
|
+
rules,
|
|
397
|
+
commands,
|
|
405
398
|
assets,
|
|
406
399
|
mcpServers,
|
|
400
|
+
hooks,
|
|
401
|
+
hookFiles,
|
|
407
402
|
};
|
|
408
403
|
}
|
|
409
404
|
function saveConfig(config, cwd) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type HookType = "beforeShellExecution" | "beforeMCPExecution" | "afterShellExecution" | "afterMCPExecution" | "beforeReadFile" | "afterFileEdit" | "beforeSubmitPrompt" | "stop";
|
|
2
|
+
export interface HookCommand {
|
|
3
|
+
command: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HooksJson {
|
|
6
|
+
version: number;
|
|
7
|
+
hooks: {
|
|
8
|
+
[K in HookType]?: HookCommand[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface HookFile {
|
|
12
|
+
name: string;
|
|
13
|
+
basename: string;
|
|
14
|
+
content: Buffer;
|
|
15
|
+
sourcePath: string;
|
|
16
|
+
source: "local" | "preset";
|
|
17
|
+
presetName?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load hooks configuration from a root directory
|
|
21
|
+
* The directory should contain hooks.json (sibling to hooks/ directory)
|
|
22
|
+
* All files in the hooks/ subdirectory are copied during installation
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadHooksFromDirectory(rootDir: string, source: "local" | "preset", presetName?: string): Promise<{
|
|
25
|
+
config: HooksJson;
|
|
26
|
+
files: HookFile[];
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Merge multiple hooks configurations into one
|
|
30
|
+
* Later configurations override earlier ones for the same hook type
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeHooksConfigs(configs: HooksJson[]): HooksJson;
|
|
33
|
+
/**
|
|
34
|
+
* Rewrite command paths to point to the managed hooks directory (hooks/aicm/)
|
|
35
|
+
* At this point, paths are already namespaced filenames from loadHooksFromDirectory
|
|
36
|
+
*/
|
|
37
|
+
export declare function rewriteHooksConfigToManagedDir(hooksConfig: HooksJson): HooksJson;
|
|
38
|
+
/**
|
|
39
|
+
* Count the number of hook entries in a hooks configuration
|
|
40
|
+
*/
|
|
41
|
+
export declare function countHooks(hooksConfig: HooksJson): number;
|
|
42
|
+
/**
|
|
43
|
+
* Dedupe hook files by namespaced path, warn on content conflicts
|
|
44
|
+
* Presets are namespaced with directories, so same basename from different presets won't collide
|
|
45
|
+
*/
|
|
46
|
+
export declare function dedupeHookFiles(hookFiles: HookFile[]): HookFile[];
|
|
47
|
+
/**
|
|
48
|
+
* Write hooks configuration and files to Cursor target
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeHooksToCursor(hooksConfig: HooksJson, hookFiles: HookFile[], cwd: string): void;
|