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.
@@ -5,133 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installWorkspaces = installWorkspaces;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const fs_extra_1 = __importDefault(require("fs-extra"));
9
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const hooks_1 = require("../utils/hooks");
10
10
  const working_directory_1 = require("../utils/working-directory");
11
- const rules_file_writer_1 = require("../utils/rules-file-writer");
12
11
  const workspace_discovery_1 = require("../utils/workspace-discovery");
13
12
  const install_1 = require("./install");
14
- /**
15
- * Extract .mdc file references from a command for warning purposes
16
- * Returns absolute paths to .mdc files that are referenced
17
- */
18
- function extractMdcReferences(content, commandSourcePath) {
19
- const commandDir = node_path_1.default.dirname(commandSourcePath);
20
- const mdcFiles = [];
21
- const seenPaths = new Set();
22
- // Same regex pattern as rewriteCommandRelativeLinks
23
- const matches = content.matchAll(/\.\.[/\\][\w\-/\\.]+/g);
24
- for (const match of matches) {
25
- const relativePath = match[0];
26
- const resolved = node_path_1.default.normalize(node_path_1.default.resolve(commandDir, relativePath));
27
- // Only process .mdc files that exist
28
- if (resolved.endsWith(".mdc") &&
29
- !seenPaths.has(resolved) &&
30
- fs_extra_1.default.existsSync(resolved) &&
31
- fs_extra_1.default.statSync(resolved).isFile()) {
32
- seenPaths.add(resolved);
33
- mdcFiles.push(resolved);
34
- }
35
- }
36
- return mdcFiles;
37
- }
38
- /**
39
- * Check if a .mdc file is a manual rule (not automatic/auto-attached/agent-requested)
40
- */
41
- function isManualRule(content) {
42
- const metadata = (0, rules_file_writer_1.parseRuleFrontmatter)(content);
43
- // Check for always rules
44
- if (metadata.type === "always" ||
45
- metadata.alwaysApply === true ||
46
- metadata.alwaysApply === "true") {
47
- return false;
48
- }
49
- // Check for auto-attached rules
50
- if (metadata.type === "auto-attached" || metadata.globs) {
51
- return false;
52
- }
53
- // Check for agent-requested rules
54
- if (metadata.type === "agent-requested" || metadata.description) {
55
- return false;
56
- }
57
- // Default to manual rule
58
- return true;
59
- }
60
- /**
61
- * Process .mdc files referenced by commands in workspace mode
62
- * Warns about non-manual rules and copies them to root for command access
63
- */
64
- function processMdcFilesForWorkspace(mdcFilePaths, commands, packages, rootDir) {
65
- const mdcAssets = [];
66
- // Build a map of command source paths to their preset names
67
- const commandPresetMap = new Map();
68
- for (const command of commands) {
69
- const commandDir = node_path_1.default.dirname(command.sourcePath);
70
- commandPresetMap.set(commandDir, command.presetName);
71
- }
72
- for (const mdcPath of mdcFilePaths) {
73
- const content = fs_extra_1.default.readFileSync(mdcPath, "utf8");
74
- if (!isManualRule(content)) {
75
- const relativePath = node_path_1.default.basename(mdcPath);
76
- console.warn(chalk_1.default.yellow(`Warning: Command references non-manual rule file "${relativePath}". ` +
77
- `This may cause the rule to be included twice in the context. ` +
78
- `Consider using manual rules (without alwaysApply, globs, or description metadata) ` +
79
- `when referencing from commands.`));
80
- }
81
- // Find which command references this .mdc file and get its preset name
82
- let mdcAssetName = "";
83
- let presetName;
84
- let found = false;
85
- // First check packages for local files
86
- for (const pkg of packages) {
87
- const rulesDir = pkg.config.config.rulesDir;
88
- if (rulesDir) {
89
- const pkgRulesPath = node_path_1.default.join(pkg.absolutePath, rulesDir);
90
- if (mdcPath.startsWith(pkgRulesPath)) {
91
- mdcAssetName = node_path_1.default.relative(pkgRulesPath, mdcPath);
92
- found = true;
93
- break;
94
- }
95
- }
96
- }
97
- // If not found in local packages, it might be from a preset
98
- if (!found) {
99
- // Find the command that references this .mdc file
100
- for (const command of commands) {
101
- if (command.presetName) {
102
- const commandDir = node_path_1.default.dirname(command.sourcePath);
103
- const rulesDir = node_path_1.default.join(node_path_1.default.dirname(commandDir), "rules");
104
- if (mdcPath.startsWith(rulesDir)) {
105
- mdcAssetName = node_path_1.default.relative(rulesDir, mdcPath);
106
- presetName = command.presetName;
107
- found = true;
108
- break;
109
- }
110
- }
111
- }
112
- }
113
- if (found) {
114
- // Build the final asset name with preset namespace if applicable
115
- const finalAssetName = presetName
116
- ? node_path_1.default.posix.join(...(0, install_1.extractNamespaceFromPresetPath)(presetName), mdcAssetName.replace(/\\/g, "/"))
117
- : mdcAssetName.replace(/\\/g, "/");
118
- // Create an AssetFile entry for the .mdc file
119
- mdcAssets.push({
120
- name: finalAssetName,
121
- content: Buffer.from(content),
122
- sourcePath: mdcPath,
123
- source: presetName ? "preset" : "local",
124
- presetName,
125
- });
126
- // Copy to root .cursor/rules/aicm/
127
- const cursorRulesDir = node_path_1.default.join(rootDir, ".cursor", "rules", "aicm");
128
- const targetPath = node_path_1.default.join(cursorRulesDir, finalAssetName);
129
- fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(targetPath));
130
- fs_extra_1.default.writeFileSync(targetPath, content);
131
- }
132
- }
133
- return mdcAssets;
134
- }
135
13
  function mergeWorkspaceCommands(packages) {
136
14
  var _a;
137
15
  const commands = [];
@@ -194,6 +72,26 @@ function mergeWorkspaceMcpServers(packages) {
194
72
  }
195
73
  return { merged, conflicts };
196
74
  }
