aicm 0.4.1 → 0.6.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.
package/README.md CHANGED
@@ -6,23 +6,13 @@ A CLI tool for syncing and managing Agentic IDE rules across projects
6
6
 
7
7
  ## Why
8
8
 
9
- Development teams struggle with:
9
+ With the rise of Agentic IDEs like cursor and windsurf, we have an opportunity to enforce best practices through rules. However, these rules are typically isolated within individual developers or projects.
10
10
 
11
- - **Inconsistent Practices**: Developers apply varying standards across projects
12
- - **Knowledge Silos**: Best practices remain trapped in individual projects
13
- - **Change Management**: No efficient way to update and distribute new standards
14
-
15
- As developers increasingly adopt AI-powered IDEs like Cursor and Windsurf, we have an opportunity to enforce best practices through rules. However, these rules are typically isolated within individual developers or projects.
16
-
17
- **aicm** is a CLI tool that helps with distribution of agentic IDE configurations, rules and mcps:
18
-
19
- - 🏛️ **Single Source of Truth**: Define, maintain and version-control all AI IDE rules in one central repository
20
- - 📦 **Seamless Distribution**: Automatically synchronize the latest rules to developers' local projects using npm packages
21
- - 🌐 **Cross-IDE Support**: Supports multiple AI-powered IDEs (Cursor, Windsurf)
11
+ **aicm** is a CLI tool for distributing Agentic IDE configurations, rules, and MCPs across projects. It leverages package managers to copy configurations from node_modules to the correct locations in your file system.
22
12
 
23
13
  ## Getting Started
24
14
 
25
- To get automatic rule updates from NPM Packages, you can create, publish, and use dedicated npm packages to distribute AI rules across multiple projects.
15
+ Since aicm is not a package manager, begin by creating an npm package that contains your rules and MCP configurations.
26
16
 
27
17
  Consider the following npm package structure:
28
18
 
@@ -31,8 +21,7 @@ Consider the following npm package structure:
31
21
  ├── package.json
32
22
  └── rules/
33
23
  ├── typescript.mdc
34
- ├── react.mdc
35
- └── general.mdc
24
+ └── react.mdc
36
25
  ```
37
26
 
38
27
  1. **Point to the path within the npm package**
@@ -44,8 +33,7 @@ In your project's `aicm.json`, reference the package and the specific rule:
44
33
  "ides": ["cursor"],
45
34
  "rules": {
46
35
  "typescript": "@myteam/ai-tools/rules/typescript.mdc",
47
- "react": "@myteam/ai-tools/rules/react.mdc",
48
- "general": "@myteam/ai-tools/rules/general.mdc"
36
+ "react": "@myteam/ai-tools/rules/react.mdc"
49
37
  }
50
38
  }
51
39
  ```
@@ -60,55 +48,111 @@ In your project's `aicm.json`, reference the package and the specific rule:
60
48
  }
61
49
  ```
62
50
 
63
- Now the rules will be linked to `.cursor/rules/` when you run `npm install`.
51
+ Now the rules will be linked to `.cursor/rules/aicm/` when you run `npm install`.
64
52
 
65
53
  ### Using Presets
66
54
 
67
- Presets allow you to bundle multiple rules into a single configuration that can be shared across projects.
55
+ Presets allow you to bundle multiple rules & mcps into a single configuration that can be shared across projects.
56
+
57
+ 1. **Create a preset package or directory**
68
58
 
69
- 1. **Create a preset file**
59
+ Create an npm package with your rule definitions in an `aicm.json` file:
70
60
 
71
- Create a JSON file with your rule definitions:
61
+ > `@myteam/ai-tools/aicm.json`
72
62
 
73
63
  ```json
74
64
  {
75
65
  "rules": {
76
66
  "typescript": "./rules/typescript.mdc",
77
67
  "react": "./rules/react.mdc"
68
+ },
69
+ "mcpServers": {
70
+ "my-mcp": {
71
+ "url": "https://example.com/sse"
72
+ }
78
73
  }
79
74
  }
80
75
  ```
81
76
 
82
77
  2. **Reference the preset in your project**
83
78
 
84
- In your project's `aicm.json`, reference the preset:
79
+ In your project's `aicm.json`, reference the preset by its npm package or directory name:
85
80
 
86
81
  ```json
87
82
  {
88
83
  "ides": ["cursor"],
89
- "presets": ["@myteam/ai-tools/my-aicm.json"]
84
+ "presets": ["@myteam/ai-tools"]
90
85
  }
