aicm 0.9.0 → 0.10.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
@@ -147,6 +147,44 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
147
147
 
148
148
  To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
149
149
 
150
+ ## Workspaces Support
151
+
152
+ aicm supports workspaces by automatically discovering and installing configurations across multiple packages in your repository.
153
+
154
+ To enable workspaces mode, use the `--workspaces` flag:
155
+
156
+ ```bash
157
+ npx aicm install --workspaces
158
+ ```
159
+
160
+ This will:
161
+
162
+ 1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository
163
+ 2. **Install per package**: Install rules and MCPs for each package individually in their respective directories
164
+
165
+ ### How It Works
166
+
167
+ Each directory containing an `aicm.json` file is treated as a separate package with its own configuration.
168
+
169
+ For example, in a workspace structure like:
170
+
171
+ ```
172
+ ├── packages/
173
+ │ ├── frontend/
174
+ │ │ └── aicm.json
175
+ │ └── backend/
176
+ │ └── aicm.json
177
+ └── services/
178
+ └── api/
179
+ └── aicm.json
180
+ ```
181
+
182
+ Running `npx aicm install --workspaces` will install rules for each package in their respective directories:
183
+
184
+ - `packages/frontend/.cursor/rules/aicm/`
185
+ - `packages/backend/.cursor/rules/aicm/`
186
+ - `services/api/.cursor/rules/aicm/`
187
+
150
188
  ## Configuration
151
189
 
152
190
  To configure aicm, use either:
@@ -252,6 +290,8 @@ npx aicm install
252
290
  Options:
253
291
 
254
292
  - `--ci`: run in CI environments (default: `false`)
293
+ - `--workspaces`: enable workspaces mode to discover and install configurations across multiple packages
294
+ - `--verbose`: show detailed output during installation
255
295
 
256
296
  ## Node.js API
257
297
 
@@ -296,6 +336,8 @@ Installs rules and MCP servers based on configuration.
296
336
  - `cwd`: Base directory to use instead of `process.cwd()`
297
337
  - `config`: Custom config object to use instead of loading from file
298
338
  - `installOnCI`: Run installation on CI environments (default: `false`)
339
+ - `workspaces`: Enable workspaces mode (default: `false`)
340
+ - `verbose`: Show verbose output during installation (default: `false`)
299
341
 
300
342
  **Returns:**
301
343
 