75
+ /**
76
+ * Merge hooks from multiple workspace packages
77
+ */
78
+ function mergeWorkspaceHooks(packages) {
79
+ const allHooksConfigs = [];
80
+ const allHookFiles = [];
81
+ for (const pkg of packages) {
82
+ // Collect hooks configs
83
+ if (pkg.config.hooks) {
84
+ allHooksConfigs.push(pkg.config.hooks);
85
+ }
86
+ // Collect hook files
87
+ allHookFiles.push(...pkg.config.hookFiles);
88
+ }
89
+ // Merge hooks configs
90
+ const merged = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
91
+ // Dedupe hook files by basename with MD5 checking
92
+ const dedupedHookFiles = (0, hooks_1.dedupeHookFiles)(allHookFiles);
93
+ return { merged, hookFiles: dedupedHookFiles };
94
+ }
197
95
  /**
198
96
  * Install aicm configurations for all packages in a workspace
199
97
  */
@@ -202,6 +100,7 @@ async function installWorkspacesPackages(packages, options = {}) {
202
100
  let totalRuleCount = 0;
203
101
  let totalCommandCount = 0;
204
102
  let totalAssetCount = 0;
103
+ let totalHookCount = 0;
205
104
  // Install packages sequentially for now (can be parallelized later)
206
105
  for (const pkg of packages) {
207
106
  const packagePath = pkg.absolutePath;
@@ -214,6 +113,7 @@ async function installWorkspacesPackages(packages, options = {}) {
214
113
  totalRuleCount += result.installedRuleCount;
215
114
  totalCommandCount += result.installedCommandCount;
216
115
  totalAssetCount += result.installedAssetCount;
116
+ totalHookCount += result.installedHookCount;
217
117
  results.push({
218
118
  path: pkg.relativePath,
219
119
  success: result.success,
@@ -221,6 +121,7 @@ async function installWorkspacesPackages(packages, options = {}) {
221
121
  installedRuleCount: result.installedRuleCount,
222
122
  installedCommandCount: result.installedCommandCount,
223
123
  installedAssetCount: result.installedAssetCount,
124
+ installedHookCount: result.installedHookCount,
224
125
  });
225
126
  }
226
127
  catch (error) {
@@ -231,6 +132,7 @@ async function installWorkspacesPackages(packages, options = {}) {
231
132
  installedRuleCount: 0,
232
133
  installedCommandCount: 0,
233
134
  installedAssetCount: 0,
135
+ installedHookCount: 0,
234
136
  });
235
137
  }
236
138
  }
@@ -241,6 +143,7 @@ async function installWorkspacesPackages(packages, options = {}) {
241
143
  totalRuleCount,
242
144
  totalCommandCount,
243
145
  totalAssetCount,
146
+ totalHookCount,
244
147
  };
245
148
  }
