aicm 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -37,12 +37,12 @@ In your project's `aicm.json`, reference the package and the specific rule:
37
37
  }
38
38
  ```
39
39
 
40
- 2. **Add a postinstall script** to your `package.json`:
40
+ 2. **Add a prepare script** to your `package.json`:
41
41
 
42
42
  ```json
43
43
  {
44
44
  "scripts": {
45
- "postinstall": "npx -y aicm install"
45
+ "prepare": "npx -y aicm install"
46
46
  }
47
47
  }
48
48
  ```
@@ -108,6 +108,8 @@ async function install(options = {}) {
108
108
  const ruleType = (0, rule_detector_1.detectRuleType)(source);
109
109
  // Get the base path of the preset file if this rule came from a preset
110
110
  const ruleBasePath = (0, config_1.getRuleSource)(config, name);
111
+ // Get the original preset path for namespacing
112
+ const originalPresetPath = (0, config_1.getOriginalPresetPath)(config, name);
111
113
  // Collect the rule based on its type
112
114
  try {
113
115
  let ruleContent;
@@ -122,6 +124,10 @@ async function install(options = {}) {
122
124
  errorMessages.push(`Unknown rule type: ${ruleType}`);
123
125
  continue;
124
126
  }
127
+ // Add the preset path to the rule content for namespacing
128
+ if (originalPresetPath) {
129
+ ruleContent.presetPath = originalPresetPath;
130
+ }
125
131
  (0, rule_collector_1.addRuleToCollection)(ruleCollection, ruleContent, config.ides);
126
132
  installedRuleCount++;
127
133
  }
@@ -35,6 +35,7 @@ export interface RuleContent {
35
35
  content: string;
36
36
  metadata: RuleMetadata;
37
37
  sourcePath: string;
38
+ presetPath?: string;
38
39
  }
39
40
  export interface RuleCollection {
40
41
  cursor: RuleContent[];
@@ -1,20 +1,30 @@
1
1
  import { Config, Rules } from "../types";
2
- interface ConfigWithMeta extends Config {
3
- __ruleSources?: Record<string, string>;
2
+ export interface RuleMetadata {
3
+ ruleSources: Record<string, string>;
4
+ originalPresetPaths: Record<string, string>;
4
5
  }
5
- export declare function getFullPresetPath(presetPath: string): string | null;
6
+ export interface ConfigResult {
7
+ config: Config;
8
+ metadata: RuleMetadata;
9
+ }
10
+ export interface PresetPathInfo {
11
+ fullPath: string;
12
+ originalPath: string;
13
+ }
14
+ export declare function getFullPresetPath(presetPath: string): PresetPathInfo | null;
6
15
  /**
7
- * Load a preset file and return its rules and mcpServers
16
+ * Load a preset file and return its contents
8
17
  */
9
18
  export declare function loadPreset(presetPath: string): {
10
19
  rules: Rules;
11
20
  mcpServers?: import("../types").MCPServers;
21
+ presets?: string[];
12
22
  } | null;
13
23
  /**
14
24
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
15
25
  * Returns the config object or null if not found.
16
26
  */
17
- export declare function loadAicmConfigCosmiconfig(): ConfigWithMeta | null;
27
+ export declare function loadAicmConfigCosmiconfig(): Config | null;
18
28
  /**
19
29
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
20
30
  */
@@ -23,8 +33,11 @@ export declare function getConfig(): Config | null;
23
33
  * Get the source preset path for a rule if it came from a preset
24
34
  */
25
35
  export declare function getRuleSource(config: Config, ruleName: string): string | undefined;
36
+ /**
37
+ * Get the original preset path for a rule if it came from a preset
38
+ */
39
+ export declare function getOriginalPresetPath(config: Config, ruleName: string): string | undefined;
26
40
  /**
27
41
  * Save the configuration to the aicm.json file
28
42
  */
29
43
  export declare function saveConfig(config: Config): boolean;
30
- export {};
@@ -8,43 +8,66 @@ exports.loadPreset = loadPreset;
8
8
  exports.loadAicmConfigCosmiconfig = loadAicmConfigCosmiconfig;
9
9
  exports.getConfig = getConfig;
10
10
  exports.getRuleSource = getRuleSource;
11
+ exports.getOriginalPresetPath = getOriginalPresetPath;
11
12
  exports.saveConfig = saveConfig;
12
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const cosmiconfig_1 = require("cosmiconfig");
15
16
  const CONFIG_FILE = "aicm.json";
16
17
  function getFullPresetPath(presetPath) {
18
+ // If it's a local file with .json extension and exists, return as is
17
19
  if (presetPath.endsWith(".json") && fs_extra_1.default.pathExistsSync(presetPath)) {
18
- return presetPath;
20
+ return { fullPath: presetPath, originalPath: presetPath };
19
21
  }
20
22
  try {
21
23
  let absolutePresetPath;
24
+ // Handle npm package with explicit JSON path
22
25
  if (presetPath.endsWith(".json")) {
23
26
  absolutePresetPath = require.resolve(presetPath, {
24
27
  paths: [__dirname, process.cwd()],
25
28
  });
26
29
  }
30
+ // Handle npm package without explicit JSON path (add aicm.json)
27
31
  else {
32
+ // For npm packages, ensure we properly handle scoped packages (@org/pkg)
28
33
  const presetPathWithConfig = node_path_1.default.join(presetPath, "aicm.json");
29
- absolutePresetPath = require.resolve(presetPathWithConfig, {
30
- paths: [__dirname, process.cwd()],
31
- });
34
+ try {
35
+ absolutePresetPath = require.resolve(presetPathWithConfig, {
36
+ paths: [__dirname, process.cwd()],
37
+ });
38
+ }
39
+ catch (_a) {
40
+ // If direct resolution fails, try as a package name
41
+ absolutePresetPath = require.resolve(presetPath, {
42
+ paths: [__dirname, process.cwd()],
43
+ });
44
+ // If we found the package but not the config file, look for aicm.json
45
+ if (fs_extra_1.default.existsSync(absolutePresetPath)) {
46
+ const packageDir = node_path_1.default.dirname(absolutePresetPath);
47
+ const configPath = node_path_1.default.join(packageDir, "aicm.json");
48
+ if (fs_extra_1.default.existsSync(configPath)) {
49
+ absolutePresetPath = configPath;
50
+ }
51
+ }
52
+ }
32
53
  }
33
- return fs_extra_1.default.existsSync(absolutePresetPath) ? absolutePresetPath : null;
54
+ return fs_extra_1.default.existsSync(absolutePresetPath)
55
+ ? { fullPath: absolutePresetPath, originalPath: presetPath }
56
+ : null;
34
57
  }
35
- catch (_a) {
58
+ catch (_b) {
36
59
  return null;
37
60
  }
38
61
  }
39
62
  /**
40
- * Load a preset file and return its rules and mcpServers
63
+ * Load a preset file and return its contents
41
64
  */
42
65
  function loadPreset(presetPath) {
43
- const fullPresetPath = getFullPresetPath(presetPath);
44
- if (!fullPresetPath) {
66
+ const pathInfo = getFullPresetPath(presetPath);
67
+ if (!pathInfo) {
45
68
  throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
46
69
  }
47
- const presetContent = fs_extra_1.default.readFileSync(fullPresetPath, "utf8");
70
+ const presetContent = fs_extra_1.default.readFileSync(pathInfo.fullPath, "utf8");
48
71
  let preset;
49
72
  try {
50
73
  preset = JSON.parse(presetContent);
@@ -56,67 +79,122 @@ function loadPreset(presetPath) {
56
79
  if (!preset.rules || typeof preset.rules !== "object") {
57
80
  throw new Error(`Error loading preset: Invalid format in ${presetPath} - missing or invalid 'rules' object`);
58
81
  }
59
- return { rules: preset.rules, mcpServers: preset.mcpServers };
82
+ return {
83
+ rules: preset.rules,
84
+ mcpServers: preset.mcpServers,
85
+ presets: preset.presets,
86
+ };
60
87
  }
88
+ // Global metadata storage
89
+ let currentMetadata = null;
90
+ // Track processed presets to avoid circular references
91
+ const processedPresets = new Set();
61
92
  /**
62
- * Process presets and merge their rules and mcpServers into the config
93
+ * Process presets and return a new config with merged rules and metadata
63
94
  */
64
95
  function processPresets(config) {
96
+ // Create a deep copy of the config to avoid mutations
97
+ const newConfig = JSON.parse(JSON.stringify(config));
98
+ const metadata = {
99
+ ruleSources: {},
100
+ originalPresetPaths: {},
101
+ };
102
+ // Clear processed presets tracking set when starting from the top level
103
+ processedPresets.clear();
104
+ return processPresetsInternal(newConfig, metadata);
105
+ }
106
+ /**
107
+ * Internal function to process presets recursively
108
+ */
109
+ function processPresetsInternal(config, metadata) {
65
110
  if (!config.presets || !Array.isArray(config.presets)) {
66
- return;
111
+ return { config, metadata };
67
112
  }
68
113
  for (const presetPath of config.presets) {
114
+ const pathInfo = getFullPresetPath(presetPath);
115
+ if (!pathInfo) {
116
+ throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
117
+ }
118
+ // Skip if we've already processed this preset (prevents circular references)
119
+ if (processedPresets.has(pathInfo.fullPath)) {
120
+ console.warn(`Skipping already processed preset: ${presetPath}`);
121
+ continue;
122
+ }
123
+ // Mark this preset as processed
124
+ processedPresets.add(pathInfo.fullPath);
69
125
  const preset = loadPreset(presetPath);
70
126
  if (!preset)
71
127
  continue;
72
- const fullPresetPath = getFullPresetPath(presetPath);
73
- if (!fullPresetPath)
74
- continue;
75
- mergePresetRules(config, preset.rules, fullPresetPath);
128
+ // Process nested presets first (depth-first)
129
+ if (preset.presets && preset.presets.length > 0) {
130
+ // Create a temporary config with just the presets from this preset
131
+ const presetConfig = {
132
+ rules: {},
133
+ presets: preset.presets,
134
+ ides: [],
135
+ };
136
+ // Recursively process the nested presets
137
+ const { config: nestedConfig } = processPresetsInternal(presetConfig, metadata);
138
+ Object.assign(preset.rules, nestedConfig.rules);
139
+ }
140
+ const { updatedConfig, updatedMetadata } = mergePresetRules(config, preset.rules, pathInfo, metadata);
141
+ Object.assign(config.rules, updatedConfig.rules);
142
+ Object.assign(metadata.ruleSources, updatedMetadata.ruleSources);
143
+ Object.assign(metadata.originalPresetPaths, updatedMetadata.originalPresetPaths);
76
144
  if (preset.mcpServers) {
77
- mergePresetMcpServers(config, preset.mcpServers);
145
+ config.mcpServers = mergePresetMcpServers(config.mcpServers || {}, preset.mcpServers);
78
146
  }
79
147
  }
148
+ return { config, metadata };
80
149
  }
81
150
  /**
82
- * Merge preset rules into the config
151
+ * Merge preset rules into the config without mutation
83
152
  */
84
- function mergePresetRules(config, presetRules, presetPath) {
153
+ function mergePresetRules(config, presetRules, pathInfo, metadata) {
154
+ const updatedRules = { ...config.rules };
155
+ const updatedMetadata = {
156
+ ruleSources: { ...metadata.ruleSources },
157
+ originalPresetPaths: { ...metadata.originalPresetPaths },
158
+ };
85
159
  for (const [ruleName, rulePath] of Object.entries(presetRules)) {
86
160
  // Cancel if set to false in config
87
161
  if (Object.prototype.hasOwnProperty.call(config.rules, ruleName) &&
88
162
  config.rules[ruleName] === false) {
89
- delete config.rules[ruleName];
90
- if (config.__ruleSources)
91
- delete config.__ruleSources[ruleName];
163
+ delete updatedRules[ruleName];
164
+ delete updatedMetadata.ruleSources[ruleName];
165
+ delete updatedMetadata.originalPresetPaths[ruleName];
92
166
  continue;
93
167
  }
94
168
  // Only add if not already defined in config (override handled by config)
95
169
  if (!Object.prototype.hasOwnProperty.call(config.rules, ruleName)) {
96
- config.rules[ruleName] = rulePath;
97
- config.__ruleSources = config.__ruleSources || {};
98
- config.__ruleSources[ruleName] = presetPath;
170
+ updatedRules[ruleName] = rulePath;
171
+ updatedMetadata.ruleSources[ruleName] = pathInfo.fullPath;
172
+ updatedMetadata.originalPresetPaths[ruleName] = pathInfo.originalPath;
99
173
  }
100
174
  }
175
+ return {
176
+ updatedConfig: { ...config, rules: updatedRules },
177
+ updatedMetadata,
178
+ };
101
179
  }
102
180
  /**
103
- * Merge preset mcpServers into the config
181
+ * Merge preset mcpServers without mutation
104
182
  */
105
- function mergePresetMcpServers(config, presetMcpServers) {
106
- if (!config.mcpServers)
107
- config.mcpServers = {};
183
+ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
184
+ const newMcpServers = { ...configMcpServers };
108
185
  for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) {
109
186
  // Cancel if set to false in config
110
- if (Object.prototype.hasOwnProperty.call(config.mcpServers, serverName) &&
111
- config.mcpServers[serverName] === false) {
112
- delete config.mcpServers[serverName];
187
+ if (Object.prototype.hasOwnProperty.call(newMcpServers, serverName) &&
188
+ newMcpServers[serverName] === false) {
189
+ delete newMcpServers[serverName];
113
190
  continue;
114
191
  }
115
192
  // Only add if not already defined in config (override handled by config)
116
- if (!Object.prototype.hasOwnProperty.call(config.mcpServers, serverName)) {
117
- config.mcpServers[serverName] = serverConfig;
193
+ if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
194
+ newMcpServers[serverName] = serverConfig;
118
195
  }
119
196
  }
197
+ return newMcpServers;
120
198
  }
121
199
  /**
122
200
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
@@ -149,15 +227,24 @@ function getConfig() {
149
227
  if (!config) {
150
228
  throw new Error(`No config found in ${process.cwd()}, create one using "aicm init"`);
151
229
  }
152
- processPresets(config);
153
- return config;
230
+ const { config: processedConfig, metadata } = processPresets(config);
231
+ // Store metadata for later access
232
+ currentMetadata = metadata;
233
+ return processedConfig;
154
234
  }
155
235
  /**
156
236
  * Get the source preset path for a rule if it came from a preset
157
237
  */
158
238
  function getRuleSource(config, ruleName) {
159
239
  var _a;
160
- return (_a = config.__ruleSources) === null || _a === void 0 ? void 0 : _a[ruleName];
240
+ return (_a = currentMetadata === null || currentMetadata === void 0 ? void 0 : currentMetadata.ruleSources) === null || _a === void 0 ? void 0 : _a[ruleName];
241
+ }
242
+ /**
243
+ * Get the original preset path for a rule if it came from a preset
244
+ */
245
+ function getOriginalPresetPath(config, ruleName) {
246
+ var _a;
247
+ return (_a = currentMetadata === null || currentMetadata === void 0 ? void 0 : currentMetadata.originalPresetPaths) === null || _a === void 0 ? void 0 : _a[ruleName];
161
248
  }
162
249
  /**
163
250
  * Save the configuration to the aicm.json file
@@ -23,6 +23,21 @@ function writeRulesToTargets(collection) {
23
23
  writeWindsurfRulesFromCollection(collection.windsurf, idePaths.windsurf);
24
24
  }
25
25
  }
26
+ /**
27
+ * Extract a normalized namespace from a preset path
28
+ * @param presetPath The original preset path
29
+ * @returns An array of path segments to use for namespacing
30
+ */
31
+ function extractNamespaceFromPresetPath(presetPath) {
32
+ // Special case: npm package names always use forward slashes, regardless of platform
33
+ if (presetPath.startsWith("@")) {
34
+ // For scoped packages like @scope/package/subdir, create nested directories
35
+ return presetPath.split("/");
36
+ }
37
+ // Handle both Unix and Windows style path separators
38
+ const parts = presetPath.split(/[/\\]/);
39
+ return parts.filter((part) => part.length > 0); // Filter out empty segments
40
+ }
26
41
  /**
27
42
  * Write rules to Cursor's rules directory
28
43
  * @param rules The rules to write
@@ -31,7 +46,20 @@ function writeRulesToTargets(collection) {
31
46
  function writeCursorRules(rules, cursorRulesDir) {
32
47
  fs_extra_1.default.emptyDirSync(cursorRulesDir);
33
48
  for (const rule of rules) {
34
- const ruleFile = node_path_1.default.join(cursorRulesDir, ...rule.name.split("/")) + ".mdc";
49
+ let rulePath;
50
+ // Parse rule name into path segments using platform-specific path separator
51
+ const ruleNameParts = rule.name.split(/[/\\]/).filter(Boolean);
52
+ if (rule.presetPath) {
53
+ // For rules from presets, create a namespaced directory structure
54
+ const namespace = extractNamespaceFromPresetPath(rule.presetPath);
55
+ // Path will be: cursorRulesDir/namespace/rule-name.mdc
56
+ rulePath = node_path_1.default.join(cursorRulesDir, ...namespace, ...ruleNameParts);
57
+ }
58
+ else {
59
+ // For local rules, maintain the original flat structure
60
+ rulePath = node_path_1.default.join(cursorRulesDir, ...ruleNameParts);
61
+ }
62
+ const ruleFile = rulePath + ".mdc";
35
63
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
36
64
  if (fs_extra_1.default.existsSync(rule.sourcePath)) {
37
65
  fs_extra_1.default.copyFileSync(rule.sourcePath, ruleFile);
@@ -49,11 +77,33 @@ function writeCursorRules(rules, cursorRulesDir) {
49
77
  function writeWindsurfRulesFromCollection(rules, ruleDir) {
50
78
  fs_extra_1.default.emptyDirSync(ruleDir);
51
79
  const ruleFiles = rules.map((rule) => {
52
- const physicalRulePath = node_path_1.default.join(ruleDir, ...rule.name.split("/")) + ".md";
80
+ let rulePath;
81
+ // Parse rule name into path segments using platform-specific path separator
82
+ const ruleNameParts = rule.name.split(/[/\\]/).filter(Boolean);
83
+ if (rule.presetPath) {
84
+ // For rules from presets, create a namespaced directory structure
85
+ const namespace = extractNamespaceFromPresetPath(rule.presetPath);
86
+ // Path will be: ruleDir/namespace/rule-name.md
87
+ rulePath = node_path_1.default.join(ruleDir, ...namespace, ...ruleNameParts);
88
+ }
89
+ else {
90
+ // For local rules, maintain the original flat structure
91
+ rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
92
+ }
93
+ const physicalRulePath = rulePath + ".md";
53
94
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
54
95
  fs_extra_1.default.writeFileSync(physicalRulePath, rule.content);
55
96
  const relativeRuleDir = node_path_1.default.basename(ruleDir); // Gets '.rules'
56
- const windsurfPath = node_path_1.default.join(relativeRuleDir, ...rule.name.split("/")) + ".md";
97
+ // For the Windsurf rules file, we need to maintain the same structure
98
+ let windsurfPath;
99
+ if (rule.presetPath) {
100
+ const namespace = extractNamespaceFromPresetPath(rule.presetPath);
101
+ windsurfPath =
102
+ node_path_1.default.join(relativeRuleDir, ...namespace, ...ruleNameParts) + ".md";
103
+ }
104
+ else {
105
+ windsurfPath = node_path_1.default.join(relativeRuleDir, ...ruleNameParts) + ".md";
106
+ }
57
107
  // Normalize to POSIX style for cross-platform compatibility in .windsurfrules
58
108
  const windsurfPathPosix = windsurfPath.replace(/\\/g, "/");
59
109
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "A TypeScript CLI tool for managing AI IDE rules across different projects and teams",
5
5
  "main": "dist/api.js",
6
6
  "types": "dist/api.d.ts",