91
86
  ```
92
87
 
93
- When you run `npx aicm install`, all rules from the preset will be installed to `.cursor/rules/`.
88
+ When you run `npx aicm install`, all rules from the preset will be installed to `.cursor/rules/aicm/` and all mcps from the preset will be installed to `.cursor/mcp.json`.
89
+
90
+ ### Notes
91
+
92
+ - Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
93
+ - Users may add `.cursor/rules/aicm/` and `.aicm/` (for Windsurf) to their `.gitignore` if they do not want to track generated rules.
94
+
95
+ ### Overriding and Disabling Rules and MCP Servers from Presets
96
+
97
+ When you use a preset, you can override or disable any rule or mcpServer from the preset in your own `aicm.json` configuration:
98
+
99
+ - **Override**: To override a rule or mcpServer, specify the same key in your config with a new value. The value in your config will take precedence over the preset.
100
+ - **Disable**: To disable a rule or mcpServer from a preset, set its value to `false` in your config.
101
+
102
+ **Example:**
103
+
104
+ ```json
105
+ {
106
+ "ides": ["cursor"],
107
+ "presets": ["@company/ai-rules/aicm.json"],
108
+ "rules": {
109
+ "rule-from-preset-a": "./rules/override-rule.mdc",
110
+ "rule-from-preset-b": false
111
+ },
112
+ "mcpServers": {
113
+ "mcp-from-preset-a": {
114
+ "command": "./scripts/override-mcp.sh",
115
+ "env": { "MCP_TOKEN": "override" }
116
+ },
117
+ "mcp-from-preset-b": false
118
+ }
119
+ }
120
+ ```
94
121
 
95
122
  ### Demo
96
123
 
97
- Here is a package to demonstrate how aicm works:
124
+ We'll install an npm package containing a simple rule to demonstrate how aicm works.
125
+
126
+ 1. Install an npm package containing a rule
98
127
 
99
128
  ```bash
100
- # Install a package containing a rule
101
129
  npm install --save-dev pirate-coding-rule
130
+ ```
102
131
 
103
- # Install the rule via the aicm CLI
104
- npx -y aicm install pirate-coding pirate-coding-rule/rule.mdc
132
+ 2. Initialize aicm config
133
+
134
+ ```bash
135
+ npx -y aicm init
136
+ ```
137
+
138
+ 3. Add the rule to your config file: `aicm.json`
139
+
140
+ ```json
141
+ {
142
+ "ides": ["cursor"],
143
+ "rules": {
144
+ "pirate-coding": "pirate-coding-rule/rule.mdc"
145
+ }
146
+ }
105
147
  ```
106
148
 
107
- This command will:
149
+ 4. Install all rules from your configuration
150
+
151
+ ```bash
152
+ npx -y aicm install
153
+ ```
108
154
 
109
- 1. Create a `aicm.json` file if it doesn't exist
110
- 2. Add the rule to the configuration
111
- 3. Install the rule to `.cursor/rules/`
155
+ This command installs all configured rules and MCPs to their IDE-specific locations.
112
156
 
113
157
  After installation, open Cursor and ask it to do something. Your AI assistant will respond with pirate-themed coding advice.
114
158
 
@@ -130,11 +174,11 @@ Example `aicm.json`:
130
174
  "ides": ["cursor"],
131
175
  "presets": ["@my-team/ai-tools/my-aicm.json"],
132
176
  "rules": {
133
- "team-standards": "@my-team/ai-tools/rules/team-standards.mdc"
177
+ "team-rules/team-standards": "@my-team/ai-tools/rules/team-standards.mdc"
134
178
  },
135
179
  "mcpServers": {
136
180
  "remote-mcp": {
137
- "url": "https://example.com/mcp-config.json"
181
+ "url": "https://example.com/sse"
138
182
  }
139
183
  }
140
184
  }
@@ -146,7 +190,7 @@ Example `aicm.json`:
146
190
 
147
191
  - **rules**: Object containing rule configurations
148
192
 
149
- - **rule-name**: A unique identifier for the rule
193
+ - **rule-name**: A unique identifier for the rule. Can include a directory path to install the rule to a specific directory.
150
194
  - **source-location**: Location of the rule file (path within an npm package or local path)
151
195
 
152
196
  - **mcpServers**: Object containing MCP server configurations. Each key is a unique server name, and the value is an object with either:
@@ -162,7 +206,6 @@ Example `aicm.json`:
162
206
 
163
207
  - **Cursor**: MCP server configs are written to `.cursor/mcp.json` (see Cursor docs for latest path).
164
208
  - **Windsurf**: Windsurf does not support project mcpServers. MCP server configuration is not installed for Windsurf projects.
165
- - All installations are per-project.
166
209
 
167
210
  ### Rule Source Types
168
211
 
@@ -186,8 +229,8 @@ Rules stored locally in your project or filesystem. Any path containing slashes
186
229
 
187
230
  ## Supported IDEs
188
231
 
189
- - **Cursor**: Rules are installed as individual `.mdc` files in the Cursor rules directory (`.cursor/rules/`), mcp servers are installed to `.cursor/mcp.json`
190
- - **Windsurf**: Rules are installed in the `.rules` directory which should be added to your `.gitignore` file. Our approach for Windsurf is to create links from the `.windsurfrules` file to the respective rules in the `.rules` directory. There is no support for local mcp servers at the moment.
232
+ - **Cursor**: Rules are installed as individual `.mdc` files in the Cursor rules directory (`.cursor/rules/aicm/`), mcp servers are installed to `.cursor/mcp.json`
233
+ - **Windsurf**: Rules are installed in the `.aicm` directory which should be added to your `.gitignore` file. Our approach for Windsurf is to create links from the `.windsurfrules` file to the respective rules in the `.aicm` directory. There is no support for local mcp servers at the moment.
191
234
 
192
235
  ## Commands
193
236
 
@@ -211,22 +254,7 @@ npx aicm init
211
254
  Installs rules from your configuration to the appropriate IDE locations.
212
255
 
213
256
  ```bash