246
149
  /**
@@ -272,6 +175,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
272
175
  installedRuleCount: 0,
273
176
  installedCommandCount: 0,
274
177
  installedAssetCount: 0,
178
+ installedHookCount: 0,
275
179
  packagesCount: 0,
276
180
  };
277
181
  }
@@ -296,20 +200,11 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
296
200
  workspaceCommands.length > 0 &&
297
201
  workspaceCommandTargets.length > 0) {
298
202
  const dedupedWorkspaceCommands = (0, install_1.dedupeCommandsForInstall)(workspaceCommands);
299
- // Collect all assets from packages for command path rewriting
203
+ // Collect all assets from packages
300
204
  const allAssets = packages.flatMap((pkg) => { var _a; return (_a = pkg.config.assets) !== null && _a !== void 0 ? _a : []; });
301
- // Copy assets to root so root commands can reference them
205
+ // Copy assets to root
302
206
  (0, install_1.writeAssetsToTargets)(allAssets, workspaceCommandTargets);
303
- // Extract and process .mdc file references from commands
304
- const mdcFilesSet = new Set();
305
- for (const command of dedupedWorkspaceCommands) {
306
- const mdcRefs = extractMdcReferences(command.content, command.sourcePath);
307
- mdcRefs.forEach((ref) => mdcFilesSet.add(ref));
308
- }
309
- const mdcAssets = processMdcFilesForWorkspace(mdcFilesSet, dedupedWorkspaceCommands, packages, cwd);
310
- // Merge .mdc assets with regular assets for link rewriting
311
- const allAssetsWithMdc = [...allAssets, ...mdcAssets];
312
- (0, install_1.writeCommandsToTargets)(dedupedWorkspaceCommands, allAssetsWithMdc, workspaceCommandTargets);
207
+ (0, install_1.writeCommandsToTargets)(dedupedWorkspaceCommands, workspaceCommandTargets);
313
208
  }
314
209
  const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
315
210
  const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
@@ -320,6 +215,12 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
320
215
  for (const conflict of conflicts) {
321
216
  console.warn(`Warning: MCP configuration conflict detected\n Key: "${conflict.key}"\n Packages: ${conflict.packages.join(", ")}\n Using configuration from: ${conflict.chosen}`);
322
217
  }
218
+ // Merge and write hooks for workspace
219
+ const { merged: rootHooks, hookFiles: rootHookFiles } = mergeWorkspaceHooks(packages);
220
+ const hasHooks = rootHooks.hooks && Object.keys(rootHooks.hooks).length > 0;
221
+ if (!dryRun && hasCursorTarget && (hasHooks || rootHookFiles.length > 0)) {
222
+ (0, hooks_1.writeHooksToCursor)(rootHooks, rootHookFiles, cwd);
223
+ }
323
224
  if (verbose) {
324
225
  result.packages.forEach((pkg) => {
325
226
  if (pkg.success) {
@@ -327,6 +228,9 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
327
228
  if (pkg.installedCommandCount > 0) {
328
229
  summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
329
230
  }
231
+ if (pkg.installedHookCount > 0) {
232
+ summaryParts.push(`${pkg.installedHookCount} hook${pkg.installedHookCount === 1 ? "" : "s"}`);
233
+ }
330
234
  console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
331
235
  }
332
236
  else {
@@ -341,7 +245,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
341
245
  const commandSummary = result.totalCommandCount > 0
342
246
  ? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
343
247
  : "";
344
- console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary})`));
248
+ const hookSummary = result.totalHookCount > 0
249
+ ? `, ${result.totalHookCount} hook${result.totalHookCount === 1 ? "" : "s"} total`
250
+ : "";
251
+ console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary}${hookSummary})`));
345
252
  console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
346
253
  }
347
254
  const errorDetails = failedPackages
@@ -353,6 +260,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
353
260
  installedRuleCount: result.totalRuleCount,
354
261
  installedCommandCount: result.totalCommandCount,
355
262
  installedAssetCount: result.totalAssetCount,
263
+ installedHookCount: result.totalHookCount,
356
264
  packagesCount: result.packages.length,
357
265
  };
358
266
  }
@@ -361,6 +269,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
361
269
  installedRuleCount: result.totalRuleCount,
362
270
  installedCommandCount: result.totalCommandCount,
363
271
  installedAssetCount: result.totalAssetCount,
272
+ installedHookCount: result.totalHookCount,
364
273
  packagesCount: result.packages.length,
365
274
  };
366
275
  });
@@ -45,14 +45,17 @@ export interface InstallResult {
45
45
  * Number of assets installed
46
46
  */
