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 +82 -56
- package/dist/commands/install.js +15 -81
- package/dist/commands/list.js +2 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/config.js +29 -6
- package/dist/utils/rule-collector.d.ts +2 -2
- package/dist/utils/rule-collector.js +4 -0
- package/dist/utils/rule-status.js +3 -7
- package/dist/utils/rule-writer.js +14 -15
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
Create an npm package with your rule definitions in an `aicm.json` file:
|
|
70
60
|
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
149
|
+
4. Install all rules from your configuration
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npx -y aicm install
|
|
153
|
+
```
|
|
108
154
|
|
|
109
|
-
|
|
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/
|
|
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 `.
|
|
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
|
|
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
|
package/dist/commands/install.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
// If config doesn't exist,
|
|
34
|
+
const config = (0, config_1.getConfig)();
|
|
35
|
+
// If config doesn't exist, print error and exit
|
|
90
36
|
if (!config) {
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
58
|
+
// Process each rule
|
|
118
59
|
let hasErrors = false;
|
|
119
60
|
for (const [name, source] of Object.entries(config.rules)) {
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
155
|
-
if (hasErrors
|
|
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
|
-
|
|
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) {
|
package/dist/commands/list.js
CHANGED
|
@@ -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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
99
|
-
if (
|
|
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
|
|
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, ".
|
|
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(`.
|
|
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.
|
|
32
|
+
fs_extra_1.default.emptyDirSync(cursorRulesDir);
|
|
33
33
|
for (const rule of rules) {
|
|
34
|
-
const ruleFile = node_path_1.default.join(cursorRulesDir,
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
fs_extra_1.default.
|
|
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:
|
|
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.
|
|
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": [
|