aicm 0.9.1 → 0.11.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
@@ -2,13 +2,14 @@
2
2
 
3
3
  > Agentic IDE Configuration Manager
4
4
 
5
- A CLI tool for syncing and managing Agentic IDE rules across projects
5
+ A CLI tool for managing Agentic IDE configurations across projects
6
6
 
7
- ## Why
7
+ https://github.com/user-attachments/assets/e80dedbc-89c4-4747-9acf-b7ecb7493fcc
8
8
 
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.
9
+ ## Why
10
10
 
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.
11
+ With the rise of Agentic IDEs, we have an opportunity to enforce best practices through rules. However, these rules are typically isolated within individual projects.
12
+ **aicm** is a CLI tool for distributing Agentic IDE configurations, rules, and MCPs across projects. It leverages node package managers, copy configurations from node_modules to the correct locations in your file system.
12
13
 
13
14
  ## Getting Started
14
15
 
@@ -33,6 +34,11 @@ In your project's `aicm.json`, reference the package and the specific rule:
33
34
  "rules": {
34
35
  "typescript": "@myteam/ai-tools/rules/typescript.mdc",
35
36
  "react": "@myteam/ai-tools/rules/react.mdc"
37
+ },
38
+ "mcpServers": {
39
+ "my-mcp": {
40
+ "url": "https://example.com/sse"
41
+ }
36
42
  }
37
43
  }
38
44
  ```
@@ -47,7 +53,7 @@ In your project's `aicm.json`, reference the package and the specific rule:
47
53
  }
48
54
  ```
49
55
 
50
- Now the rules will be linked to `.cursor/rules/aicm/` when you run `npm install`.
56
+ Now, when you run `npm install`, the rules will be added to `.cursor/rules/aicm/` and the mcps to `.cursor/mcp.json`.
51
57
 
52
58
  ### Using Presets
53
59
 
@@ -147,6 +153,44 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
147
153
 
148
154
  To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
149
155
 
156
+ ## Workspaces Support
157
+
158
+ aicm supports workspaces by automatically discovering and installing configurations across multiple packages in your repository.
159
+
160
+ To enable workspaces mode, use the `--workspaces` flag:
161
+
162
+ ```bash
163
+ npx aicm install --workspaces
164
+ ```
165
+
166
+ This will:
167
+
168
+ 1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository
169
+ 2. **Install per package**: Install rules and MCPs for each package individually in their respective directories
170
+
171
+ ### How It Works
172
+
173
+ Each directory containing an `aicm.json` file is treated as a separate package with its own configuration.
174
+
175
+ For example, in a workspace structure like:
176
+
177
+ ```
178
+ ├── packages/
179
+ │ ├── frontend/
180
+ │ │ └── aicm.json
181
+ │ └── backend/
182
+ │ └── aicm.json
183
+ └── services/
184
+ └── api/
185
+ └── aicm.json
186
+ ```
187
+
188
+ Running `npx aicm install --workspaces` will install rules for each package in their respective directories:
189
+
190
+ - `packages/frontend/.cursor/rules/aicm/`
191
+ - `packages/backend/.cursor/rules/aicm/`
192
+ - `services/api/.cursor/rules/aicm/`
193
+
150
194
  ## Configuration
151
195
 
152
196
  To configure aicm, use either:
@@ -252,6 +296,8 @@ npx aicm install
252
296
  Options:
253
297
 
254
298
  - `--ci`: run in CI environments (default: `false`)
299
+ - `--workspaces`: enable workspaces mode to discover and install configurations across multiple packages
300
+ - `--verbose`: show detailed output during installation
255
301
 
256
302
  ## Node.js API
257
303
 
@@ -296,6 +342,8 @@ Installs rules and MCP servers based on configuration.
296
342
  - `cwd`: Base directory to use instead of `process.cwd()`
297
343
  - `config`: Custom config object to use instead of loading from file
298
344
  - `installOnCI`: Run installation on CI environments (default: `false`)
345
+ - `workspaces`: Enable workspaces mode (default: `false`)
346
+ - `verbose`: Show verbose output during installation (default: `false`)
299
347
 
300
348
  **Returns:**
301
349
 
@@ -15,6 +15,14 @@ export interface InstallOptions {
15
15
  * allow installation on CI environments
16
16
  */
17
17
  installOnCI?: boolean;
18
+ /**
19
+ * Enable workspaces mode
20
+ */
21
+ workspaces?: boolean;
22
+ /**
23
+ * Show verbose output during installation
24
+ */
25
+ verbose?: boolean;
18
26
  }
19
27
  /**
20
28
  * Result of the install operation
@@ -32,6 +40,10 @@ export interface InstallResult {
32
40
  * Number of rules installed
33
41
  */
34
42
  installedRuleCount: number;
43
+ /**
44
+ * Number of packages installed
45
+ */
46
+ packagesCount: number;
35
47
  }
36
48
  /**
37
49
  * Core implementation of the rule installation logic
@@ -39,4 +51,4 @@ export interface InstallResult {
39
51
  * @returns Result of the install operation
40
52
  */
41
53
  export declare function install(options?: InstallOptions): Promise<InstallResult>;