47
47
  installedAssetCount: number;
48
+ /**
49
+ * Number of hooks installed
50
+ */
51
+ installedHookCount: number;
48
52
  /**
49
53
  * Number of packages installed
50
54
  */
51
55
  packagesCount: number;
52
56
  }
53
- export declare function extractNamespaceFromPresetPath(presetPath: string): string[];
54
57
  export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
55
- export declare function writeCommandsToTargets(commands: CommandFile[], assets: AssetFile[], targets: SupportedTarget[]): void;
58
+ export declare function writeCommandsToTargets(commands: CommandFile[], targets: SupportedTarget[]): void;
56
59
  export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
57
60
  export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
58
61
  /**
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
7
6
  exports.writeAssetsToTargets = writeAssetsToTargets;
8
7
  exports.writeCommandsToTargets = writeCommandsToTargets;
9
8
  exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
@@ -16,14 +15,25 @@ const chalk_1 = __importDefault(require("chalk"));
16
15
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
16
  const node_path_1 = __importDefault(require("node:path"));
18
17
  const config_1 = require("../utils/config");
18
+ const hooks_1 = require("../utils/hooks");
19
19
  const working_directory_1 = require("../utils/working-directory");
20
20
  const is_ci_1 = require("../utils/is-ci");
21
21
  const rules_file_writer_1 = require("../utils/rules-file-writer");
22
22
  const install_workspaces_1 = require("./install-workspaces");
23
+ /**
24
+ * Rewrite asset references from source paths to installation paths
25
+ * Only rewrites the ../assets/ pattern - everything else is preserved
26
+ */
27
+ function rewriteAssetReferences(content) {
28
+ // Replace ../assets/ with ../../assets/aicm/
29
+ // Handles both forward slashes and backslashes for cross-platform compatibility
30
+ return content.replace(/\.\.[\\/]assets[\\/]/g, "../../assets/aicm/");
31
+ }
23
32
  function getTargetPaths() {
24
33
  const projectDir = process.cwd();
25
34
  return {
26
35
  cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
36
+ assetsAicm: node_path_1.default.join(projectDir, ".cursor", "assets", "aicm"),
27
37
  aicm: node_path_1.default.join(projectDir, ".aicm"),
28
38
  };
29
39
  }
