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.
@@ -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,41 @@ 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
+ * @param content - The file content to rewrite
28
+ * @param presetName - The preset name if this file is from a preset
29
+ * @param fileInstallDepth - The depth of the file's installation directory relative to .cursor/
30
+ * For example: .cursor/commands/aicm/file.md has depth 2 (commands, aicm)
31
+ * .cursor/rules/aicm/preset/file.mdc has depth 3 (rules, aicm, preset)
32
+ */
33
+ function rewriteAssetReferences(content, presetName, fileInstallDepth = 2) {
34
+ // Calculate the relative path from the file to .cursor/assets/aicm/
35
+ // We need to go up fileInstallDepth levels to reach .cursor/, then down to assets/aicm/
36
+ const upLevels = "../".repeat(fileInstallDepth);
37
+ // If this is from a preset, include the preset namespace in the asset path
38
+ let assetBasePath = "assets/aicm/";
39
+ if (presetName) {
40
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
41
+ assetBasePath = node_path_1.default.posix.join("assets", "aicm", ...namespace) + "/";
42
+ }
43
+ const targetPath = upLevels + assetBasePath;
44
+ // Replace ../assets/ with the calculated target path
45
+ // Handles both forward slashes and backslashes for cross-platform compatibility
46
+ return content.replace(/\.\.[\\/]assets[\\/]/g, targetPath);
47
+ }
23
48
  function getTargetPaths() {
24
49
  const projectDir = process.cwd();
25
50
  return {
26
51
  cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
52
+ assetsAicm: node_path_1.default.join(projectDir, ".cursor", "assets", "aicm"),
27
53
  aicm: node_path_1.default.join(projectDir, ".aicm"),
28
54
  };
29
55
  }
@@ -34,7 +60,7 @@ function writeCursorRules(rules, cursorRulesDir) {
34
60
  const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean);
35
61
  if (rule.presetName) {
36
62
  // For rules from presets, create a namespaced directory structure
37
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
63
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
38
64
  // Path will be: cursorRulesDir/namespace/rule-name.mdc
39
65
  rulePath = node_path_1.default.join(cursorRulesDir, ...namespace, ...ruleNameParts);
40
66
  }
@@ -44,10 +70,22 @@ function writeCursorRules(rules, cursorRulesDir) {
44
70
  }
45
71
  const ruleFile = rulePath + ".mdc";
46
72
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
47
- fs_extra_1.default.writeFileSync(ruleFile, rule.content);
73
+ // Calculate the depth for asset path rewriting
74
+ // cursorRulesDir is .cursor/rules/aicm (depth 2 from .cursor)
75
+ // Add namespace depth if present
76
+ let fileInstallDepth = 2; // rules, aicm
77
+ if (rule.presetName) {
78
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
79
+ fileInstallDepth += namespace.length;
80
+ }
81
+ // Add any subdirectories in the rule name
82
+ fileInstallDepth += ruleNameParts.length - 1; // -1 because the last part is the filename
83
+ // Rewrite asset references before writing
84
+ const content = rewriteAssetReferences(rule.content, rule.presetName, fileInstallDepth);
85
+ fs_extra_1.default.writeFileSync(ruleFile, content);
48
86
  }
49
87
  }
50
- function writeCursorCommands(commands, cursorCommandsDir, assets) {
88
+ function writeCursorCommands(commands, cursorCommandsDir) {
51
89
  fs_extra_1.default.removeSync(cursorCommandsDir);
52
90
  for (const command of commands) {
53
91
  const commandNameParts = command.name
@@ -57,46 +95,17 @@ function writeCursorCommands(commands, cursorCommandsDir, assets) {
57
95
  const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
58
96
  const commandFile = commandPath + ".md";
59
97
  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);
98
+ // Calculate the depth for asset path rewriting
99
+ // cursorCommandsDir is .cursor/commands/aicm (depth 2 from .cursor)
100
+ // Commands are NOT namespaced by preset, but we still need to account for subdirectories
101
+ let fileInstallDepth = 2; // commands, aicm
102
+ // Add any subdirectories in the command name
103
+ fileInstallDepth += commandNameParts.length - 1; // -1 because the last part is the filename
104
+ // Rewrite asset references before writing
105
+ const content = rewriteAssetReferences(command.content, command.presetName, fileInstallDepth);
66
106
  fs_extra_1.default.writeFileSync(commandFile, content);
67
107
  }
68
108
  }
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
109
  /**
101
110
  * Write rules to a shared directory and update the given rules file
102
111
  */
@@ -107,7 +116,7 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
107
116
  const ruleNameParts = rule.name.split(node_path_1.default.sep).filter(Boolean);
108
117
  if (rule.presetName) {
109
118
  // For rules from presets, create a namespaced directory structure
110
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
119
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
111
120
  // Path will be: ruleDir/namespace/rule-name.md
112
121
  rulePath = node_path_1.default.join(ruleDir, ...namespace, ...ruleNameParts);
113
122
  }
@@ -115,7 +124,12 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
115
124
  // For local rules, maintain the original flat structure
116
125
  rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
117
126
  }