@@ -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>;
@@ -13,6 +13,26 @@ const rule_writer_1 = require("../utils/rule-writer");
13
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const ci_info_1 = require("ci-info");
16
+ const discovery_1 = require("./workspaces/discovery");
17
+ const workspaces_install_1 = require("./workspaces/workspaces-install");
18
+ /**
19
+ * Helper function to execute a function within a specific working directory
20
+ * and ensure the original directory is always restored
21
+ */
22
+ async function withWorkingDirectory(targetDir, fn) {
23
+ const originalCwd = process.cwd();
24
+ if (targetDir !== originalCwd) {
25
+ process.chdir(targetDir);
26
+ }
27
+ try {
28
+ return await fn();
29
+ }
30
+ finally {
31
+ if (targetDir !== originalCwd) {
32
+ process.chdir(originalCwd);
33
+ }
34
+ }
35
+ }
16
36
  /**
17
37
  * Write MCP servers configuration to IDE targets
18
38
  * @param mcpServers The MCP servers configuration
@@ -47,6 +67,64 @@ function isInCIEnvironment() {
47
67
  // Fall back to ci-info's detection
48
68
  return ci_info_1.isCI;
49
69
  }
70
+ async function handleWorkspacesInstallation(cwd, installOnCI, verbose = false) {
71
+ return withWorkingDirectory(cwd, async () => {
72
+ if (verbose) {
73
+ console.log(chalk_1.default.blue("🔍 Discovering packages..."));
74
+ }
75
+ const packages = await (0, discovery_1.discoverPackagesWithAicm)(cwd);
76
+ if (packages.length === 0) {
77
+ return {
78
+ success: false,
79
+ error: "No packages with aicm configurations found",
80
+ installedRuleCount: 0,
81
+ packagesCount: 0,
82
+ };
83
+ }
84
+ if (verbose) {
85
+ console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations:`));
86
+ packages.forEach((pkg) => {
87
+ console.log(chalk_1.default.gray(` - ${pkg.relativePath}`));
88
+ });
89
+ console.log(chalk_1.default.blue(`📦 Installing configurations...`));
90
+ }
91
+ const result = await (0, workspaces_install_1.installWorkspacesPackages)(packages, {
92
+ installOnCI,
93
+ });
94
+ if (verbose) {
95
+ result.packages.forEach((pkg) => {
96
+ if (pkg.success) {
97
+ console.log(chalk_1.default.green(`✅ ${pkg.path} (${pkg.installedRuleCount} rules)`));
98
+ }
99
+ else {
100
+ console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
101
+ }
102
+ });
103
+ }
104
+ const failedPackages = result.packages.filter((r) => !r.success);
105
+ if (failedPackages.length > 0) {
106
+ console.log(chalk_1.default.yellow(`Installation completed with errors`));
107
+ if (verbose) {
108
+ console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rules total)`));
109
+ console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
110
+ }
111
+ const errorDetails = failedPackages
112
+ .map((p) => `${p.path}: ${p.error}`)
113
+ .join("; ");
114
+ return {
115
+ success: false,
116
+ error: `Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`,
117
+ installedRuleCount: result.totalRuleCount,
118
+ packagesCount: result.packages.length,
119
+ };
120
+ }
121
+ return {
122
+ success: true,
123
+ installedRuleCount: result.totalRuleCount,
124
+ packagesCount: result.packages.length,
125
+ };
126
+ });
127
+ }
50
128
  /**
51
129
  * Core implementation of the rule installation logic
52
130
  * @param options Install options
@@ -55,45 +133,38 @@ function isInCIEnvironment() {
55
133
  async function install(options = {}) {
56
134
  const cwd = options.cwd || process.cwd();
57
135
  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);
136
+ return withWorkingDirectory(cwd, async () => {
137
+ if (options.workspaces) {
138
+ return await handleWorkspacesInstallation(cwd, installOnCI, options.verbose);
62
139
  }
63
- const ruleCollection = (0, rule_collector_1.initRuleCollection)();
64
140
  const config = options.config || (0, config_1.getConfig)();
141
+ const ruleCollection = (0, rule_collector_1.initRuleCollection)();
65
142
  if (!config) {
66
- if (cwd !== originalCwd) {
67
- process.chdir(originalCwd);
68
- }
69
143
  return {
70
144
  success: false,
71
- error: "Configuration file not found!",
145
+ error: "Configuration file not found",
72
146
  installedRuleCount: 0,
147
+ packagesCount: 0,
73
148
  };
74
149
  }
75
150
  const inCI = isInCIEnvironment();
76
151
  if (inCI && !installOnCI && !config.installOnCI) {
77
- if (cwd !== originalCwd) {
78
- process.chdir(originalCwd);
79
- }
80
152
  console.log(chalk_1.default.yellow("Detected CI environment, skipping install."));
81
153
  return {
82
154
  success: true,
83
155
  installedRuleCount: 0,
156
+ packagesCount: 0,
84
157
  };
85
158
  }
86
159
  // Check if rules are defined (either directly or through presets)
87
160
  if (!config.rules || Object.keys(config.rules).length === 0) {
88
161
  // If there are no presets defined either, show a message
89
162
  if (!config.presets || config.presets.length === 0) {
90
- if (cwd !== originalCwd) {
91
- process.chdir(originalCwd);
92
- }
93
163
  return {
94
164
  success: false,
95
- error: "No rules defined in configuration.",
165
+ error: "No rules defined in configuration",
96
166
  installedRuleCount: 0,
167
+ packagesCount: 0,
97
168
  };
98
169
  }
99
170
  }
@@ -139,13 +210,11 @@ async function install(options = {}) {
139
210
  }
140
211
  // If there were errors, exit with error
141
212
  if (hasErrors) {
142
- if (cwd !== originalCwd) {
143
- process.chdir(originalCwd);
144
- }
145
213
  return {
146
214
  success: false,
147
215
  error: errorMessages.join("; "),
148
216
  installedRuleCount,
217
+ packagesCount: 0,
149
218
  };
150
219
  }
151
220
  // Write all collected rules to their targets
@@ -156,37 +225,30 @@ async function install(options = {}) {
156
225
  const filteredMcpServers = Object.fromEntries(Object.entries(config.mcpServers).filter(([, v]) => v !== false));
157
226
  writeMcpServersToTargets(filteredMcpServers, config.ides, cwd);
158
227
  }
159
- // Restore original cwd
160
- if (cwd !== originalCwd) {
161
- process.chdir(originalCwd);
162
- }
163
228
  return {
164
229
  success: true,
165
230
  installedRuleCount,
231
+ packagesCount: 1,
166
232
  };
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
- }
233
+ });
180
234
  }
181
- async function installCommand(installOnCI) {
235
+ async function installCommand(installOnCI, workspaces, verbose) {
182
236
  try {
183
- const result = await install({ installOnCI });
237
+ const result = await install({ installOnCI, workspaces, verbose });
184
238
  if (!result.success) {
185
239
  console.error(chalk_1.default.red(result.error));
186
240
  process.exit(1);
187
241
  }
188
242
  else {
189
- console.log("Rules installation completed");
243
+ if (result.packagesCount > 1) {
244
+ console.log(`Successfully installed ${result.installedRuleCount} rules across ${result.packagesCount} packages`);
245
+ }
246
+ else if (workspaces) {
247
+ console.log(`Successfully installed ${result.installedRuleCount} rules across ${result.packagesCount} packages`);
248
+ }
249
+ else {
250
+ console.log("Rules installation completed");
251
+ }
190
252
  }
191
253
  }
192
254
  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
+ }
@@ -1,29 +1,34 @@
1
1
  import { Config, Rules } from "../types";
2
- interface ConfigWithMeta extends Config {
3
- __ruleSources?: Record<string, string>;
4
- __originalPresetPaths?: Record<string, string>;
2
+ export interface RuleMetadata {
3
+ ruleSources: Record<string, string>;
4
+ originalPresetPaths: Record<string, string>;
5
+ }
6
+ export interface ConfigResult {
7
+ config: Config;
8
+ metadata: RuleMetadata;
5
9
  }
6
10
  export interface PresetPathInfo {
7
11
  fullPath: string;
8
12
  originalPath: string;
9
13
  }
10
- export declare function getFullPresetPath(presetPath: string): PresetPathInfo | null;
14
+ export declare function getFullPresetPath(presetPath: string, cwd?: string): PresetPathInfo | null;
11
15
  /**
12
- * Load a preset file and return its rules and mcpServers
16
+ * Load a preset file and return its contents
13
17
  */