214
- npx aicm install [rule-name] [rule-source]
215
- ```
216
-
217
- **Options:**
218
-
219
- - `[rule-name]`: Optional - Name of a specific rule to install instead of all rules
220
- - `[rule-source]`: Optional - Source of the rule (npm package or local path)
221
-
222
- **Examples:**
223
-
224
- ```bash
225
- # Install all configured rules
226
- npx -y aicm install
227
-
228
- # Install a rule from an npm package and update configuration
229
- npx -y aicm install react-best-practices @my-team/ai-tools/react-best-practices.mdc
257
+ npx aicm install
230
258
  ```
231
259
 
232
260
  ## Contributing
@@ -237,8 +265,6 @@ Contributions are welcome! Please feel free to submit a Pull Request.
237
265
 
238
266
  ### Testing
239
267
 
240
- The project includes both unit tests and end-to-end (E2E) tests:
241
-
242
268
  ```bash
243
269
  # Run all tests
244
270
  npm test
@@ -5,19 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installCommand = installCommand;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const arg_1 = __importDefault(require("arg"));
9
8
  const config_1 = require("../utils/config");
10
9
  const rule_detector_1 = require("../utils/rule-detector");
11
10
  const rule_collector_1 = require("../utils/rule-collector");
12
11
  const rule_writer_1 = require("../utils/rule-writer");
13
12
  const fs_extra_1 = __importDefault(require("fs-extra"));
14
13
  const node_path_1 = __importDefault(require("node:path"));
15
- // Default configuration
16
- const defaultConfig = {
17
- ides: ["cursor", "windsurf"],
18
- rules: {},
19
- presets: [],
20
- };
21
14
  function writeMcpServersToTargets(mcpServers, ides) {
22
15
  if (!mcpServers)
23
16
  return;
@@ -34,74 +27,22 @@ function writeMcpServersToTargets(mcpServers, ides) {
34
27
  }
35
28
  }