118
- const content = rule.content;
127
+ // For windsurf/codex/claude, assets are installed at the same namespace level as rules
128
+ // Example: .aicm/my-preset/rule.md and .aicm/my-preset/asset.json
129
+ // So we need to remove the 'assets/' part from the path
130
+ // ../assets/file.json -> ../file.json
131
+ // ../../assets/file.json -> ../../file.json
132
+ const content = rule.content.replace(/(\.\.[/\\])assets[/\\]/g, "$1");
119
133
  const physicalRulePath = rulePath + ".md";
120
134
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
121
135
  fs_extra_1.default.writeFileSync(physicalRulePath, content);
@@ -123,7 +137,7 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
123
137
  // For the rules file, maintain the same structure
124
138
  let windsurfPath;
125
139
  if (rule.presetName) {
126
- const namespace = extractNamespaceFromPresetPath(rule.presetName);
140
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
127
141
  windsurfPath =
128
142
  node_path_1.default.join(relativeRuleDir, ...namespace, ...ruleNameParts) + ".md";
129
143
  }
@@ -147,7 +161,7 @@ function writeAssetsToTargets(assets, targets) {
147
161
  let targetDir;
148
162
  switch (target) {
149
163
  case "cursor":
150
- targetDir = targetPaths.cursor;
164
+ targetDir = targetPaths.assetsAicm;
151
165
  break;
152
166
  case "windsurf":
153
167
  case "codex":
@@ -160,7 +174,7 @@ function writeAssetsToTargets(assets, targets) {
160
174
  for (const asset of assets) {
161
175
  let assetPath;
162
176
  if (asset.presetName) {
163
- const namespace = extractNamespaceFromPresetPath(asset.presetName);
177
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(asset.presetName);
164
178
  assetPath = node_path_1.default.join(targetDir, ...namespace, asset.name);
165
179
  }
166
180
  else {
@@ -203,13 +217,13 @@ function writeRulesToTargets(rules, assets, targets) {
203
217
  // Write assets after rules so they don't get wiped by emptyDirSync
204
218
  writeAssetsToTargets(assets, targets);
205
219
  }
206
- function writeCommandsToTargets(commands, assets, targets) {
220
+ function writeCommandsToTargets(commands, targets) {
207
221
  const projectDir = process.cwd();
208
222
  const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
209
223
  for (const target of targets) {
210
224
  if (target === "cursor") {
211
225
  const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
212
- writeCursorCommands(commands, commandsDir, assets);
226
+ writeCursorCommands(commands, commandsDir);
213
227
  }
214
228
  // Other targets do not support commands yet
215
229
  }
@@ -259,6 +273,21 @@ function writeMcpServersToTargets(mcpServers, targets, cwd) {
259
273
  // Windsurf and Codex do not support project mcpServers, so skip
260
274
  }
261
275
  }
276
+ /**
277
+ * Write hooks to IDE targets
278
+ */
279
+ function writeHooksToTargets(hooksConfig, hookFiles, targets, cwd) {
280
+ const hasHooks = hooksConfig.hooks && Object.keys(hooksConfig.hooks).length > 0;
281
+ if (!hasHooks && hookFiles.length === 0) {
282
+ return;
283
+ }
284
+ for (const target of targets) {
285
+ if (target === "cursor") {
286
+ (0, hooks_1.writeHooksToCursor)(hooksConfig, hookFiles, cwd);
287
+ }
288
+ // Other targets do not support hooks yet
289
+ }
290
+ }
262
291
  /**
263
292
  * Write MCP servers configuration to a specific file
264
293
  */
@@ -318,16 +347,18 @@ async function installPackage(options = {}) {
318
347
  installedRuleCount: 0,
319
348
  installedCommandCount: 0,
320
349
  installedAssetCount: 0,
350
+ installedHookCount: 0,
321
351
  packagesCount: 0,
322
352
  };
323
353
  }
324
- const { config, rules, commands, assets, mcpServers } = resolvedConfig;
354
+ const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
325
355
  if (config.skipInstall === true) {
326
356
  return {
327
357
  success: true,
328
358
  installedRuleCount: 0,
329
359
  installedCommandCount: 0,
330
360
  installedAssetCount: 0,
361
+ installedHookCount: 0,
331
362
  packagesCount: 0,
332
363
  };
333
364
  }
@@ -336,18 +367,23 @@ async function installPackage(options = {}) {
336
367
  try {
337
368
  if (!options.dryRun) {
338
369
  writeRulesToTargets(rules, assets, config.targets);
339
- writeCommandsToTargets(commandsToInstall, assets, config.targets);
370
+ writeCommandsToTargets(commandsToInstall, config.targets);
340
371
  if (mcpServers && Object.keys(mcpServers).length > 0) {
341
372
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
342
373
  }
374
+ if (hooks && ((0, hooks_1.countHooks)(hooks) > 0 || hookFiles.length > 0)) {
375
+ writeHooksToTargets(hooks, hookFiles, config.targets, cwd);
376
+ }
343
377
  }
344
378
  const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
345
379
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
380
+ const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
346
381
  return {
347
382
  success: true,
348
383
  installedRuleCount: uniqueRuleCount,
349
384
  installedCommandCount: uniqueCommandCount,
350
385
  installedAssetCount: assets.length,
386
+ installedHookCount: uniqueHookCount,
351
387
  packagesCount: 1,
352
388
  };
353
389
  }
@@ -358,6 +394,7 @@ async function installPackage(options = {}) {
358
394
  installedRuleCount: 0,
359
395
  installedCommandCount: 0,
360
396
  installedAssetCount: 0,
397
+ installedHookCount: 0,
361
398
  packagesCount: 0,
362
399
  };
363
400
  }
@@ -377,6 +414,7 @@ async function install(options = {}) {
377
414
  installedRuleCount: 0,
378
415
  installedCommandCount: 0,
379
416
  installedAssetCount: 0,
417
+ installedHookCount: 0,
380
418
  packagesCount: 0,
381
419
  };
382
420
  }
@@ -408,10 +446,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
408
446
  else {
409
447
  const ruleCount = result.installedRuleCount;
410
448
  const commandCount = result.installedCommandCount;
449
+ const hookCount = result.installedHookCount;
411
450
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
412
451
  const commandMessage = commandCount > 0
413
452
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
414
453
  : null;
454
+ const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
415
455
  const countsParts = [];
416
456
  if (ruleMessage) {
417
457
  countsParts.push(ruleMessage);
@@ -419,7 +459,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
419
459
  if (commandMessage) {
420
460
  countsParts.push(commandMessage);
421
461
  }
422
- const countsMessage = countsParts.length > 0 ? countsParts.join(" and ") : "0 rules";
462
+ if (hookMessage) {
463
+ countsParts.push(hookMessage);
464
+ }
465
+ const countsMessage = countsParts.length > 0
466
+ ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
467
+ : "0 rules";
423
468
  if (dryRun) {
424
469
  if (result.packagesCount > 1) {
425
470
  console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
@@ -428,8 +473,8 @@ async function installCommand(installOnCI, verbose, dryRun) {
428
473
  console.log(`Dry run: validated ${countsMessage}`);
429
474
  }
430
475
  }
431
- else if (ruleCount === 0 && commandCount === 0) {
432
- console.log("No rules or commands installed");
476
+ else if (ruleCount === 0 && commandCount === 0 && hookCount === 0) {
477
+ console.log("No rules, commands, or hooks installed");
433
478
  }
434
479
  else if (result.packagesCount > 1) {
435
480
  console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);