14
- export declare function loadPreset(presetPath: string): {
18
+ export declare function loadPreset(presetPath: string, cwd?: string): {
15
19
  rules: Rules;
16
20
  mcpServers?: import("../types").MCPServers;
21
+ presets?: string[];
17
22
  } | null;
18
23
  /**
19
24
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
20
25
  * Returns the config object or null if not found.
21
26
  */
22
- export declare function loadAicmConfigCosmiconfig(): ConfigWithMeta | null;
27
+ export declare function loadAicmConfigCosmiconfig(searchFrom?: string): Config | null;
23
28
  /**
24
29
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
25
30
  */
26
- export declare function getConfig(): Config | null;
31
+ export declare function getConfig(cwd?: string): Config | null;
27
32
  /**
28
33
  * Get the source preset path for a rule if it came from a preset
29
34
  */
@@ -35,5 +40,4 @@ export declare function getOriginalPresetPath(config: Config, ruleName: string):
35
40
  /**
36
41
  * Save the configuration to the aicm.json file
37
42
  */
38
- export declare function saveConfig(config: Config): boolean;
39
- export {};
43
+ export declare function saveConfig(config: Config, cwd?: string): boolean;
@@ -14,36 +14,62 @@ 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 (presetPath.endsWith(".json") && fs_extra_1.default.pathExistsSync(presetPath)) {
19
- 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
+ }
20
27
  }
