aicm 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,17 @@
1
1
  import { CosmiconfigResult } from "cosmiconfig";
2
+ import { HooksJson, HookFile } from "./hooks";
2
3
  export interface RawConfig {
3
- rulesDir?: string;
4
- commandsDir?: string;
4
+ rootDir?: string;
5
5
  targets?: string[];
6
6
  presets?: string[];
7
- overrides?: Record<string, string | false>;
8
7
  mcpServers?: MCPServers;
9
8
  workspaces?: boolean;
10
9
  skipInstall?: boolean;
11
10
  }
12
11
  export interface Config {
13
- rulesDir?: string;
14
- commandsDir?: string;
12
+ rootDir?: string;
15
13
  targets: string[];
16
14
  presets?: string[];
17
- overrides?: Record<string, string | false>;
18
15
  mcpServers?: MCPServers;
19
16
  workspaces?: boolean;
20
17
  skipInstall?: boolean;
@@ -58,30 +55,42 @@ export interface ResolvedConfig {
58
55
  commands: CommandFile[];
59
56
  assets: AssetFile[];
60
57
  mcpServers: MCPServers;
58
+ hooks: HooksJson;
59
+ hookFiles: HookFile[];
61
60
  }
62
- export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "commandsDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
61
+ export declare const ALLOWED_CONFIG_KEYS: readonly ["rootDir", "targets", "presets", "mcpServers", "workspaces", "skipInstall"];
63
62
  export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex", "claude"];
64
63
  export type SupportedTarget = (typeof SUPPORTED_TARGETS)[number];
65
64
  export declare function detectWorkspacesFromPackageJson(cwd: string): boolean;
66
65
  export declare function resolveWorkspaces(config: unknown, configFilePath: string, cwd: string): boolean;
67
66
  export declare function applyDefaults(config: RawConfig, workspaces: boolean): Config;
68
67
  export declare function validateConfig(config: unknown, configFilePath: string, cwd: string, isWorkspaceMode?: boolean): asserts config is Config;
69
- export declare function loadRulesFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
70
- export declare function loadCommandsFromDirectory(commandsDir: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
71
- export declare function loadAssetsFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
68
+ export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
69
+ export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
70
+ export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
71
+ /**
72
+ * Extract namespace from preset path for directory structure
73
+ * Handles both npm packages and local paths consistently
74
+ */
75
+ export declare function extractNamespaceFromPresetPath(presetPath: string): string[];
72
76
  export declare function resolvePresetPath(presetPath: string, cwd: string): string | null;
73
77
  export declare function loadPreset(presetPath: string, cwd: string): Promise<{
74
78
  config: Config;
75
- rulesDir?: string;
76
- commandsDir?: string;
79
+ rootDir: string;
77
80
  }>;
78
81
  export declare function loadAllRules(config: Config, cwd: string): Promise<{
79
82
  rules: RuleFile[];
80
83
  commands: CommandFile[];
81
84
  assets: AssetFile[];
82
85
  mcpServers: MCPServers;
86
+ hooks: HooksJson;
87
+ hookFiles: HookFile[];
83
88
  }>;
84
- export declare function applyOverrides<T extends ManagedFile>(files: T[], overrides: Record<string, string | false>, cwd: string): T[];
85
89
  export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
90
+ /**
91
+ * Check if workspaces mode is enabled without loading all rules/presets
92
+ * This is useful for commands that only need to know the workspace setting
93
+ */
94
+ export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
86
95
  export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
87
96
  export declare function saveConfig(config: Config, cwd?: string): boolean;
@@ -11,23 +11,23 @@ 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
+ exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
19
20
  exports.loadConfig = loadConfig;
20
21
  exports.saveConfig = saveConfig;
21
22
  const fs_extra_1 = __importDefault(require("fs-extra"));
22
23
  const node_path_1 = __importDefault(require("node:path"));
23
24
  const cosmiconfig_1 = require("cosmiconfig");
24
25
  const fast_glob_1 = __importDefault(require("fast-glob"));
26
+ const hooks_1 = require("./hooks");
25
27
  exports.ALLOWED_CONFIG_KEYS = [
26
- "rulesDir",
27
- "commandsDir",
28
+ "rootDir",
28
29
  "targets",
29
30
  "presets",
30
- "overrides",
31
31
  "mcpServers",
32
32
  "workspaces",
33
33
  "skipInstall",
@@ -63,11 +63,9 @@ function resolveWorkspaces(config, configFilePath, cwd) {
63
63
  }
64
64
  function applyDefaults(config, workspaces) {
65
65
  return {
66
- rulesDir: config.rulesDir,
67
- commandsDir: config.commandsDir,
66
+ rootDir: config.rootDir,
68
67
  targets: config.targets || ["cursor"],
69
68
  presets: config.presets || [],
70
- overrides: config.overrides || {},
71
69
  mcpServers: config.mcpServers || {},
72
70
  workspaces,
73
71
  skipInstall: config.skipInstall || false,
@@ -81,36 +79,37 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
81
79
  if (unknownKeys.length > 0) {
82
80
  throw new Error(`Invalid configuration at ${configFilePath}: unknown keys: ${unknownKeys.join(", ")}`);
83
81
  }
84
- // Validate that either rulesDir or presets is provided
85
- const hasRulesDir = "rulesDir" in config && typeof config.rulesDir === "string";
86
- const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
82
+ // Validate rootDir
83
+ const hasRootDir = "rootDir" in config && typeof config.rootDir === "string";
87
84
  const hasPresets = "presets" in config &&
88
85
  Array.isArray(config.presets) &&
89
86
  config.presets.length > 0;
90
- // In workspace mode, root config doesn't need rulesDir or presets
91
- // since packages will have their own configurations
92
- if (!isWorkspaceMode && !hasRulesDir && !hasPresets && !hasCommandsDir) {
93
- throw new Error(`At least one of rulesDir, commandsDir, or presets must be specified in config at ${configFilePath}`);
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}`);
87
+ if (hasRootDir) {
88
+ const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
89
+ if (!fs_extra_1.default.existsSync(rootPath)) {
90
+ throw new Error(`Root directory does not exist: ${rootPath}`);
100
91
  }
101
- if (!fs_extra_1.default.statSync(rulesPath).isDirectory()) {
102
- throw new Error(`Rules path is not a directory: ${rulesPath}`);
92
+ if (!fs_extra_1.default.statSync(rootPath).isDirectory()) {
93
+ throw new Error(`Root path is not a directory: ${rootPath}`);
103
94
  }
104
- }
105
- if (hasCommandsDir) {
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}`);
109
- }
110
- if (!fs_extra_1.default.statSync(commandsPath).isDirectory()) {
111
- throw new Error(`Commands path is not a directory: ${commandsPath}`);
95
+ // Check for at least one valid subdirectory or file
96
+ const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
97
+ const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
98
+ const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
99
+ // In workspace mode, root config doesn't need these directories
100
+ // since packages will have their own configurations
101
+ if (!isWorkspaceMode &&
102
+ !hasRules &&
103
+ !hasCommands &&
104
+ !hasHooks &&
105
+ !hasPresets) {
106
+ throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
112
107
  }
113
108
  }
109
+ else if (!isWorkspaceMode && !hasPresets) {
110
+ // If no rootDir specified and not in workspace mode, must have presets
111
+ throw new Error(`At least one of rootDir or presets must be specified in config at ${configFilePath}`);
112
+ }
114
113
  if ("targets" in config) {
115
114
  if (!Array.isArray(config.targets)) {
116
115
  throw new Error(`targets must be an array in config at ${configFilePath}`);
@@ -124,22 +123,21 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
124
123
  }
125
124
  }
126
125
  }
127
- // Validate override rule names will be checked after rule resolution
128
126
  }
129
- async function loadRulesFromDirectory(rulesDir, source, presetName) {
127
+ async function loadRulesFromDirectory(directoryPath, source, presetName) {
130
128
  const rules = [];
131
- if (!fs_extra_1.default.existsSync(rulesDir)) {
129
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
132
130
  return rules;
133
131
  }
134
- const pattern = node_path_1.default.join(rulesDir, "**/*.mdc").replace(/\\/g, "/");
132
+ const pattern = node_path_1.default.join(directoryPath, "**/*.mdc").replace(/\\/g, "/");
135
133
  const filePaths = await (0, fast_glob_1.default)(pattern, {
136
134
  onlyFiles: true,
137
135
  absolute: true,
138
136
  });
139
137
  for (const filePath of filePaths) {
140
138
  const content = await fs_extra_1.default.readFile(filePath, "utf8");
141
- // Preserve directory structure by using relative path from rulesDir
142
- const relativePath = node_path_1.default.relative(rulesDir, filePath);
139
+ // Preserve directory structure by using relative path from source directory
140
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
143
141
  const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/");
144
142
  rules.push({
145
143
  name: ruleName,
@@ -151,12 +149,12 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
151
149
  }
152
150
  return rules;
153
151
  }
154
- async function loadCommandsFromDirectory(commandsDir, source, presetName) {
152
+ async function loadCommandsFromDirectory(directoryPath, source, presetName) {
155
153
  const commands = [];
156
- if (!fs_extra_1.default.existsSync(commandsDir)) {
154
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
157
155
  return commands;
158
156
  }
159
- const pattern = node_path_1.default.join(commandsDir, "**/*.md").replace(/\\/g, "/");
157
+ const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
160
158
  const filePaths = await (0, fast_glob_1.default)(pattern, {
161
159
  onlyFiles: true,
162
160
  absolute: true,
@@ -164,7 +162,7 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
164
162
  filePaths.sort();
165
163
  for (const filePath of filePaths) {
166
164
  const content = await fs_extra_1.default.readFile(filePath, "utf8");
167
- const relativePath = node_path_1.default.relative(commandsDir, filePath);
165
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
168
166
  const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
169
167
  commands.push({
170
168
  name: commandName,
@@ -176,13 +174,13 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
176
174
  }
177
175
  return commands;
178
176
  }
179
- async function loadAssetsFromDirectory(rulesDir, source, presetName) {
177
+ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
180
178
  const assets = [];
181
- if (!fs_extra_1.default.existsSync(rulesDir)) {
179
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
182
180
  return assets;
183
181
  }
184
182
  // Find all files except .mdc files and hidden files
185
- const pattern = node_path_1.default.join(rulesDir, "**/*").replace(/\\/g, "/");
183
+ const pattern = node_path_1.default.join(directoryPath, "**/*").replace(/\\/g, "/");
186
184
  const filePaths = await (0, fast_glob_1.default)(pattern, {
187
185
  onlyFiles: true,
188
186
  absolute: true,
@@ -190,8 +188,8 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
190
188
  });
191
189
  for (const filePath of filePaths) {
192
190
  const content = await fs_extra_1.default.readFile(filePath);
193
- // Preserve directory structure by using relative path from rulesDir
194
- const relativePath = node_path_1.default.relative(rulesDir, filePath);
191
+ // Preserve directory structure by using relative path from source directory
192
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
195
193
  // Keep extension for assets
196
194
  const assetName = relativePath.replace(/\\/g, "/");
197
195
  assets.push({
@@ -204,6 +202,19 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
204
202
  }
205
203
  return assets;
206
204
  }
205
+ /**
206
+ * Extract namespace from preset path for directory structure
207
+ * Handles both npm packages and local paths consistently
208
+ */
209
+ function extractNamespaceFromPresetPath(presetPath) {
210
+ // Special case: npm package names always use forward slashes, regardless of platform
211
+ if (presetPath.startsWith("@")) {
212
+ // For scoped packages like @scope/package/subdir, create nested directories
213
+ return presetPath.split("/");
214
+ }
215
+ const parts = presetPath.split(node_path_1.default.sep);
216
+ return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
217
+ }
207
218
  function resolvePresetPath(presetPath, cwd) {
208
219
  // Support specifying aicm.json directory and load the config from it
209
220
  if (!presetPath.endsWith(".json")) {
@@ -241,99 +252,103 @@ async function loadPreset(presetPath, cwd) {
241
252
  throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
242
253
  }
243
254
  const presetDir = node_path_1.default.dirname(resolvedPresetPath);
244
- const presetRulesDir = presetConfig.rulesDir
245
- ? node_path_1.default.resolve(presetDir, presetConfig.rulesDir)
246
- : undefined;
247
- const presetCommandsDir = presetConfig.commandsDir
248
- ? node_path_1.default.resolve(presetDir, presetConfig.commandsDir)
249
- : undefined;
250
- if (!presetRulesDir && !presetCommandsDir) {
251
- throw new Error(`Preset "${presetPath}" must have a rulesDir or commandsDir specified`);
255
+ const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
256
+ // Check for at least one valid subdirectory
257
+ const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
258
+ const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
259
+ const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
260
+ const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
261
+ if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
262
+ throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, hooks.json, or assets/`);
252
263
  }
253
264
  return {
254
265
  config: presetConfig,
255
- rulesDir: presetRulesDir,
256
- commandsDir: presetCommandsDir,
266
+ rootDir: presetRootDir,
257
267
  };
258
268
  }
259
269
  async function loadAllRules(config, cwd) {
260
270
  const allRules = [];
261
271
  const allCommands = [];
262
272
  const allAssets = [];
273
+ const allHookFiles = [];
274
+ const allHooksConfigs = [];
263
275
  let mergedMcpServers = { ...config.mcpServers };
264
- // Load local rules only if rulesDir is provided
265
- if (config.rulesDir) {
266
- const localRulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
267
- const localRules = await loadRulesFromDirectory(localRulesPath, "local");
268
- const localAssets = await loadAssetsFromDirectory(localRulesPath, "local");
269
- allRules.push(...localRules);
270
- allAssets.push(...localAssets);
271
- }
272
- if (config.commandsDir) {
273
- const localCommandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
274
- const localCommands = await loadCommandsFromDirectory(localCommandsPath, "local");
275
- allCommands.push(...localCommands);
276
+ // Load local files from rootDir only if specified
277
+ if (config.rootDir) {
278
+ const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
279
+ // Load rules from rules/ subdirectory
280
+ const rulesPath = node_path_1.default.join(rootPath, "rules");
281
+ if (fs_extra_1.default.existsSync(rulesPath)) {
282
+ const localRules = await loadRulesFromDirectory(rulesPath, "local");
283
+ allRules.push(...localRules);
284
+ }
285
+ // Load commands from commands/ subdirectory
286
+ const commandsPath = node_path_1.default.join(rootPath, "commands");
287
+ if (fs_extra_1.default.existsSync(commandsPath)) {
288
+ const localCommands = await loadCommandsFromDirectory(commandsPath, "local");
289
+ allCommands.push(...localCommands);
290
+ }
291
+ // Load hooks from hooks.json (sibling to hooks/ directory)
292
+ const hooksFilePath = node_path_1.default.join(rootPath, "hooks.json");
293
+ if (fs_extra_1.default.existsSync(hooksFilePath)) {
294
+ const { config: localHooksConfig, files: localHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(rootPath, "local");
295
+ allHooksConfigs.push(localHooksConfig);
296
+ allHookFiles.push(...localHookFiles);
297
+ }
298
+ // Load assets from assets/ subdirectory
299
+ const assetsPath = node_path_1.default.join(rootPath, "assets");
300
+ if (fs_extra_1.default.existsSync(assetsPath)) {
301
+ const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
302
+ allAssets.push(...localAssets);
303
+ }
276
304
  }
305
+ // Load presets
277
306
  if (config.presets) {
278
307
  for (const presetPath of config.presets) {
279
308
  const preset = await loadPreset(presetPath, cwd);
280
- if (preset.rulesDir) {
281
- const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
282
- const presetAssets = await loadAssetsFromDirectory(preset.rulesDir, "preset", presetPath);
309
+ const presetRootDir = preset.rootDir;
310
+ // Load preset rules from rules/ subdirectory
311
+ const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
312
+ if (fs_extra_1.default.existsSync(presetRulesPath)) {
313
+ const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
283
314
  allRules.push(...presetRules);
284
- allAssets.push(...presetAssets);
285
315
  }
286
- if (preset.commandsDir) {
287
- const presetCommands = await loadCommandsFromDirectory(preset.commandsDir, "preset", presetPath);
316
+ // Load preset commands from commands/ subdirectory
317
+ const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
318
+ if (fs_extra_1.default.existsSync(presetCommandsPath)) {
319
+ const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
288
320
  allCommands.push(...presetCommands);
289
321
  }
322
+ // Load preset hooks from hooks.json (sibling to hooks/ directory)
323
+ const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
324
+ if (fs_extra_1.default.existsSync(presetHooksFile)) {
325
+ const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
326
+ allHooksConfigs.push(presetHooksConfig);
327
+ allHookFiles.push(...presetHookFiles);
328
+ }
329
+ // Load preset assets from assets/ subdirectory
330
+ const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
331
+ if (fs_extra_1.default.existsSync(presetAssetsPath)) {
332
+ const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
333
+ allAssets.push(...presetAssets);
334
+ }
290
335
  // Merge MCP servers from preset
291
336
  if (preset.config.mcpServers) {
292
337
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
293
338
  }
294
339
  }
295
340
  }
341
+ // Merge all hooks configurations
342
+ const mergedHooks = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
296
343
  return {
297
344
  rules: allRules,
298
345
  commands: allCommands,
299
346
  assets: allAssets,
300
347
  mcpServers: mergedMcpServers,
348
+ hooks: mergedHooks,
349
+ hookFiles: allHookFiles,
301
350
  };
302
351
  }
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
352
  /**
338
353
  * Merge preset MCP servers with local config MCP servers
339
354
  * Local config takes precedence over preset config
@@ -347,7 +362,7 @@ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
347
362
  delete newMcpServers[serverName];
348
363
  continue;
349
364
  }
350
- // Only add if not already defined in config (override handled by config)
365
+ // Only add if not already defined in config (local config takes precedence)
351
366
  if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
352
367
  newMcpServers[serverName] = serverConfig;
353
368
  }
@@ -366,6 +381,18 @@ async function loadConfigFile(searchFrom) {
366
381
  throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
367
382
  }
368
383
  }
384
+ /**
385
+ * Check if workspaces mode is enabled without loading all rules/presets
386
+ * This is useful for commands that only need to know the workspace setting
387
+ */
388
+ async function checkWorkspacesEnabled(cwd) {
389
+ const workingDir = cwd || process.cwd();
390
+ const configResult = await loadConfigFile(workingDir);
391
+ if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
392
+ return detectWorkspacesFromPackageJson(workingDir);
393
+ }
394
+ return resolveWorkspaces(configResult.config, configResult.filepath, workingDir);
395
+ }
369
396
  async function loadConfig(cwd) {
370
397
  const workingDir = cwd || process.cwd();
371
398
  const configResult = await loadConfigFile(workingDir);
@@ -376,34 +403,15 @@ async function loadConfig(cwd) {
376
403
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
377
404
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
378
405
  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
- }
406
+ const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
401
407
  return {
402
408
  config: configWithDefaults,
403
- rules: rulesWithOverrides,
404
- commands: commandsWithOverrides,
409
+ rules,
410
+ commands,
405
411
  assets,
406
412
  mcpServers,
413
+ hooks,
414
+ hookFiles,
407
415
  };
408
416
  }
409
417
  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;