@@ -34,7 +44,7 @@ function writeCursorRules(rules, cursorRulesDir) {
34
44
  const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean);
35
45
  if (rule.presetName) {
36
46
  // For rules from presets, create a namespaced directory structure
37
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
47
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
38
48
  // Path will be: cursorRulesDir/namespace/rule-name.mdc
39
49
  rulePath = node_path_1.default.join(cursorRulesDir, ...namespace, ...ruleNameParts);
40
50
  }
@@ -44,10 +54,12 @@ function writeCursorRules(rules, cursorRulesDir) {
44
54
  }
45
55
  const ruleFile = rulePath + ".mdc";
46
56
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
47
- fs_extra_1.default.writeFileSync(ruleFile, rule.content);
57
+ // Rewrite asset references before writing
58
+ const content = rewriteAssetReferences(rule.content);
59
+ fs_extra_1.default.writeFileSync(ruleFile, content);
48
60
  }
49
61
  }
50
- function writeCursorCommands(commands, cursorCommandsDir, assets) {
62
+ function writeCursorCommands(commands, cursorCommandsDir) {
51
63
  fs_extra_1.default.removeSync(cursorCommandsDir);
52
64
  for (const command of commands) {
53
65
  const commandNameParts = command.name
@@ -57,46 +69,11 @@ function writeCursorCommands(commands, cursorCommandsDir, assets) {
57
69
  const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
58
70
  const commandFile = commandPath + ".md";
59
71
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
60
- // If the command file references assets in the rules directory, we need to rewrite the links.
61
- // Commands are installed in .cursor/commands/aicm/
62
- // Rules/assets are installed in .cursor/rules/aicm/
63
- // So a link like "../rules/asset.json" in source (from commands/ to rules/)
64
- // needs to become "../../rules/aicm/asset.json" in target (from .cursor/commands/aicm/ to .cursor/rules/aicm/)
65
- const content = rewriteCommandRelativeLinks(command.content, command.sourcePath, assets);
72
+ // Rewrite asset references before writing
73
+ const content = rewriteAssetReferences(command.content);
66
74
  fs_extra_1.default.writeFileSync(commandFile, content);
67
75
  }
68
76
  }
69
- function rewriteCommandRelativeLinks(content, commandSourcePath, assets) {
70
- const commandDir = node_path_1.default.dirname(commandSourcePath);
71
- const assetMap = new Map(assets.map((a) => {
72
- let targetPath;
73
- if (a.presetName) {
74
- const namespace = extractNamespaceFromPresetPath(a.presetName);
75
- // Use posix paths for URLs/links (always forward slashes)
76
- targetPath = node_path_1.default.posix.join(...namespace, a.name);
77
- }
78
- else {
79
- // Normalize to posix for consistent forward slashes in links
80
- targetPath = a.name.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
81
- }
82
- return [node_path_1.default.normalize(a.sourcePath), targetPath];
83
- }));
84
- return content.replace(/\.\.[/\\][\w\-/\\.]+/g, (match) => {
85
- const resolved = node_path_1.default.normalize(node_path_1.default.resolve(commandDir, match));
86
- return assetMap.has(resolved)
87
- ? `../../rules/aicm/${assetMap.get(resolved)}`
88
- : match;
89
- });
90
- }
91
- function extractNamespaceFromPresetPath(presetPath) {
92
- // Special case: npm package names always use forward slashes, regardless of platform
93
- if (presetPath.startsWith("@")) {
94
- // For scoped packages like @scope/package/subdir, create nested directories
95
- return presetPath.split("/");
96
- }
97
- const parts = presetPath.split(node_path_1.default.sep);
98
- return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
99
- }
100
77
  /**
101
78
  * Write rules to a shared directory and update the given rules file
102
79
  */
@@ -107,7 +84,7 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
107
84
  const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean);
108
85
  if (rule.presetName) {
109
86
  // For rules from presets, create a namespaced directory structure
110
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
87
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
111
88
  // Path will be: ruleDir/namespace/rule-name.md
112
89
  rulePath = node_path_1.default.join(ruleDir, ...namespace, ...ruleNameParts);
113
90
  }
@@ -115,7 +92,8 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
115
92
  // For local rules, maintain the original flat structure
116
93
  rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
117
94
  }