21
28
  try {
22
29
  let absolutePresetPath;
30
+ // Handle npm package with explicit JSON path
23
31
  if (presetPath.endsWith(".json")) {
24
32
  absolutePresetPath = require.resolve(presetPath, {
25
- paths: [__dirname, process.cwd()],
33
+ paths: [__dirname, workingDir],
26
34
  });
27
35
  }
36
+ // Handle npm package without explicit JSON path (add aicm.json)
28
37
  else {
38
+ // For npm packages, ensure we properly handle scoped packages (@org/pkg)
29
39
  const presetPathWithConfig = node_path_1.default.join(presetPath, "aicm.json");
30
- absolutePresetPath = require.resolve(presetPathWithConfig, {
31
- paths: [__dirname, process.cwd()],
32
- });
40
+ try {
41
+ absolutePresetPath = require.resolve(presetPathWithConfig, {
42
+ paths: [__dirname, workingDir],
43
+ });
44
+ }
45
+ catch (_a) {
46
+ // If direct resolution fails, try as a package name
47
+ absolutePresetPath = require.resolve(presetPath, {
48
+ paths: [__dirname, workingDir],
49
+ });
50
+ // If we found the package but not the config file, look for aicm.json
51
+ if (fs_extra_1.default.existsSync(absolutePresetPath)) {
52
+ const packageDir = node_path_1.default.dirname(absolutePresetPath);
53
+ const configPath = node_path_1.default.join(packageDir, "aicm.json");
54
+ if (fs_extra_1.default.existsSync(configPath)) {
55
+ absolutePresetPath = configPath;
56
+ }
57
+ }
58
+ }
33
59
  }
34
60
  return fs_extra_1.default.existsSync(absolutePresetPath)
35
61
  ? { fullPath: absolutePresetPath, originalPath: presetPath }
36
62
  : null;
37
63
  }
38
- catch (_a) {
64
+ catch (_b) {
39
65
  return null;
40
66
  }
41
67
  }
42
68
  /**
43
- * Load a preset file and return its rules and mcpServers
69
+ * Load a preset file and return its contents
44
70
  */
