aicm 0.9.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.
@@ -1,7 +1,11 @@
1
1
  import { Config, Rules } from "../types";
2
- interface ConfigWithMeta extends Config {
3
- __ruleSources?: Record<string, string>;
4
- __originalPresetPaths?: Record<string, string>;
2
+ export interface RuleMetadata {
3
+ ruleSources: Record<string, string>;
4
+ originalPresetPaths: Record<string, string>;
5
+ }
6
+ export interface ConfigResult {
7
+ config: Config;
8
+ metadata: RuleMetadata;
5
9
  }
6
10
  export interface PresetPathInfo {
7
11
  fullPath: string;
@@ -9,17 +13,18 @@ export interface PresetPathInfo {
9
13
  }
10
14
  export declare function getFullPresetPath(presetPath: string): PresetPathInfo | null;
11
15
  /**
12
- * Load a preset file and return its rules and mcpServers
16
+ * Load a preset file and return its contents
13
17
  */
14
18
  export declare function loadPreset(presetPath: string): {
15
19
  rules: Rules;
16
20
  mcpServers?: import("../types").MCPServers;
21
+ presets?: string[];
17
22
  } | null;
18
23
  /**
19
24
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
20
25
  * Returns the config object or null if not found.
21
26
  */
22
- export declare function loadAicmConfigCosmiconfig(): ConfigWithMeta | null;
27
+ export declare function loadAicmConfigCosmiconfig(): Config | null;
23
28
  /**
24
29
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
25
30
  */
@@ -36,4 +41,3 @@ export declare function getOriginalPresetPath(config: Config, ruleName: string):
36
41
  * Save the configuration to the aicm.json file
37
42
  */
38
43
  export declare function saveConfig(config: Config): boolean;
39
- export {};
@@ -15,32 +15,52 @@ const node_path_1 = __importDefault(require("node:path"));
15
15
  const cosmiconfig_1 = require("cosmiconfig");
16
16
  const CONFIG_FILE = "aicm.json";
17
17
  function getFullPresetPath(presetPath) {
18
+ // If it's a local file with .json extension and exists, return as is
18
19
  if (presetPath.endsWith(".json") && fs_extra_1.default.pathExistsSync(presetPath)) {
19
20
  return { fullPath: presetPath, originalPath: presetPath };
20
21
  }
21
22
  try {
22
23
  let absolutePresetPath;
24
+ // Handle npm package with explicit JSON path
23
25
  if (presetPath.endsWith(".json")) {
24
26
  absolutePresetPath = require.resolve(presetPath, {
25
27
  paths: [__dirname, process.cwd()],
26
28
  });
27
29
  }
30
+ // Handle npm package without explicit JSON path (add aicm.json)
28
31
  else {
32
+ // For npm packages, ensure we properly handle scoped packages (@org/pkg)
29
33
  const presetPathWithConfig = node_path_1.default.join(presetPath, "aicm.json");
30
- absolutePresetPath = require.resolve(presetPathWithConfig, {
31
- paths: [__dirname, process.cwd()],
32
- });
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
+ }
33
53
  }
34
54
  return fs_extra_1.default.existsSync(absolutePresetPath)
35
55
  ? { fullPath: absolutePresetPath, originalPath: presetPath }
36
56
  : null;
37
57
  }
38
- catch (_a) {
58
+ catch (_b) {
39
59
  return null;
40
60
  }
41
61
  }
42
62
  /**
43
- * Load a preset file and return its rules and mcpServers
63
+ * Load a preset file and return its contents
44
64
  */
45
65
  function loadPreset(presetPath) {
46
66
  const pathInfo = getFullPresetPath(presetPath);
@@ -59,71 +79,122 @@ function loadPreset(presetPath) {
59
79
  if (!preset.rules || typeof preset.rules !== "object") {
60
80
  throw new Error(`Error loading preset: Invalid format in ${presetPath} - missing or invalid 'rules' object`);
61
81
  }
62
- return { rules: preset.rules, mcpServers: preset.mcpServers };
82
+ return {
83
+ rules: preset.rules,
84
+ mcpServers: preset.mcpServers,
85
+ presets: preset.presets,
86
+ };
63
87
  }
88
+ // Global metadata storage
89
+ let currentMetadata = null;
90
+ // Track processed presets to avoid circular references
91
+ const processedPresets = new Set();
64
92
  /**
65
- * Process presets and merge their rules and mcpServers into the config
93
+ * Process presets and return a new config with merged rules and metadata
66
94
  */
67
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) {
68
110
  if (!config.presets || !Array.isArray(config.presets)) {
69
- return;
111
+ return { config, metadata };
70
112
  }
71
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);
72
125
  const preset = loadPreset(presetPath);
73
126
  if (!preset)
74
127
  continue;
75
- const pathInfo = getFullPresetPath(presetPath);
76
- if (!pathInfo)
77
- continue;
78
- mergePresetRules(config, preset.rules, pathInfo);
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);
79
144
  if (preset.mcpServers) {
80
- mergePresetMcpServers(config, preset.mcpServers);
145
+ config.mcpServers = mergePresetMcpServers(config.mcpServers || {}, preset.mcpServers);
81
146
  }
82
147
  }