42
- export declare function installCommand(installOnCI?: boolean): Promise<void>;
54
+ export declare function installCommand(installOnCI?: boolean, workspaces?: boolean, verbose?: boolean): Promise<void>;
@@ -10,27 +10,25 @@ const config_1 = require("../utils/config");
10
10
  const rule_detector_1 = require("../utils/rule-detector");
11
11
  const rule_collector_1 = require("../utils/rule-collector");
12
12
  const rule_writer_1 = require("../utils/rule-writer");
13
- const fs_extra_1 = __importDefault(require("fs-extra"));
14
- const node_path_1 = __importDefault(require("node:path"));
15
13
  const ci_info_1 = require("ci-info");
14
+ const discovery_1 = require("./workspaces/discovery");
15
+ const workspaces_install_1 = require("./workspaces/workspaces-install");
16
+ const mcp_writer_1 = require("../utils/mcp-writer");
16
17
  /**
17
- * Write MCP servers configuration to IDE targets
18
- * @param mcpServers The MCP servers configuration
19
- * @param ides The IDEs to write to
20
- * @param cwd The current working directory
18
+ * Helper function to execute a function within a specific working directory
19
+ * and ensure the original directory is always restored
21
20
  */
22
- function writeMcpServersToTargets(mcpServers, ides, cwd) {
23
- if (!mcpServers)
24
- return;
25
- for (const ide of ides) {
26
- let mcpPath = null;
27
- if (ide === "cursor") {
28
- mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json");
29
- fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(mcpPath));
30
- }
31
- // Windsurf does not support project mcpServers, so skip
32
- if (mcpPath) {
33
- fs_extra_1.default.writeJsonSync(mcpPath, { mcpServers }, { spaces: 2 });
21
+ async function withWorkingDirectory(targetDir, fn) {
22
+ const originalCwd = process.cwd();
23
+ if (targetDir !== originalCwd) {
24
+ process.chdir(targetDir);
25
+ }
26
+ try {
27
+ return await fn();
28
+ }
29
+ finally {
30
+ if (targetDir !== originalCwd) {
31
+ process.chdir(originalCwd);
34
32
  }
35
33
  }
36
34
  }
@@ -47,6 +45,64 @@ function isInCIEnvironment() {
47
45
  // Fall back to ci-info's detection
48
46
  return ci_info_1.isCI;
49
47
  }
48
+ async function handleWorkspacesInstallation(cwd, installOnCI, verbose = false) {
49
+ return withWorkingDirectory(cwd, async () => {
50
+ if (verbose) {
51
+ console.log(chalk_1.default.blue("🔍 Discovering packages..."));
52
+ }
53
+ const packages = await (0, discovery_1.discoverPackagesWithAicm)(cwd);
54
+ if (packages.length === 0) {
55
+ return {
56
+ success: false,
57
+ error: "No packages with aicm configurations found",
58
+ installedRuleCount: 0,
59
+ packagesCount: 0,
60
+ };
61
+ }
62
+ if (verbose) {
63
+ console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations:`));
64
+ packages.forEach((pkg) => {
65
+ console.log(chalk_1.default.gray(` - ${pkg.relativePath}`));
66
+ });
67
+ console.log(chalk_1.default.blue(`📦 Installing configurations...`));
68
+ }
69
+ const result = await (0, workspaces_install_1.installWorkspacesPackages)(packages, {
70
+ installOnCI,
71
+ });
72
+ if (verbose) {
73
+ result.packages.forEach((pkg) => {
74
+ if (pkg.success) {
75
+ console.log(chalk_1.default.green(`✅ ${pkg.path} (${pkg.installedRuleCount} rules)`));
76
+ }
77
+ else {
78
+ console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
79
+ }
80
+ });
81
+ }
82
+ const failedPackages = result.packages.filter((r) => !r.success);
83
+ if (failedPackages.length > 0) {
84
+ console.log(chalk_1.default.yellow(`Installation completed with errors`));
85
+ if (verbose) {
86
+ console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rules total)`));
87
+ console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
88
+ }
89
+ const errorDetails = failedPackages
90
+ .map((p) => `${p.path}: ${p.error}`)
91
+ .join("; ");
92
+ return {
93
+ success: false,
94
+ error: `Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`,
95
+ installedRuleCount: result.totalRuleCount,
96
+ packagesCount: result.packages.length,
97
+ };
98
+ }
99
+ return {
100
+ success: true,
101
+ installedRuleCount: result.totalRuleCount,
102
+ packagesCount: result.packages.length,
103
+ };
104
+ });
105
+ }
50
106
  /**
51
107
  * Core implementation of the rule installation logic
52
108
  * @param options Install options
@@ -55,45 +111,38 @@ function isInCIEnvironment() {
55
111
  async function install(options = {}) {
56
112
  const cwd = options.cwd || process.cwd();
57
113
  const installOnCI = options.installOnCI === true; // Default to false if not specified
58
- try {
59
- const originalCwd = process.cwd();
60
- if (cwd !== originalCwd) {
61
- process.chdir(cwd);
114
+ return withWorkingDirectory(cwd, async () => {
115
+ if (options.workspaces) {
116
+ return await handleWorkspacesInstallation(cwd, installOnCI, options.verbose);
62
117
  }
63
- const ruleCollection = (0, rule_collector_1.initRuleCollection)();
64
118
  const config = options.config || (0, config_1.getConfig)();
119
+ const ruleCollection = (0, rule_collector_1.initRuleCollection)();
65
120
  if (!config) {
66
- if (cwd !== originalCwd) {
67
- process.chdir(originalCwd);
68
- }
69
121
  return {
70
122
  success: false,
71
- error: "Configuration file not found!",
123
+ error: "Configuration file not found",
72
124
  installedRuleCount: 0,
125
+ packagesCount: 0,
73
126
  };
74
127
  }
75
128
  const inCI = isInCIEnvironment();
76
129
  if (inCI && !installOnCI && !config.installOnCI) {
77
- if (cwd !== originalCwd) {
78
- process.chdir(originalCwd);
79
- }
80
130
  console.log(chalk_1.default.yellow("Detected CI environment, skipping install."));
81
131
  return {
82
132
  success: true,
83
133
  installedRuleCount: 0,
134
+ packagesCount: 0,
84
135
  };
85
136
  }
86
137
  // Check if rules are defined (either directly or through presets)
87
138
  if (!config.rules || Object.keys(config.rules).length === 0) {
88
139
  // If there are no presets defined either, show a message
89
140
  if (!config.presets || config.presets.length === 0) {
90
- if (cwd !== originalCwd) {
91
- process.chdir(originalCwd);
92
- }
93
141
  return {
94
142
  success: false,
95
- error: "No rules defined in configuration.",
143
+ error: "No rules defined in configuration",
96
144
  installedRuleCount: 0,
145
+ packagesCount: 0,
97
146
  };
98
147
  }
99
148
  }
@@ -139,13 +188,11 @@ async function install(options = {}) {
139
188
  }
140
189
  // If there were errors, exit with error
141
190
  if (hasErrors) {
142
- if (cwd !== originalCwd) {
143
- process.chdir(originalCwd);
144
- }
145
191
  return {
146
192
  success: false,
147
193
  error: errorMessages.join("; "),
148
194
  installedRuleCount,
195
+ packagesCount: 0,
149
196
  };
150
197
  }
151
198
  // Write all collected rules to their targets
@@ -154,39 +201,32 @@ async function install(options = {}) {
154
201
  if (config.mcpServers) {
155
202
  // Filter out canceled servers
156
203
  const filteredMcpServers = Object.fromEntries(Object.entries(config.mcpServers).filter(([, v]) => v !== false));
157
- writeMcpServersToTargets(filteredMcpServers, config.ides, cwd);
158
- }
159
- // Restore original cwd
160
- if (cwd !== originalCwd) {
161
- process.chdir(originalCwd);
204
+ (0, mcp_writer_1.writeMcpServersToTargets)(filteredMcpServers, config.ides, cwd);
162
205
  }
163
206
  return {
164
207
  success: true,
165
208
  installedRuleCount,
209
+ packagesCount: 1,
166
210
  };
167
- }
168
- catch (e) {
169
- const errorMessage = e instanceof Error ? e.message : String(e);
170
- // If cwd was changed, restore it
171
- if (cwd !== process.cwd()) {
172
- process.chdir(cwd);
173
- }
174
- return {
175
- success: false,
176
- error: errorMessage,
177
- installedRuleCount: 0,
178
- };
179
- }
211
+ });
180
212
  }
181
- async function installCommand(installOnCI) {
213
+ async function installCommand(installOnCI, workspaces, verbose) {
182
214
  try {
183
- const result = await install({ installOnCI });
215
+ const result = await install({ installOnCI, workspaces, verbose });
184
216
  if (!result.success) {
185
217
  console.error(chalk_1.default.red(result.error));
186
218
  process.exit(1);
187
219
  }
188
220
  else {
189
- console.log("Rules installation completed");
221
+ if (result.packagesCount > 1) {
222
+ console.log(`Successfully installed ${result.installedRuleCount} rules across ${result.packagesCount} packages`);
223
+ }
224
+ else if (workspaces) {
225
+ console.log(`Successfully installed ${result.installedRuleCount} rules across ${result.packagesCount} packages`);
226
+ }
227
+ else {
228
+ console.log("Rules installation completed");
229
+ }
190
230
  }
191
231
  }
192
232
  catch (error) {
@@ -0,0 +1,7 @@
1
+ import { PackageInfo } from "../../types";
2
+ /**
3
+ * Discover all packages with aicm configurations in a monorepo
4
+ * @param rootDir The root directory to search from
5
+ * @returns Array of discovered packages
6
+ */
7
+ export declare function discoverPackagesWithAicm(rootDir: string): Promise<PackageInfo[]>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.discoverPackagesWithAicm = discoverPackagesWithAicm;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const config_1 = require("../../utils/config");
10
+ /**
11
+ * Discover aicm.json files using git ls-files
12
+ * @param rootDir The root directory to search from
13
+ * @returns Array of aicm.json file paths
14
+ */
15
+ function findAicmFiles(rootDir) {
16
+ const output = (0, node_child_process_1.execSync)("git ls-files --cached --others --exclude-standard aicm.json **/aicm.json", {
17
+ cwd: rootDir,
18
+ encoding: "utf8",
19
+ });
20
+ return output
21
+ .trim()
22
+ .split("\n")
23
+ .filter(Boolean)
24
+ .map((file) => node_path_1.default.resolve(rootDir, file));
25
+ }
26
+ /**
27
+ * Discover all packages with aicm configurations in a monorepo
28
+ * @param rootDir The root directory to search from
29
+ * @returns Array of discovered packages
30
+ */
31
+ async function discoverPackagesWithAicm(rootDir) {
32
+ const aicmFiles = findAicmFiles(rootDir);
33
+ const packages = [];
34
+ for (const aicmFile of aicmFiles) {
35
+ const packageDir = node_path_1.default.dirname(aicmFile);
36
+ const relativePath = node_path_1.default.relative(rootDir, packageDir);
37
+ // Normalize to forward slashes for cross-platform compatibility
38
+ const normalizedRelativePath = relativePath.replace(/\\/g, "/");
39
+ const config = (0, config_1.getConfig)(packageDir);
40
+ if (config) {
41
+ packages.push({
42
+ relativePath: normalizedRelativePath || ".",
43
+ absolutePath: packageDir,
44
+ config,
45
+ });
46
+ }
47
+ }
48
+ // Sort packages by relativePath for deterministic order
49
+ return packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
50
+ }
@@ -0,0 +1,9 @@
1
+ import { PackageInfo, MonorepoInstallResult } from "./types";
2
+ import { InstallOptions } from "../install";
3
+ /**
4
+ * Install aicm configurations for all packages in a monorepo
5
+ * @param packages The packages to install configurations for
6
+ * @param options Install options
7
+ * @returns Result of the monorepo installation
8
+ */
9
+ export declare function installMonorepoPackages(packages: PackageInfo[], options?: InstallOptions): Promise<MonorepoInstallResult>;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installMonorepoPackages = installMonorepoPackages;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const install_1 = require("../install");
10
+ /**
11
+ * Install aicm configurations for all packages in a monorepo
12
+ * @param packages The packages to install configurations for
13
+ * @param options Install options
14
+ * @returns Result of the monorepo installation
15
+ */
16
+ async function installMonorepoPackages(packages, options = {}) {
17
+ console.log(chalk_1.default.blue(`📦 Installing configurations...`));
18
+ const results = [];
19
+ let totalRuleCount = 0;
20
+ // Install packages sequentially for now (can be parallelized later)
21
+ for (const pkg of packages) {
22
+ const packagePath = node_path_1.default.resolve(process.cwd(), pkg.path);
23
+ try {
24
+ console.log(chalk_1.default.gray(` Installing ${pkg.path}...`));
25
+ const result = await (0, install_1.install)({
26
+ ...options,
27
+ cwd: packagePath,
28
+ });
29
+ if (result.success) {
30
+ console.log(chalk_1.default.green(`✅ ${pkg.path} (${result.installedRuleCount} rules)`));
31
+ totalRuleCount += result.installedRuleCount;
32
+ }
33
+ else {
34
+ console.log(chalk_1.default.red(`❌ ${pkg.path}: ${result.error}`));
35
+ }
36
+ results.push({
37
+ path: pkg.path,
38
+ success: result.success,
39
+ error: result.error,
40
+ installedRuleCount: result.installedRuleCount,
41
+ });
42
+ }
43
+ catch (error) {
44
+ const errorMessage = error instanceof Error ? error.message : String(error);
45
+ console.log(chalk_1.default.red(`❌ ${pkg.path}: ${errorMessage}`));
46
+ results.push({
47
+ path: pkg.path,
48
+ success: false,
49
+ error: errorMessage,
50
+ installedRuleCount: 0,
51
+ });
52
+ }
53
+ }
54
+ const successfulPackages = results.filter((r) => r.success);
55
+ const failedPackages = results.filter((r) => !r.success);
56
+ // Print summary
57
+ if (failedPackages.length > 0) {
58
+ console.log(chalk_1.default.yellow(`\n⚠️ Installation completed with errors`));
59
+ console.log(chalk_1.default.green(`Successfully installed: ${successfulPackages.length}/${packages.length} packages (${totalRuleCount} rules total)`));
60
+ console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
61
+ }
62
+ else {
63
+ console.log(chalk_1.default.green(`\n🎉 Successfully installed ${totalRuleCount} rules across ${packages.length} packages`));
64
+ }
65
+ return {
66
+ success: failedPackages.length === 0,
67
+ packages: results,
68
+ totalRuleCount,
69
+ };
70
+ }
@@ -0,0 +1,9 @@
1
+ import { PackageInfo, MonorepoInstallResult } from "../../types";
2
+ import { InstallOptions } from "../install";
3
+ /**
4
+ * Install aicm configurations for all packages in a monorepo
5
+ * @param packages The packages to install configurations for
6
+ * @param options Install options
7
+ * @returns Result of the monorepo installation
8
+ */
9
+ export declare function installMonorepoPackages(packages: PackageInfo[], options?: InstallOptions): Promise<MonorepoInstallResult>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installMonorepoPackages = installMonorepoPackages;
4
+ const install_1 = require("../install");
5
+ /**
6
+ * Install aicm configurations for all packages in a monorepo
7
+ * @param packages The packages to install configurations for
8
+ * @param options Install options
9
+ * @returns Result of the monorepo installation
10
+ */
11
+ async function installMonorepoPackages(packages, options = {}) {
12
+ const results = [];
13
+ let totalRuleCount = 0;
14
+ // Install packages sequentially for now (can be parallelized later)
15
+ for (const pkg of packages) {
16
+ const packagePath = pkg.absolutePath;
17
+ try {
18
+ const result = await (0, install_1.install)({
19
+ ...options,
20
+ cwd: packagePath,
21
+ });
22
+ totalRuleCount += result.installedRuleCount;
23
+ results.push({
24
+ path: pkg.relativePath,
25
+ success: result.success,
26
+ error: result.error,
27
+ installedRuleCount: result.installedRuleCount,
28
+ });
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : String(error);
32
+ results.push({
33
+ path: pkg.relativePath,
34
+ success: false,
35
+ error: errorMessage,
36
+ installedRuleCount: 0,
37
+ });
38
+ }
39
+ }
40
+ const failedPackages = results.filter((r) => !r.success);
41
+ return {
42
+ success: failedPackages.length === 0,
43
+ packages: results,
44
+ totalRuleCount,
45
+ };
46
+ }
@@ -0,0 +1 @@
1
+ export * from "../../types";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("../../types"), exports);
@@ -0,0 +1,7 @@
1
+ import { PackageInfo } from "../../types";
2
+ /**
3
+ * Discover all packages with aicm configurations
4
+ * @param rootDir The root directory to search from
5
+ * @returns Array of discovered packages
6
+ */
7
+ export declare function discoverPackagesWithAicm(rootDir: string): Promise<PackageInfo[]>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.discoverPackagesWithAicm = discoverPackagesWithAicm;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const config_1 = require("../../utils/config");
10
+ /**
11
+ * Discover aicm.json files using git ls-files
12
+ * @param rootDir The root directory to search from
13
+ * @returns Array of aicm.json file paths
14
+ */
15
+ function findAicmFiles(rootDir) {
16
+ const output = (0, node_child_process_1.execSync)("git ls-files --cached --others --exclude-standard aicm.json **/aicm.json", {
17
+ cwd: rootDir,
18
+ encoding: "utf8",
19
+ });
20
+ return output
21
+ .trim()
22
+ .split("\n")
23
+ .filter(Boolean)
24
+ .map((file) => node_path_1.default.resolve(rootDir, file));
25
+ }
26
+ /**
27
+ * Discover all packages with aicm configurations
28
+ * @param rootDir The root directory to search from
29
+ * @returns Array of discovered packages
30
+ */
31
+ async function discoverPackagesWithAicm(rootDir) {
32
+ const aicmFiles = findAicmFiles(rootDir);
33
+ const packages = [];
34
+ for (const aicmFile of aicmFiles) {
35
+ const packageDir = node_path_1.default.dirname(aicmFile);
36
+ const relativePath = node_path_1.default.relative(rootDir, packageDir);
37
+ // Normalize to forward slashes for cross-platform compatibility
38
+ const normalizedRelativePath = relativePath.replace(/\\/g, "/");
39
+ const config = (0, config_1.getConfig)(packageDir);
40
+ if (config) {
41
+ packages.push({
42
+ relativePath: normalizedRelativePath || ".",
43
+ absolutePath: packageDir,
44
+ config,
45
+ });
46
+ }
47
+ }
48
+ // Sort packages by relativePath for deterministic order
49
+ return packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
50
+ }
@@ -0,0 +1,9 @@
1
+ import { PackageInfo, WorkspacesInstallResult } from "../../types";
2
+ import { InstallOptions } from "../install";
3
+ /**
4
+ * Install aicm configurations for all packages in a workspace
5
+ * @param packages The packages to install configurations for
6
+ * @param options Install options
7
+ * @returns Result of the workspace installation
8
+ */
9
+ export declare function installWorkspacesPackages(packages: PackageInfo[], options?: InstallOptions): Promise<WorkspacesInstallResult>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installWorkspacesPackages = installWorkspacesPackages;
4
+ const install_1 = require("../install");
5
+ /**
6
+ * Install aicm configurations for all packages in a workspace
7
+ * @param packages The packages to install configurations for
8
+ * @param options Install options
9
+ * @returns Result of the workspace installation
10
+ */
11
+ async function installWorkspacesPackages(packages, options = {}) {
12
+ const results = [];
13
+ let totalRuleCount = 0;
14
+ // Install packages sequentially for now (can be parallelized later)
15
+ for (const pkg of packages) {
16
+ const packagePath = pkg.absolutePath;
17
+ try {
18
+ const result = await (0, install_1.install)({
19
+ ...options,
20
+ cwd: packagePath,
21
+ });
22
+ totalRuleCount += result.installedRuleCount;
23
+ results.push({
24
+ path: pkg.relativePath,
25
+ success: result.success,
26
+ error: result.error,
27
+ installedRuleCount: result.installedRuleCount,
28
+ });
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : String(error);
32
+ results.push({
33
+ path: pkg.relativePath,
34
+ success: false,
35
+ error: errorMessage,
36
+ installedRuleCount: 0,
37
+ });
38
+ }
39
+ }
40
+ const failedPackages = results.filter((r) => !r.success);
41
+ return {
42
+ success: failedPackages.length === 0,
43
+ packages: results,
44
+ totalRuleCount,
45
+ };
46
+ }
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ const args = (0, arg_1.default)({
17
17
  "--help": Boolean,
18
18
  "--version": Boolean,
19
19
  "--ci": Boolean,
20
+ "--workspaces": Boolean,
21
+ "--verbose": Boolean,
20
22
  "-h": "--help",
21
23
  "-v": "--version",
22
24
  }, {
@@ -36,7 +38,7 @@ switch (command) {
36
38
  (0, init_1.initCommand)();
37
39
  break;
38
40
  case "install":
39
- (0, install_1.installCommand)(args["--ci"]);
41
+ (0, install_1.installCommand)(args["--ci"], args["--workspaces"], args["--verbose"]);
40
42
  break;
41
43
  case "list":
42
44
  (0, list_1.listCommand)();
@@ -62,10 +64,13 @@ ${chalk_1.default.bold("OPTIONS")}
62
64
  -h, --help Show this help message
63
65
  -v, --version Show version number
64
66
  --ci Run in CI environments (default: \`false\`)
67
+ --workspaces Install rules across all workspaces
68
+ --verbose Show detailed output during installation
65
69
 
66
70
  ${chalk_1.default.bold("EXAMPLES")}
67
71
  $ aicm init
68
72
  $ aicm install
73
+ $ aicm install --workspaces
69
74
  $ aicm list
70
75
  `);
71
76
  }
@@ -41,3 +41,18 @@ export interface RuleCollection {
41
41
  cursor: RuleContent[];
42
42
  windsurf: RuleContent[];
43
43
  }
44
+ export interface PackageInfo {
45
+ relativePath: string;
46
+ absolutePath: string;
47
+ config: Config;
48
+ }
49
+ export interface WorkspacesInstallResult {
50
+ success: boolean;
51
+ packages: Array<{
52
+ path: string;
53
+ success: boolean;
54
+ error?: string;
55
+ installedRuleCount: number;
56
+ }>;
57
+ totalRuleCount: number;
58
+ }
@@ -11,11 +11,11 @@ export interface PresetPathInfo {
11
11
  fullPath: string;
12
12
  originalPath: string;
13
13
  }
14
- export declare function getFullPresetPath(presetPath: string): PresetPathInfo | null;
14
+ export declare function getFullPresetPath(presetPath: string, cwd?: string): PresetPathInfo | null;
15
15
  /**
16
16
  * Load a preset file and return its contents
17
17
  */
18
- export declare function loadPreset(presetPath: string): {
18
+ export declare function loadPreset(presetPath: string, cwd?: string): {
19
19
  rules: Rules;
20
20
  mcpServers?: import("../types").MCPServers;
21
21
  presets?: string[];
@@ -24,11 +24,11 @@ export declare function loadPreset(presetPath: string): {
24
24
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
25
25
  * Returns the config object or null if not found.
26
26
  */
27
- export declare function loadAicmConfigCosmiconfig(): Config | null;
27
+ export declare function loadAicmConfigCosmiconfig(searchFrom?: string): Config | null;
28
28
  /**
29
29
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
30
30
  */
31
- export declare function getConfig(): Config | null;
31
+ export declare function getConfig(cwd?: string): Config | null;
32
32
  /**
33
33
  * Get the source preset path for a rule if it came from a preset
34
34
  */
@@ -40,4 +40,4 @@ export declare function getOriginalPresetPath(config: Config, ruleName: string):
40
40
  /**
41
41
  * Save the configuration to the aicm.json file
42
42
  */
43
- export declare function saveConfig(config: Config): boolean;
43
+ export declare function saveConfig(config: Config, cwd?: string): boolean;
@@ -14,17 +14,23 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const cosmiconfig_1 = require("cosmiconfig");
16
16
  const CONFIG_FILE = "aicm.json";
17
- function getFullPresetPath(presetPath) {
18
- // If it's a local file with .json extension and exists, return as is
19
- if (presetPath.endsWith(".json") && fs_extra_1.default.pathExistsSync(presetPath)) {
20
- return { fullPath: presetPath, originalPath: presetPath };
17
+ function getFullPresetPath(presetPath, cwd) {
18
+ const workingDir = cwd || process.cwd();
19
+ // If it's a local file with .json extension, check relative to the working directory
20
+ if (presetPath.endsWith(".json")) {
21
+ const absolutePath = node_path_1.default.isAbsolute(presetPath)
22
+ ? presetPath
23
+ : node_path_1.default.resolve(workingDir, presetPath);
24
+ if (fs_extra_1.default.pathExistsSync(absolutePath)) {
25
+ return { fullPath: absolutePath, originalPath: presetPath };
26
+ }
21
27
  }
22
28
  try {
23
29
  let absolutePresetPath;
24
30
  // Handle npm package with explicit JSON path
25
31
  if (presetPath.endsWith(".json")) {
26
32
  absolutePresetPath = require.resolve(presetPath, {
27
- paths: [__dirname, process.cwd()],
33
+ paths: [__dirname, workingDir],
28
34
  });
29
35
  }
30
36
  // Handle npm package without explicit JSON path (add aicm.json)
@@ -33,13 +39,13 @@ function getFullPresetPath(presetPath) {
33
39
  const presetPathWithConfig = node_path_1.default.join(presetPath, "aicm.json");
34
40
  try {
35
41
  absolutePresetPath = require.resolve(presetPathWithConfig, {
36
- paths: [__dirname, process.cwd()],
42
+ paths: [__dirname, workingDir],
37
43
  });
38
44
  }
39
45
  catch (_a) {
40
46
  // If direct resolution fails, try as a package name
41
47
  absolutePresetPath = require.resolve(presetPath, {
42
- paths: [__dirname, process.cwd()],
48
+ paths: [__dirname, workingDir],
43
49
  });
44
50
  // If we found the package but not the config file, look for aicm.json
45
51
  if (fs_extra_1.default.existsSync(absolutePresetPath)) {
@@ -62,8 +68,8 @@ function getFullPresetPath(presetPath) {
62
68
  /**
63
69
  * Load a preset file and return its contents
64
70
  */
65
- function loadPreset(presetPath) {
66
- const pathInfo = getFullPresetPath(presetPath);
71
+ function loadPreset(presetPath, cwd) {
72
+ const pathInfo = getFullPresetPath(presetPath, cwd);
67
73
  if (!pathInfo) {
68
74
  throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
69
75
  }
@@ -92,7 +98,7 @@ const processedPresets = new Set();
92
98
  /**
93
99
  * Process presets and return a new config with merged rules and metadata
94
100
  */
95
- function processPresets(config) {
101
+ function processPresets(config, cwd) {
96
102
  // Create a deep copy of the config to avoid mutations
97
103
  const newConfig = JSON.parse(JSON.stringify(config));
98
104
  const metadata = {
@@ -101,17 +107,17 @@ function processPresets(config) {
101
107
  };
102
108
  // Clear processed presets tracking set when starting from the top level
103
109
  processedPresets.clear();
104
- return processPresetsInternal(newConfig, metadata);
110
+ return processPresetsInternal(newConfig, metadata, cwd);
105
111
  }
106
112
  /**
107
113
  * Internal function to process presets recursively
108
114
  */
109
- function processPresetsInternal(config, metadata) {
115
+ function processPresetsInternal(config, metadata, cwd) {
110
116
  if (!config.presets || !Array.isArray(config.presets)) {
111
117
  return { config, metadata };
112
118
  }
113
119
  for (const presetPath of config.presets) {
114
- const pathInfo = getFullPresetPath(presetPath);
120
+ const pathInfo = getFullPresetPath(presetPath, cwd);
115
121
  if (!pathInfo) {
116
122
  throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
117
123
  }
@@ -122,7 +128,7 @@ function processPresetsInternal(config, metadata) {
122
128
  }
123
129
  // Mark this preset as processed
124
130
  processedPresets.add(pathInfo.fullPath);
125
- const preset = loadPreset(presetPath);
131
+ const preset = loadPreset(presetPath, cwd);
126
132
  if (!preset)
127
133
  continue;
128
134
  // Process nested presets first (depth-first)
@@ -134,7 +140,7 @@ function processPresetsInternal(config, metadata) {
134
140
  ides: [],
135
141
  };
136
142
  // Recursively process the nested presets
137
- const { config: nestedConfig } = processPresetsInternal(presetConfig, metadata);
143
+ const { config: nestedConfig } = processPresetsInternal(presetConfig, metadata, cwd);
138
144
  Object.assign(preset.rules, nestedConfig.rules);
139
145
  }
140
146
  const { updatedConfig, updatedMetadata } = mergePresetRules(config, preset.rules, pathInfo, metadata);
@@ -200,12 +206,12 @@ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
200
206
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
201
207
  * Returns the config object or null if not found.
202
208
  */
203
- function loadAicmConfigCosmiconfig() {
209
+ function loadAicmConfigCosmiconfig(searchFrom) {
204
210
  const explorer = (0, cosmiconfig_1.cosmiconfigSync)("aicm", {
205
211
  searchPlaces: ["package.json", "aicm.json"],
206
212
  });
207
213
  try {
208
- const result = explorer.search();
214
+ const result = explorer.search(searchFrom);
209
215
  if (!result || !result.config)
210
216
  return null;
211
217
  const config = result.config;
@@ -222,12 +228,13 @@ function loadAicmConfigCosmiconfig() {
222
228
  /**
223
229
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
224
230
  */
225
- function getConfig() {
226
- const config = loadAicmConfigCosmiconfig();
231
+ function getConfig(cwd) {
232
+ const workingDir = cwd || process.cwd();
233
+ const config = loadAicmConfigCosmiconfig(workingDir);
227
234
  if (!config) {
228
- throw new Error(`No config found in ${process.cwd()}, create one using "aicm init"`);
235
+ throw new Error(`No config found in ${workingDir}, create one using "aicm init"`);
229
236
  }
230
- const { config: processedConfig, metadata } = processPresets(config);
237
+ const { config: processedConfig, metadata } = processPresets(config, workingDir);
231
238
  // Store metadata for later access
232
239
  currentMetadata = metadata;
233
240
  return processedConfig;
@@ -249,8 +256,9 @@ function getOriginalPresetPath(config, ruleName) {
249
256
  /**
250
257
  * Save the configuration to the aicm.json file
251
258
  */
252
- function saveConfig(config) {
253
- const configPath = node_path_1.default.join(process.cwd(), CONFIG_FILE);
259
+ function saveConfig(config, cwd) {
260
+ const workingDir = cwd || process.cwd();
261
+ const configPath = node_path_1.default.join(workingDir, CONFIG_FILE);
254
262
  try {
255
263
  fs_extra_1.default.writeJsonSync(configPath, config, { spaces: 2 });
256
264
  return true;
@@ -0,0 +1,14 @@
1
+ import { Config } from "../types";
2
+ /**
3
+ * Write MCP servers configuration to IDE targets
4
+ * @param mcpServers The MCP servers configuration
5
+ * @param ides The IDEs to write to
6
+ * @param cwd The current working directory
7
+ */
8
+ export declare function writeMcpServersToTargets(mcpServers: Config["mcpServers"], ides: string[], cwd: string): void;
9
+ /**
10
+ * Write MCP servers configuration to a specific file
11
+ * @param mcpServers The MCP servers configuration
12
+ * @param mcpPath The path to the mcp.json file
13
+ */
14
+ export declare function writeMcpServersToFile(mcpServers: Config["mcpServers"], mcpPath: string): void;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeMcpServersToTargets = writeMcpServersToTargets;
7
+ exports.writeMcpServersToFile = writeMcpServersToFile;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ /**
11
+ * Write MCP servers configuration to IDE targets
12
+ * @param mcpServers The MCP servers configuration
13
+ * @param ides The IDEs to write to
14
+ * @param cwd The current working directory
15
+ */
16
+ function writeMcpServersToTargets(mcpServers, ides, cwd) {
17
+ if (!mcpServers)
18
+ return;
19
+ for (const ide of ides) {
20
+ if (ide === "cursor") {
21
+ const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json");
22
+ writeMcpServersToFile(mcpServers, mcpPath);
23
+ }
24
+ // Windsurf does not support project mcpServers, so skip
25
+ }
26
+ }
27
+ /**
28
+ * Write MCP servers configuration to a specific file
29
+ * @param mcpServers The MCP servers configuration
30
+ * @param mcpPath The path to the mcp.json file
31
+ */
32
+ function writeMcpServersToFile(mcpServers, mcpPath) {
33
+ var _a;
34
+ if (!mcpServers)
35
+ return;
36
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(mcpPath));
37
+ const existingConfig = fs_extra_1.default.existsSync(mcpPath)
38
+ ? fs_extra_1.default.readJsonSync(mcpPath)
39
+ : {};
40
+ const existingMcpServers = (_a = existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.mcpServers) !== null && _a !== void 0 ? _a : {};
41
+ // Filter out any existing aicm-managed servers (with aicm: true)
42
+ // This removes stale aicm servers that are no longer in the configuration
43
+ const userMcpServers = {};
44
+ for (const [key, value] of Object.entries(existingMcpServers)) {
45
+ if (typeof value === "object" && value !== null && value.aicm !== true) {
46
+ userMcpServers[key] = value;
47
+ }
48
+ }
49
+ // Mark new aicm servers as managed and filter out canceled servers
50
+ const aicmMcpServers = {};
51
+ for (const [key, value] of Object.entries(mcpServers)) {
52
+ if (value !== false) {
53
+ aicmMcpServers[key] = {
54
+ ...value,
55
+ aicm: true,
56
+ };
57
+ }
58
+ }
59
+ // Merge user servers with aicm servers (aicm servers override user servers with same key)
60
+ const mergedMcpServers = {
61
+ ...userMcpServers,
62
+ ...aicmMcpServers,
63
+ };
64
+ const mergedConfig = {
65
+ ...existingConfig,
66
+ mcpServers: mergedMcpServers,
67
+ };
68
+ fs_extra_1.default.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 });
69
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detect the type of package based on files present in the directory
3
+ * @param packageDir The directory to check
4
+ * @returns The detected package type
5
+ */
6
+ export declare function detectPackageType(packageDir: string): "npm" | "bazel" | "unknown";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectPackageType = detectPackageType;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * Detect the type of package based on files present in the directory
11
+ * @param packageDir The directory to check
12
+ * @returns The detected package type
13
+ */
14
+ function detectPackageType(packageDir) {
15
+ // Check for npm package
16
+ if (fs_extra_1.default.existsSync(node_path_1.default.join(packageDir, "package.json"))) {
17
+ return "npm";
18
+ }
19
+ // Check for Bazel package
20
+ if (fs_extra_1.default.existsSync(node_path_1.default.join(packageDir, "BUILD")) ||
21
+ fs_extra_1.default.existsSync(node_path_1.default.join(packageDir, "BUILD.bazel"))) {
22
+ return "bazel";
23
+ }
24
+ return "unknown";
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
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",