aicm 0.4.0 → 0.5.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
@@ -66,9 +66,9 @@ Now the rules will be linked to `.cursor/rules/` when you run `npm install`.
66
66
 
67
67
  Presets allow you to bundle multiple rules into a single configuration that can be shared across projects.
68
68
 
69
- 1. **Create a preset file**
69
+ 1. **Create a preset package or directory**
70
70
 
71
- Create a JSON file with your rule definitions:
71
+ Create an npm package with your rule definitions in an `aicm.json` file:
72
72
 
73
73
  ```json
74
74
  {
@@ -81,34 +81,83 @@ Create a JSON file with your rule definitions:
81
81
 
82
82
  2. **Reference the preset in your project**
83
83
 
84
- In your project's `aicm.json`, reference the preset:
84
+ In your project's `aicm.json`, reference the preset by its npm package or directory name:
85
85
 
86
86
  ```json
87
87
  {
88
88
  "ides": ["cursor"],
89
- "presets": ["@myteam/ai-tools/my-aicm.json"]
89
+ "presets": ["@myteam/ai-tools"]
90
90
  }
91
91
  ```
92
92
 
93
93
  When you run `npx aicm install`, all rules from the preset will be installed to `.cursor/rules/`.
94
94
 
95
+ ### Overriding and Canceling Rules and MCP Servers from Presets
96
+
97
+ When you use a preset, you can override or cancel 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
+ - **Cancel**: To cancel (remove) 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
+ ```
121
+
122
+ - In this example, `npm-rule` is overridden with a local rule, and `preset-rule` is canceled.
123
+ - The `preset-mcp` server is overridden, and `another-mcp` is canceled.
124
+
125
+ This allows you to fully customize or selectively disable rules and servers from any preset you use.
126
+
95
127
  ### Demo
96
128
 
97
129
  Here is a package to demonstrate how aicm works:
98
130
 
131
+ 1. Install a package containing a rule
132
+
99
133
  ```bash
100
- # Install a package containing a rule
101
134
  npm install --save-dev pirate-coding-rule
135
+ ```
136
+
137
+ 2. Initialize aicm config
102
138
 
103
- # Install the rule via the aicm CLI
104
- npx -y aicm install pirate-coding pirate-coding-rule/rule.mdc
139
+ ```bash
140
+ npx -y aicm init
105
141
  ```
106
142
 
107
- This command will:
143
+ 3. Add the rule to your config file: `aicm.json`
108
144
 
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/`
145
+ ```json
146
+ {
147
+ "ides": ["cursor"],
148
+ "rules": {
149
+ "pirate-coding": "pirate-coding-rule/rule.mdc"
150
+ }
151
+ }
152
+ ```
153
+
154
+ 4. Install all rules from your configuration
155
+
156
+ ```bash
157
+ npx -y aicm install
158
+ ```
159
+
160
+ This command installs all configured rules and MCPs to their IDE-specific locations.
112
161
 
113
162
  After installation, open Cursor and ask it to do something. Your AI assistant will respond with pirate-themed coding advice.
114
163
 
@@ -162,7 +211,6 @@ Example `aicm.json`:
162
211
 
163
212
  - **Cursor**: MCP server configs are written to `.cursor/mcp.json` (see Cursor docs for latest path).
164
213
  - **Windsurf**: Windsurf does not support project mcpServers. MCP server configuration is not installed for Windsurf projects.
165
- - All installations are per-project.
166
214
 
167
215
  ### Rule Source Types
168
216
 
@@ -211,22 +259,18 @@ npx aicm init
211
259
  Installs rules from your configuration to the appropriate IDE locations.
212
260
 
213
261
  ```bash
214
- npx aicm install [rule-name] [rule-source]
262
+ npx aicm install
215
263
  ```
216
264
 
217
265
  **Options:**
218
266
 
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)
267
+ - No arguments are supported. All rules are installed from your configuration and any referenced presets.
221
268
 
222
269
  **Examples:**
223
270
 
224
271
  ```bash
225
272
  # Install all configured rules
226
273
  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
230
274
  ```
231
275
 
232
276
  ## Contributing
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.4.0",
3
+ "version": "0.5.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": {