148
+ return { config, metadata };
83
149
  }
84
150
  /**
85
- * Merge preset rules into the config
151
+ * Merge preset rules into the config without mutation
86
152
  */
87
- function mergePresetRules(config, presetRules, pathInfo) {
153
+ function mergePresetRules(config, presetRules, pathInfo, metadata) {
154
+ const updatedRules = { ...config.rules };
155
+ const updatedMetadata = {
156
+ ruleSources: { ...metadata.ruleSources },
157
+ originalPresetPaths: { ...metadata.originalPresetPaths },
158
+ };
88
159
  for (const [ruleName, rulePath] of Object.entries(presetRules)) {
89
160
  // Cancel if set to false in config
90
161
  if (Object.prototype.hasOwnProperty.call(config.rules, ruleName) &&
91
162
  config.rules[ruleName] === false) {
92
- delete config.rules[ruleName];
93
- if (config.__ruleSources)
94
- delete config.__ruleSources[ruleName];
95
- if (config.__originalPresetPaths)
96
- delete config.__originalPresetPaths[ruleName];
163
+ delete updatedRules[ruleName];
164
+ delete updatedMetadata.ruleSources[ruleName];
165
+ delete updatedMetadata.originalPresetPaths[ruleName];
97
166
  continue;
98
167
  }
99
168
  // Only add if not already defined in config (override handled by config)
100
169
  if (!Object.prototype.hasOwnProperty.call(config.rules, ruleName)) {
101
- config.rules[ruleName] = rulePath;
102
- config.__ruleSources = config.__ruleSources || {};
103
- config.__ruleSources[ruleName] = pathInfo.fullPath;
104
- config.__originalPresetPaths = config.__originalPresetPaths || {};
105
- config.__originalPresetPaths[ruleName] = pathInfo.originalPath;
170
+ updatedRules[ruleName] = rulePath;
171
+ updatedMetadata.ruleSources[ruleName] = pathInfo.fullPath;
172
+ updatedMetadata.originalPresetPaths[ruleName] = pathInfo.originalPath;
106
173
  }
107
174
  }
175
+ return {
176
+ updatedConfig: { ...config, rules: updatedRules },
177
+ updatedMetadata,
178
+ };
108
179
  }
109
180
  /**
110
- * Merge preset mcpServers into the config
181
+ * Merge preset mcpServers without mutation
111
182
  */
112
- function mergePresetMcpServers(config, presetMcpServers) {
113
- if (!config.mcpServers)
114
- config.mcpServers = {};
183
+ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
184
+ const newMcpServers = { ...configMcpServers };
115
185
  for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) {
116
186
  // Cancel if set to false in config
117
- if (Object.prototype.hasOwnProperty.call(config.mcpServers, serverName) &&
118
- config.mcpServers[serverName] === false) {
119
- delete config.mcpServers[serverName];
187
+ if (Object.prototype.hasOwnProperty.call(newMcpServers, serverName) &&
188
+ newMcpServers[serverName] === false) {
189
+ delete newMcpServers[serverName];
120
190
  continue;
121
191
  }
122
192
  // Only add if not already defined in config (override handled by config)
123
- if (!Object.prototype.hasOwnProperty.call(config.mcpServers, serverName)) {
124
- config.mcpServers[serverName] = serverConfig;
193
+ if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
194
+ newMcpServers[serverName] = serverConfig;
125
195
  }
126
196
  }
197
+ return newMcpServers;
127
198
  }
128
199
  /**
129
200
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
@@ -156,22 +227,24 @@ function getConfig() {
156
227
  if (!config) {
157
228
  throw new Error(`No config found in ${process.cwd()}, create one using "aicm init"`);
158
229
  }
159
- processPresets(config);
160
- return config;
230
+ const { config: processedConfig, metadata } = processPresets(config);
231
+ // Store metadata for later access
232
+ currentMetadata = metadata;
233
+ return processedConfig;
161
234
  }
162
235
  /**
163
236
  * Get the source preset path for a rule if it came from a preset
164
237
  */
165
238
  function getRuleSource(config, ruleName) {
166
239
  var _a;
167
- 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];
168
241
  }
169
242
  /**
170
243
  * Get the original preset path for a rule if it came from a preset
171
244
  */
172
245
  function getOriginalPresetPath(config, ruleName) {
173
246
  var _a;
174
- return (_a = config.__originalPresetPaths) === null || _a === void 0 ? void 0 : _a[ruleName];
247
+ return (_a = currentMetadata === null || currentMetadata === void 0 ? void 0 : currentMetadata.originalPresetPaths) === null || _a === void 0 ? void 0 : _a[ruleName];
175
248
  }
176
249
  /**
177
250
  * Save the configuration to the aicm.json file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.9.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",