118
- const content = rule.content;
95
+ // Rewrite asset references before writing
96
+ const content = rewriteAssetReferences(rule.content);
119
97
  const physicalRulePath = rulePath + ".md";
120
98
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
121
99
  fs_extra_1.default.writeFileSync(physicalRulePath, content);
@@ -123,7 +101,7 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
123
101
  // For the rules file, maintain the same structure
124
102
  let windsurfPath;
125
103
  if (rule.presetName) {
126
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
104
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
127
105
  windsurfPath =
128
106
  node_path_1.default.join(relativeRuleDir, ...namespace, ...ruleNameParts) + ".md";
129
107
  }
@@ -147,7 +125,7 @@ function writeAssetsToTargets(assets, targets) {
147
125
  let targetDir;
148
126
  switch (target) {
149
127
  case "cursor":
150
- targetDir = targetPaths.cursor;
128
+ targetDir = targetPaths.assetsAicm;
151
129
  break;
152
130
  case "windsurf":
153
131
  case "codex":
@@ -160,7 +138,7 @@ function writeAssetsToTargets(assets, targets) {
160
138
  for (const asset of assets) {
161
139
  let assetPath;
162
140
  if (asset.presetName) {
163
- const namespace = extractNamespaceFromPresetPath(asset.presetName);
141
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(asset.presetName);
164
142
  assetPath = node_path_1.default.join(targetDir, ...namespace, asset.name);
165
143
  }
166
144
  else {
@@ -203,13 +181,13 @@ function writeRulesToTargets(rules, assets, targets) {
203
181
  // Write assets after rules so they don't get wiped by emptyDirSync
204
182
  writeAssetsToTargets(assets, targets);
205
183
  }
206
- function writeCommandsToTargets(commands, assets, targets) {
184
+ function writeCommandsToTargets(commands, targets) {
207
185
  const projectDir = process.cwd();
208
186
  const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
209
187
  for (const target of targets) {
210
188
  if (target === "cursor") {
211
189
  const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
212
- writeCursorCommands(commands, commandsDir, assets);
190
+ writeCursorCommands(commands, commandsDir);
213
191
  }
214
192
  // Other targets do not support commands yet
215
193
  }
@@ -259,6 +237,21 @@ function writeMcpServersToTargets(mcpServers, targets, cwd) {
259
237
  // Windsurf and Codex do not support project mcpServers, so skip
260
238
  }
261
239
  }
240
+ /**
241
+ * Write hooks to IDE targets
242
+ */
243
+ function writeHooksToTargets(hooksConfig, hookFiles, targets, cwd) {
244
+ const hasHooks = hooksConfig.hooks && Object.keys(hooksConfig.hooks).length > 0;
245
+ if (!hasHooks && hookFiles.length === 0) {
246
+ return;
247
+ }
248
+ for (const target of targets) {
249
+ if (target === "cursor") {
250
+ (0, hooks_1.writeHooksToCursor)(hooksConfig, hookFiles, cwd);
251
+ }
252
+ // Other targets do not support hooks yet
253
+ }
254
+ }
262
255
  /**
263
256
  * Write MCP servers configuration to a specific file
264
257
  */
@@ -318,16 +311,18 @@ async function installPackage(options = {}) {
318
311
  installedRuleCount: 0,
319
312
  installedCommandCount: 0,
320
313
  installedAssetCount: 0,
314
+ installedHookCount: 0,
321
315
  packagesCount: 0,
322
316
  };
323
317
  }
324
- const { config, rules, commands, assets, mcpServers } = resolvedConfig;
318
+ const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
325
319
  if (config.skipInstall === true) {
326
320
  return {
327
321
  success: true,
328
322
  installedRuleCount: 0,
329
323
  installedCommandCount: 0,
330
324
  installedAssetCount: 0,
325
+ installedHookCount: 0,
331
326
  packagesCount: 0,
332
327
  };
333
328
  }
@@ -336,18 +331,23 @@ async function installPackage(options = {}) {
336
331
  try {
337
332
  if (!options.dryRun) {
338
333
  writeRulesToTargets(rules, assets, config.targets);
339
- writeCommandsToTargets(commandsToInstall, assets, config.targets);
334
+ writeCommandsToTargets(commandsToInstall, config.targets);
340
335
  if (mcpServers && Object.keys(mcpServers).length > 0) {
341
336
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
342
337
  }
338
+ if (hooks && ((0, hooks_1.countHooks)(hooks) > 0 || hookFiles.length > 0)) {
339
+ writeHooksToTargets(hooks, hookFiles, config.targets, cwd);
340
+ }
343
341
  }
344
342
  const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
345
343
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
344
+ const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
346
345
  return {
347
346
  success: true,
348
347
  installedRuleCount: uniqueRuleCount,
349
348
  installedCommandCount: uniqueCommandCount,
350
349
  installedAssetCount: assets.length,
350
+ installedHookCount: uniqueHookCount,
351
351
  packagesCount: 1,
352
352
  };
353
353
  }
@@ -358,6 +358,7 @@ async function installPackage(options = {}) {
358
358
  installedRuleCount: 0,
359
359
  installedCommandCount: 0,
360
360
  installedAssetCount: 0,
361
+ installedHookCount: 0,
361
362
  packagesCount: 0,
362
363
  };
363
364
  }
@@ -377,6 +378,7 @@ async function install(options = {}) {
377
378
  installedRuleCount: 0,
378
379
  installedCommandCount: 0,
379
380
  installedAssetCount: 0,
381
+ installedHookCount: 0,
380
382
  packagesCount: 0,
381
383
  };
382
384
  }
@@ -408,10 +410,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
408
410
  else {
409
411
  const ruleCount = result.installedRuleCount;
410
412
  const commandCount = result.installedCommandCount;
413
+ const hookCount = result.installedHookCount;
411
414
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
412
415
  const commandMessage = commandCount > 0
413
416
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
414
417
  : null;
418
+ const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
415
419
  const countsParts = [];
416
420
  if (ruleMessage) {
417
421
  countsParts.push(ruleMessage);
@@ -419,7 +423,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
419
423
  if (commandMessage) {
420
424
  countsParts.push(commandMessage);
421
425
  }
422
- const countsMessage = countsParts.length > 0 ? countsParts.join(" and ") : "0 rules";
426
+ if (hookMessage) {
427
+ countsParts.push(hookMessage);
428
+ }
429
+ const countsMessage = countsParts.length > 0
430
+ ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
431
+ : "0 rules";
423
432
  if (dryRun) {
424
433
  if (result.packagesCount > 1) {
425
434
  console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
@@ -428,8 +437,8 @@ async function installCommand(installOnCI, verbose, dryRun) {
428
437
  console.log(`Dry run: validated ${countsMessage}`);
429
438
  }
430
439
  }
431
- else if (ruleCount === 0 && commandCount === 0) {
432
- console.log("No rules or commands installed");
440
+ else if (ruleCount === 0 && commandCount === 0 && hookCount === 0) {
441
+ console.log("No rules, commands, or hooks installed");
433
442
  }
434
443
  else if (result.packagesCount > 1) {
435
444
  console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
@@ -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,37 @@ 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>;
86
90
  export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
87
91
  export declare function saveConfig(config: Config, cwd?: string): boolean;