45
- function loadPreset(presetPath) {
46
- const pathInfo = getFullPresetPath(presetPath);
71
+ function loadPreset(presetPath, cwd) {
72
+ const pathInfo = getFullPresetPath(presetPath, cwd);
47
73
  if (!pathInfo) {
48
74
  throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
49
75
  }
@@ -59,82 +85,133 @@ function loadPreset(presetPath) {
59
85
  if (!preset.rules || typeof preset.rules !== "object") {
60
86
  throw new Error(`Error loading preset: Invalid format in ${presetPath} - missing or invalid 'rules' object`);
61
87
  }
62
- return { rules: preset.rules, mcpServers: preset.mcpServers };
88
+ return {
89
+ rules: preset.rules,
90
+ mcpServers: preset.mcpServers,
91
+ presets: preset.presets,
92
+ };
93
+ }
94
+ // Global metadata storage
95
+ let currentMetadata = null;
96
+ // Track processed presets to avoid circular references
97
+ const processedPresets = new Set();
98
+ /**
99
+ * Process presets and return a new config with merged rules and metadata
100
+ */
101
+ function processPresets(config, cwd) {
102
+ // Create a deep copy of the config to avoid mutations
103
+ const newConfig = JSON.parse(JSON.stringify(config));
104
+ const metadata = {
105
+ ruleSources: {},
106
+ originalPresetPaths: {},
107
+ };
108
+ // Clear processed presets tracking set when starting from the top level
109
+ processedPresets.clear();
110
+ return processPresetsInternal(newConfig, metadata, cwd);
63
111
  }
64
112
  /**
65
- * Process presets and merge their rules and mcpServers into the config
113
+ * Internal function to process presets recursively
66
114
  */
67
- function processPresets(config) {
115
+ function processPresetsInternal(config, metadata, cwd) {
68
116
  if (!config.presets || !Array.isArray(config.presets)) {
69
- return;
117
+ return { config, metadata };
70
118
  }
71
119
  for (const presetPath of config.presets) {
72
- const preset = loadPreset(presetPath);
73
- if (!preset)
120
+ const pathInfo = getFullPresetPath(presetPath, cwd);
121
+ if (!pathInfo) {
122
+ throw new Error(`Error loading preset: "${presetPath}". Make sure the package is installed in your project.`);
123
+ }
124
+ // Skip if we've already processed this preset (prevents circular references)
125
+ if (processedPresets.has(pathInfo.fullPath)) {
126
+ console.warn(`Skipping already processed preset: ${presetPath}`);
74
127
  continue;
75
- const pathInfo = getFullPresetPath(presetPath);
76
- if (!pathInfo)
128
+ }
129
+ // Mark this preset as processed
130
+ processedPresets.add(pathInfo.fullPath);
131
+ const preset = loadPreset(presetPath, cwd);
132
+ if (!preset)
77
133
  continue;
78
- mergePresetRules(config, preset.rules, pathInfo);
134
+ // Process nested presets first (depth-first)
135
+ if (preset.presets && preset.presets.length > 0) {
136
+ // Create a temporary config with just the presets from this preset
137
+ const presetConfig = {
138
+ rules: {},
139
+ presets: preset.presets,
140
+ ides: [],
141
+ };
142
+ // Recursively process the nested presets
143
+ const { config: nestedConfig } = processPresetsInternal(presetConfig, metadata, cwd);
144
+ Object.assign(preset.rules, nestedConfig.rules);
145
+ }
146
+ const { updatedConfig, updatedMetadata } = mergePresetRules(config, preset.rules, pathInfo, metadata);
147
+ Object.assign(config.rules, updatedConfig.rules);
148
+ Object.assign(metadata.ruleSources, updatedMetadata.ruleSources);
149
+ Object.assign(metadata.originalPresetPaths, updatedMetadata.originalPresetPaths);
79
150
  if (preset.mcpServers) {
80
- mergePresetMcpServers(config, preset.mcpServers);
151
+ config.mcpServers = mergePresetMcpServers(config.mcpServers || {}, preset.mcpServers);
81
152
  }
82
153
  }
154
+ return { config, metadata };
83
155
  }
84
156
  /**
85
- * Merge preset rules into the config
157
+ * Merge preset rules into the config without mutation
86
158
  */
87
- function mergePresetRules(config, presetRules, pathInfo) {
159
+ function mergePresetRules(config, presetRules, pathInfo, metadata) {
160
+ const updatedRules = { ...config.rules };
161
+ const updatedMetadata = {
162
+ ruleSources: { ...metadata.ruleSources },
163
+ originalPresetPaths: { ...metadata.originalPresetPaths },
164
+ };
88
165
  for (const [ruleName, rulePath] of Object.entries(presetRules)) {
89
166
  // Cancel if set to false in config
90
167
  if (Object.prototype.hasOwnProperty.call(config.rules, ruleName) &&
91
168
  config.rules[ruleName] === false) {
92
- delete config.rules[ruleName];
93
- if (config.__ruleSources)
94
- delete config.__ruleSources[ruleName];
95
- if (config.__originalPresetPaths)
96
- delete config.__originalPresetPaths[ruleName];
169
+ delete updatedRules[ruleName];
170
+ delete updatedMetadata.ruleSources[ruleName];
171
+ delete updatedMetadata.originalPresetPaths[ruleName];
97
172
  continue;
98
173
  }
99
174
  // Only add if not already defined in config (override handled by config)
100
175
  if (!Object.prototype.hasOwnProperty.call(config.rules, ruleName)) {
101
- config.rules[ruleName] = rulePath;
102
- config.__ruleSources = config.__ruleSources || {};
103
- config.__ruleSources[ruleName] = pathInfo.fullPath;
104
- config.__originalPresetPaths = config.__originalPresetPaths || {};
105
- config.__originalPresetPaths[ruleName] = pathInfo.originalPath;
176
+ updatedRules[ruleName] = rulePath;
177
+ updatedMetadata.ruleSources[ruleName] = pathInfo.fullPath;
178
+ updatedMetadata.originalPresetPaths[ruleName] = pathInfo.originalPath;
106
179
  }
107
180
  }
181
+ return {
182
+ updatedConfig: { ...config, rules: updatedRules },
183
+ updatedMetadata,
184
+ };
108
185
  }
109
186
  /**
110
- * Merge preset mcpServers into the config
187
+ * Merge preset mcpServers without mutation
111
188
  */
112
- function mergePresetMcpServers(config, presetMcpServers) {
113
- if (!config.mcpServers)
114
- config.mcpServers = {};
189
+ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
190
+ const newMcpServers = { ...configMcpServers };
115
191
  for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) {
116
192
  // Cancel if set to false in config
117
- if (Object.prototype.hasOwnProperty.call(config.mcpServers, serverName) &&
118
- config.mcpServers[serverName] === false) {
119
- delete config.mcpServers[serverName];
193
+ if (Object.prototype.hasOwnProperty.call(newMcpServers, serverName) &&
194
+ newMcpServers[serverName] === false) {
195
+ delete newMcpServers[serverName];
120
196
  continue;
121
197
  }
122
198
  // Only add if not already defined in config (override handled by config)
123
- if (!Object.prototype.hasOwnProperty.call(config.mcpServers, serverName)) {
124
- config.mcpServers[serverName] = serverConfig;
199
+ if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
200
+ newMcpServers[serverName] = serverConfig;
125
201
  }
126
202
  }
203
+ return newMcpServers;
127
204
  }
128
205
  /**
129
206
  * Load the aicm config using cosmiconfigSync, supporting both aicm.json and package.json.
130
207
  * Returns the config object or null if not found.
131
208
  */
132
- function loadAicmConfigCosmiconfig() {
209
+ function loadAicmConfigCosmiconfig(searchFrom) {
133
210
  const explorer = (0, cosmiconfig_1.cosmiconfigSync)("aicm", {
134
211
  searchPlaces: ["package.json", "aicm.json"],
135
212
  });
136
213
  try {
137
- const result = explorer.search();
214
+ const result = explorer.search(searchFrom);
138
215
  if (!result || !result.config)
139
216
  return null;
140
217
  const config = result.config;
@@ -151,33 +228,37 @@ function loadAicmConfigCosmiconfig() {
151
228
  /**
152
229
  * Get the configuration from aicm.json or package.json (using cosmiconfigSync) and merge with any presets
153
230
  */
154
- function getConfig() {
155
- const config = loadAicmConfigCosmiconfig();
231
+ function getConfig(cwd) {
232
+ const workingDir = cwd || process.cwd();
233
+ const config = loadAicmConfigCosmiconfig(workingDir);
156
234
  if (!config) {
157
- 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"`);
158
236
  }
159
- processPresets(config);
160
- return config;
237
+ const { config: processedConfig, metadata } = processPresets(config, workingDir);
238
+ // Store metadata for later access
239
+ currentMetadata = metadata;
240
+ return processedConfig;
161
241
  }
162
242
  /**
163
243
  * Get the source preset path for a rule if it came from a preset
164
244
  */
165
245
  function getRuleSource(config, ruleName) {
166
246
  var _a;
167
- return (_a = config.__ruleSources) === null || _a === void 0 ? void 0 : _a[ruleName];
247
+ return (_a = currentMetadata === null || currentMetadata === void 0 ? void 0 : currentMetadata.ruleSources) === null || _a === void 0 ? void 0 : _a[ruleName];
168
248
  }
169
249
  /**
170
250
  * Get the original preset path for a rule if it came from a preset
171
251
  */
172
252
  function getOriginalPresetPath(config, ruleName) {
173
253
  var _a;
174
- return (_a = config.__originalPresetPaths) === null || _a === void 0 ? void 0 : _a[ruleName];
254
+ return (_a = currentMetadata === null || currentMetadata === void 0 ? void 0 : currentMetadata.originalPresetPaths) === null || _a === void 0 ? void 0 : _a[ruleName];
175
255
  }
176
256
  /**
177
257
  * Save the configuration to the aicm.json file
178
258
  */
179
- function saveConfig(config) {
180
- 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);
181
262
  try {
182
263
  fs_extra_1.default.writeJsonSync(configPath, config, { spaces: 2 });
183
264
  return true;
@@ -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.0",
3
+ "version": "0.10.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",