36
29
  async function installCommand() {
37
- // Parse command-specific arguments
38
- const args = (0, arg_1.default)({
39
- // Removed flags as we'll infer the type from the source
40
- }, {
41
- permissive: true,
42
- argv: process.argv.slice(3), // Skip the first two args and the command name
43
- });
44
- // Get rule name and source if provided
45
- const ruleName = args._.length > 0 ? args._[0] : null;
46
- const ruleSource = args._.length > 1 ? args._[1] : null;
47
30
  try {
48
31
  // Initialize rule collection
49
32
  const ruleCollection = (0, rule_collector_1.initRuleCollection)();
50
- // If a rule name and source are provided, install directly
51
- if (ruleName && ruleSource) {
52
- // Detect rule type from the source string
53
- const ruleType = (0, rule_detector_1.detectRuleType)(ruleSource);
54
- // Create config file if it doesn't exist
55
- let config = (0, config_1.getConfig)();
56
- if (!config) {
57
- config = { ...defaultConfig };
58
- console.log(chalk_1.default.blue("Configuration file not found. Creating a new one..."));
59
- }
60
- // Add the rule to the config
61
- config.rules[ruleName] = ruleSource;
62
- // Save the updated config
63
- (0, config_1.saveConfig)(config);
64
- console.log(chalk_1.default.green("Configuration updated successfully!"));
65
- // Collect the rule based on its type
66
- let ruleContent;
67
- switch (ruleType) {
68
- case "npm":
69
- ruleContent = (0, rule_collector_1.collectNpmRule)(ruleName, ruleSource);
70
- break;
71
- case "local":
72
- ruleContent = (0, rule_collector_1.collectLocalRule)(ruleName, ruleSource);
73
- break;
74
- default:
75
- console.log(chalk_1.default.yellow(`Unknown rule type: ${ruleType}`));
76
- return;
77
- }
78
- // Add rule to collection
79
- (0, rule_collector_1.addRuleToCollection)(ruleCollection, ruleContent, config.ides);
80
- // Write rules to targets
81
- (0, rule_writer_1.writeRulesToTargets)(ruleCollection);
82
- // Write mcpServers config to IDE targets
83
- writeMcpServersToTargets(config.mcpServers, config.ides);
84
- console.log(chalk_1.default.green("\nRules installation completed!"));
85
- return;
86
- }
87
33
  // Load configuration
88
- let config = (0, config_1.getConfig)();
89
- // If config doesn't exist, create a new one
34
+ const config = (0, config_1.getConfig)();
35
+ // If config doesn't exist, print error and exit
90
36
  if (!config) {
91
- config = { ...defaultConfig };
92
- console.log(chalk_1.default.blue("Configuration file not found. Creating a new one..."));
93
- (0, config_1.saveConfig)(config);
94
- console.log(chalk_1.default.green("Empty configuration file created successfully!"));
95
- console.log(chalk_1.default.yellow("No rules defined in configuration."));
96
- console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules or use the direct install command: npx aicm install <rule-name> <rule-source>`);
97
- return;
37
+ console.error(chalk_1.default.red("Configuration file not found! Please run 'npx aicm init' to create one."));
38
+ process.exit(1);
98
39
  }
99
40
  // Check if rules are defined (either directly or through presets)
100
41
  if (!config.rules || Object.keys(config.rules).length === 0) {
101
42
  // If there are no presets defined either, show a message
102
43
  if (!config.presets || config.presets.length === 0) {
103
44
  console.log(chalk_1.default.yellow("No rules defined in configuration."));
104
- console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules or use the direct install command: npx aicm install <rule-name> <rule-source>`);
45
+ console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules.`);
105
46
  return;
106
47
  }
107
48
  }
@@ -114,13 +55,11 @@ async function installCommand() {
114
55
  return;
115
56
  }
116
57
  }
117
- // Process each rule (or just the specific one if provided)
58
+ // Process each rule
118
59
  let hasErrors = false;
119
60
  for (const [name, source] of Object.entries(config.rules)) {
120
- // Skip if a specific rule was requested and this isn't it
121
- if (ruleName && name !== ruleName) {
122
- continue;
123
- }
61
+ if (source === false)
62
+ continue; // skip canceled rules
124
63
  // Detect rule type from the source string
125
64
  const ruleType = (0, rule_detector_1.detectRuleType)(source);
126
65
  // Get the base path of the preset file if this rule came from a preset
@@ -145,25 +84,20 @@ async function installCommand() {
145
84
  catch (error) {
146
85
  hasErrors = true;
147
86
  console.error(chalk_1.default.red(`Error processing rule ${name}: ${error instanceof Error ? error.message : String(error)}`));
148
- // If a specific rule was requested and it failed, exit immediately
149
- if (ruleName) {
150
- throw error;
151
- }
152
87
  }
153
88
  }
154
- // If there were errors and we're not processing a specific rule, exit with error
155
- if (hasErrors && !ruleName) {
89
+ // If there were errors, exit with error
90
+ if (hasErrors) {
156
91
  throw new Error("One or more rules failed to process");
157
92
  }
158
- // If a specific rule was requested but not found
159
- if (ruleName && !Object.keys(config.rules).includes(ruleName)) {
160
- console.log(chalk_1.default.yellow(`Rule "${ruleName}" not found in configuration.`));
161
- return;
162
- }
163
93
  // Write all collected rules to their targets
164
94
  (0, rule_writer_1.writeRulesToTargets)(ruleCollection);
165
95
  // Write mcpServers config to IDE targets
166
- writeMcpServersToTargets(config.mcpServers, config.ides);
96
+ if (config.mcpServers) {
97
+ // Filter out canceled servers
98
+ const filteredMcpServers = Object.fromEntries(Object.entries(config.mcpServers).filter(([, v]) => v !== false));
99
+ writeMcpServersToTargets(filteredMcpServers, config.ides);
100
+ }
167
101
  console.log(chalk_1.default.green("\nRules installation completed!"));
168
102
  }
169
103
  catch (error) {
@@ -23,6 +23,8 @@ function listCommand() {
23
23
  console.log(chalk_1.default.blue("Configured Rules:"));
24
24
  console.log(chalk_1.default.dim("─".repeat(50)));
25
25
  for (const [ruleName, source] of Object.entries(config.rules)) {
26
+ if (source === false)
27
+ continue; // skip canceled rules
26
28
  const ruleType = (0, rule_detector_1.detectRuleType)(source);
27
29
  const status = (0, rule_status_1.checkRuleStatus)(ruleName, ruleType, config.ides);
28
30
  const statusColor = status
@@ -1,4 +1,4 @@
1
- export type Rule = string;
1
+ export type Rule = string | false;
2
2
  export interface Rules {
3
3
  [ruleName: string]: Rule;
4
4
  }
@@ -12,7 +12,7 @@ export type MCPServer = {
12
12
  env?: Record<string, string>;
13
13
  command?: never;
14
14
  args?: never;
15
- };
15
+ } | false;
16
16
  export interface MCPServers {
17
17
  [serverName: string]: MCPServer;
18
18
  }
@@ -23,14 +23,24 @@ function getFullPresetPath(presetPath) {
23
23
  let fullPresetPath = presetPath;
24
24
  if (ruleType === "npm") {
25
25
  try {
26
+ // Try to resolve as a file first
26
27
  fullPresetPath = require.resolve(presetPath, {
27
28
  paths: [process.cwd()],
28
29
  });
29
30
  }
30
31
  catch (_a) {
32
+ // If not a file, check if it's a directory in node_modules
31
33
  const directPath = node_path_1.default.join(process.cwd(), "node_modules", presetPath);
32
34
  if (fs_extra_1.default.existsSync(directPath)) {
33
- fullPresetPath = directPath;
35
+ // If it's a directory, look for aicm.json inside
36
+ const aicmJsonPath = node_path_1.default.join(directPath, "aicm.json");
37
+ if (fs_extra_1.default.existsSync(aicmJsonPath)) {
38
+ fullPresetPath = aicmJsonPath;
39
+ }
40
+ else {
41
+ // If aicm.json doesn't exist, treat the directory as invalid
42
+ return null;
43
+ }
34
44
  }
35
45
  else {
36
46
  return null;
@@ -93,12 +103,18 @@ function processPresets(config) {
93
103
  * Merge preset rules into the config
94
104
  */
95
105
  function mergePresetRules(config, presetRules, presetPath) {
96
- // Add preset rules, but don't override existing rules
97
106
  for (const [ruleName, rulePath] of Object.entries(presetRules)) {
98
- // Only add if not already defined in config
99
- if (!config.rules[ruleName]) {
107
+ // Cancel if set to false in config
108
+ if (Object.prototype.hasOwnProperty.call(config.rules, ruleName) &&
109
+ config.rules[ruleName] === false) {
110
+ delete config.rules[ruleName];
111
+ if (config.__ruleSources)
112
+ delete config.__ruleSources[ruleName];
113
+ continue;
114
+ }
115
+ // Only add if not already defined in config (override handled by config)
116
+ if (!Object.prototype.hasOwnProperty.call(config.rules, ruleName)) {
100
117
  config.rules[ruleName] = rulePath;
101
- // Store the source preset path in metadata
102
118
  config.__ruleSources = config.__ruleSources || {};
103
119
  config.__ruleSources[ruleName] = presetPath;
104
120
  }
@@ -111,7 +127,14 @@ function mergePresetMcpServers(config, presetMcpServers) {
111
127
  if (!config.mcpServers)
112
128
  config.mcpServers = {};
113
129
  for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) {
114
- if (!config.mcpServers[serverName]) {
130
+ // Cancel if set to false in config
131
+ if (Object.prototype.hasOwnProperty.call(config.mcpServers, serverName) &&
132
+ config.mcpServers[serverName] === false) {
133
+ delete config.mcpServers[serverName];
134
+ continue;
135
+ }
136
+ // Only add if not already defined in config (override handled by config)
137
+ if (!Object.prototype.hasOwnProperty.call(config.mcpServers, serverName)) {
115
138
  config.mcpServers[serverName] = serverConfig;
116
139
  }
117
140
  }
@@ -23,11 +23,11 @@ export declare function addRuleToCollection(collection: RuleCollection, rule: Ru
23
23
  * @param ruleBasePath Optional base path for resolving relative paths
24
24
  * @returns The rule content
25
25
  */
26
- export declare function collectLocalRule(ruleName: string, source: string, ruleBasePath?: string): RuleContent;
26
+ export declare function collectLocalRule(ruleName: string, source: string | false, ruleBasePath?: string): RuleContent;
27
27
  /**
28
28
  * Collect a rule from an npm package
29
29
  * @param ruleName The name of the rule
30
30
  * @param source The npm package source (can include path)
31
31
  * @returns The rule content
32
32
  */
33
- export declare function collectNpmRule(ruleName: string, source: string): RuleContent;
33
+ export declare function collectNpmRule(ruleName: string, source: string | false): RuleContent;
@@ -87,6 +87,8 @@ function addRuleToCollection(collection, rule, ides) {
87
87
  * @returns The rule content
88
88
  */
89
89
  function collectLocalRule(ruleName, source, ruleBasePath) {
90
+ if (source === false)
91
+ throw new Error(`Rule '${ruleName}' is canceled and should not be processed.`);
90
92
  // Resolve path relative to base path or current directory
91
93
  let sourcePath = source;
92
94
  if (!node_path_1.default.isAbsolute(source)) {
@@ -118,6 +120,8 @@ function collectLocalRule(ruleName, source, ruleBasePath) {
118
120
  * @returns The rule content
119
121
  */
120
122
  function collectNpmRule(ruleName, source) {
123
+ if (source === false)
124
+ throw new Error(`Rule '${ruleName}' is canceled and should not be processed.`);
121
125
  // Parse source into package and file path
122
126
  let packageName;
123
127
  let packagePath;
@@ -13,8 +13,8 @@ const node_path_1 = __importDefault(require("node:path"));
13
13
  function getIdePaths() {
14
14
  const projectDir = process.cwd(); // Get current working directory (project root)
15
15
  return {
16
- cursor: node_path_1.default.join(projectDir, ".cursor", "rules"),
17
- windsurf: node_path_1.default.join(projectDir, ".rules"),
16
+ cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
17
+ windsurf: node_path_1.default.join(projectDir, ".aicm"),
18
18
  };
19
19
  }
20
20
  /**
@@ -22,7 +22,6 @@ function getIdePaths() {
22
22
  */
23
23
  function checkRuleStatus(ruleName, ruleType, ides) {
24
24
  const idePaths = getIdePaths();
25
- // Check if rule is installed in all specified IDEs
26
25
  return ides.every((ide) => {
27
26
  if (!idePaths[ide]) {
28
27
  return false;
@@ -31,14 +30,11 @@ function checkRuleStatus(ruleName, ruleType, ides) {
31
30
  return fs_extra_1.default.existsSync(node_path_1.default.join(idePaths[ide], `${ruleName}.mdc`));
32
31
  }
33
32
  if (ide === "windsurf") {
34
- // For Windsurf, check if the rule exists in .rules directory
35
- // and if it's referenced in .windsurfrules
36
33
  const ruleExists = fs_extra_1.default.existsSync(node_path_1.default.join(idePaths[ide], `${ruleName}.md`));
37
- // Check if .windsurfrules exists and contains a reference to this rule
38
34
  const windsurfRulesPath = node_path_1.default.join(process.cwd(), ".windsurfrules");
39
35
  if (fs_extra_1.default.existsSync(windsurfRulesPath)) {
40
36
  const windsurfRulesContent = fs_extra_1.default.readFileSync(windsurfRulesPath, "utf8");
41
- return (ruleExists && windsurfRulesContent.includes(`.rules/${ruleName}.md`));
37
+ return (ruleExists && windsurfRulesContent.includes(`.aicm/${ruleName}.md`));
42
38
  }
43
39
  return false;
44
40
  }
@@ -20,7 +20,7 @@ function writeRulesToTargets(collection) {
20
20
  }
21
21
  // Write Windsurf rules
22
22
  if (collection.windsurf.length > 0) {
23
- writeWindsurfRulesFromCollection(collection.windsurf);
23
+ writeWindsurfRulesFromCollection(collection.windsurf, idePaths.windsurf);
24
24
  }
25
25
  }
26
26
  /**
@@ -29,16 +29,14 @@ function writeRulesToTargets(collection) {
29
29
  * @param cursorRulesDir The path to Cursor's rules directory
30
30
  */
31
31
  function writeCursorRules(rules, cursorRulesDir) {
32
- fs_extra_1.default.ensureDirSync(cursorRulesDir);
32
+ fs_extra_1.default.emptyDirSync(cursorRulesDir);
33
33
  for (const rule of rules) {
34
- const ruleFile = node_path_1.default.join(cursorRulesDir, `${rule.name}.mdc`);
35
- // For Cursor, we either copy the file or create a symlink to the original
34
+ const ruleFile = node_path_1.default.join(cursorRulesDir, ...rule.name.split("/")) + ".mdc";
35
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
36
36
  if (fs_extra_1.default.existsSync(rule.sourcePath)) {
37
- // Copy the file (safer than symlink)
38
37
  fs_extra_1.default.copyFileSync(rule.sourcePath, ruleFile);
39
38
  }
40
39
  else {
41
- // If source path doesn't exist (shouldn't happen), write content directly
42
40
  const mdcContent = `---\n${JSON.stringify(rule.metadata, null, 2)}\n---\n\n${rule.content}`;
43
41
  fs_extra_1.default.writeFileSync(ruleFile, mdcContent);
44
42
  }
@@ -48,21 +46,22 @@ function writeCursorRules(rules, cursorRulesDir) {
48
46
  * Write rules to Windsurf's rules directory and update .windsurfrules file
49
47
  * @param rules The rules to write
50
48
  */
51
- function writeWindsurfRulesFromCollection(rules) {
52
- const idePaths = (0, rule_status_1.getIdePaths)();
53
- const ruleDir = idePaths.windsurf;
54
- fs_extra_1.default.ensureDirSync(ruleDir);
55
- // First write individual rule files
49
+ function writeWindsurfRulesFromCollection(rules, ruleDir) {
50
+ fs_extra_1.default.emptyDirSync(ruleDir);
56
51
  const ruleFiles = rules.map((rule) => {
57
- const ruleFile = node_path_1.default.join(ruleDir, `${rule.name}.md`);
58
- fs_extra_1.default.writeFileSync(ruleFile, rule.content);
52
+ const physicalRulePath = node_path_1.default.join(ruleDir, ...rule.name.split("/")) + ".md";
53
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
54
+ fs_extra_1.default.writeFileSync(physicalRulePath, rule.content);
55
+ const relativeRuleDir = node_path_1.default.basename(ruleDir); // Gets '.rules'
56
+ const windsurfPath = node_path_1.default.join(relativeRuleDir, ...rule.name.split("/")) + ".md";
57
+ // Normalize to POSIX style for cross-platform compatibility in .windsurfrules
58
+ const windsurfPathPosix = windsurfPath.replace(/\\/g, "/");
59
59
  return {
60
60
  name: rule.name,
61
- path: `.rules/${rule.name}.md`,
61
+ path: windsurfPathPosix,
62
62
  metadata: rule.metadata,
63
63
  };
64
64
  });
65
- // Then generate and write the .windsurfrules file
66
65
  const windsurfRulesContent = (0, windsurf_writer_1.generateWindsurfRulesContent)(ruleFiles);
67
66
  (0, windsurf_writer_1.writeWindsurfRules)(windsurfRulesContent);
68
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "A TypeScript CLI tool for managing AI IDE rules across different projects and teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "format": "prettier --write .",
25
25
  "format:check": "prettier --check .",
26
26
  "lint": "eslint",
27
- "prepare": "husky && npx ts-node src/index.ts install",
27
+ "prepare": "husky install && npx ts-node src/index.ts install",
28
28
  "version": "auto-changelog -p && git add CHANGELOG.md"
29
29
  },
30
30
  "keywords": [