aicm 0.17.3 → 0.18.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 +17 -0
- package/dist/commands/install-workspaces.d.ts +5 -0
- package/dist/commands/install-workspaces.js +367 -0
- package/dist/commands/install.d.ts +10 -1
- package/dist/commands/install.js +21 -225
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -215,6 +215,17 @@ When installed, `aicm` will automatically rewrite the link to point to the corre
|
|
|
215
215
|
|
|
216
216
|
> **Note:** Path rewriting works for any relative path format in your commands - markdown links, inline code references, or bare paths - as long as they point to actual files in your `rulesDir`.
|
|
217
217
|
|
|
218
|
+
#### usage in workspaces mode
|
|
219
|
+
|
|
220
|
+
When using workspaces, commands installed at the monorepo root need to access auxiliary files located in nested packages (e.g., `packages/frontend/rules/helper.js`).
|
|
221
|
+
|
|
222
|
+
`aicm` handles this automatically by:
|
|
223
|
+
|
|
224
|
+
1. Copying referenced auxiliary files from nested packages to the root `.cursor/rules/aicm/` directory
|
|
225
|
+
2. Rewriting paths in the root command to point to these copied files
|
|
226
|
+
|
|
227
|
+
**Warning:** If your command references a `.mdc` file (Cursor rule), `aicm` will check if it's a "manual" rule or an "automatic" rule (one that is always applied or auto-attached via globs). If it's an automatic rule, `aicm` will warn you that copying it to the root might cause the rule to be included twice in the context (once from the nested package and once from the root copy). For best results, only reference manual `.mdc` files or other file types (like `.js`, `.json`, `.md`) from commands.
|
|
228
|
+
|
|
218
229
|
### Overrides
|
|
219
230
|
|
|
220
231
|
You can disable or replace specific rules or commands provided by presets using the `overrides` field:
|
|
@@ -271,6 +282,12 @@ Running `npx aicm install` will install rules for each package in their respecti
|
|
|
271
282
|
- `packages/backend/.cursor/rules/aicm/`
|
|
272
283
|
- `services/api/.cursor/rules/aicm/`
|
|
273
284
|
|
|
285
|
+
**Why install in both places?**
|
|
286
|
+
`aicm` installs configurations at both the package level AND the root level to support different workflows:
|
|
287
|
+
|
|
288
|
+
- **Package-level context:** When a developer opens a specific package folder (e.g., `packages/frontend`) in their IDE, they get the specific rules, commands, and MCP servers for that package.
|
|
289
|
+
- **Root-level context:** When a developer opens the monorepo root, `aicm` ensures they have access to all commands and MCP servers from all packages via the merged root configuration. While rules are typically read from nested directories by Cursor, commands and MCP servers must be configured at the root to be accessible.
|
|
290
|
+
|
|
274
291
|
### Preset Packages in Workspaces
|
|
275
292
|
|
|
276
293
|
When you have a preset package within your workspace (a package that provides rules to be consumed by others), you can prevent aicm from installing rules into it by setting `skipInstall: true`:
|
|
@@ -0,0 +1,367 @@
|
|
|
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.installWorkspaces = installWorkspaces;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const working_directory_1 = require("../utils/working-directory");
|
|
11
|
+
const rules_file_writer_1 = require("../utils/rules-file-writer");
|
|
12
|
+
const workspace_discovery_1 = require("../utils/workspace-discovery");
|
|
13
|
+
const install_1 = require("./install");
|
|
14
|
+
/**
|
|
15
|
+
* Extract .mdc file references from a command for warning purposes
|
|
16
|
+
* Returns absolute paths to .mdc files that are referenced
|
|
17
|
+
*/
|
|
18
|
+
function extractMdcReferences(content, commandSourcePath) {
|
|
19
|
+
const commandDir = node_path_1.default.dirname(commandSourcePath);
|
|
20
|
+
const mdcFiles = [];
|
|
21
|
+
const seenPaths = new Set();
|
|
22
|
+
// Same regex pattern as rewriteCommandRelativeLinks
|
|
23
|
+
const matches = content.matchAll(/\.\.[/\\][\w\-/\\.]+/g);
|
|
24
|
+
for (const match of matches) {
|
|
25
|
+
const relativePath = match[0];
|
|
26
|
+
const resolved = node_path_1.default.normalize(node_path_1.default.resolve(commandDir, relativePath));
|
|
27
|
+
// Only process .mdc files that exist
|
|
28
|
+
if (resolved.endsWith(".mdc") &&
|
|
29
|
+
!seenPaths.has(resolved) &&
|
|
30
|
+
fs_extra_1.default.existsSync(resolved) &&
|
|
31
|
+
fs_extra_1.default.statSync(resolved).isFile()) {
|
|
32
|
+
seenPaths.add(resolved);
|
|
33
|
+
mdcFiles.push(resolved);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return mdcFiles;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a .mdc file is a manual rule (not automatic/auto-attached/agent-requested)
|
|
40
|
+
*/
|
|
41
|
+
function isManualRule(content) {
|
|
42
|
+
const metadata = (0, rules_file_writer_1.parseRuleFrontmatter)(content);
|
|
43
|
+
// Check for always rules
|
|
44
|
+
if (metadata.type === "always" ||
|
|
45
|
+
metadata.alwaysApply === true ||
|
|
46
|
+
metadata.alwaysApply === "true") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// Check for auto-attached rules
|
|
50
|
+
if (metadata.type === "auto-attached" || metadata.globs) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// Check for agent-requested rules
|
|
54
|
+
if (metadata.type === "agent-requested" || metadata.description) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Default to manual rule
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Process .mdc files referenced by commands in workspace mode
|
|
62
|
+
* Warns about non-manual rules and copies them to root for command access
|
|
63
|
+
*/
|
|
64
|
+
function processMdcFilesForWorkspace(mdcFilePaths, commands, packages, rootDir) {
|
|
65
|
+
const mdcAssets = [];
|
|
66
|
+
// Build a map of command source paths to their preset names
|
|
67
|
+
const commandPresetMap = new Map();
|
|
68
|
+
for (const command of commands) {
|
|
69
|
+
const commandDir = node_path_1.default.dirname(command.sourcePath);
|
|
70
|
+
commandPresetMap.set(commandDir, command.presetName);
|
|
71
|
+
}
|
|
72
|
+
for (const mdcPath of mdcFilePaths) {
|
|
73
|
+
const content = fs_extra_1.default.readFileSync(mdcPath, "utf8");
|
|
74
|
+
if (!isManualRule(content)) {
|
|
75
|
+
const relativePath = node_path_1.default.basename(mdcPath);
|
|
76
|
+
console.warn(chalk_1.default.yellow(`Warning: Command references non-manual rule file "${relativePath}". ` +
|
|
77
|
+
`This may cause the rule to be included twice in the context. ` +
|
|
78
|
+
`Consider using manual rules (without alwaysApply, globs, or description metadata) ` +
|
|
79
|
+
`when referencing from commands.`));
|
|
80
|
+
}
|
|
81
|
+
// Find which command references this .mdc file and get its preset name
|
|
82
|
+
let mdcAssetName = "";
|
|
83
|
+
let presetName;
|
|
84
|
+
let found = false;
|
|
85
|
+
// First check packages for local files
|
|
86
|
+
for (const pkg of packages) {
|
|
87
|
+
const rulesDir = pkg.config.config.rulesDir;
|
|
88
|
+
if (rulesDir) {
|
|
89
|
+
const pkgRulesPath = node_path_1.default.join(pkg.absolutePath, rulesDir);
|
|
90
|
+
if (mdcPath.startsWith(pkgRulesPath)) {
|
|
91
|
+
mdcAssetName = node_path_1.default.relative(pkgRulesPath, mdcPath);
|
|
92
|
+
found = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// If not found in local packages, it might be from a preset
|
|
98
|
+
if (!found) {
|
|
99
|
+
// Find the command that references this .mdc file
|
|
100
|
+
for (const command of commands) {
|
|
101
|
+
if (command.presetName) {
|
|
102
|
+
const commandDir = node_path_1.default.dirname(command.sourcePath);
|
|
103
|
+
const rulesDir = node_path_1.default.join(node_path_1.default.dirname(commandDir), "rules");
|
|
104
|
+
if (mdcPath.startsWith(rulesDir)) {
|
|
105
|
+
mdcAssetName = node_path_1.default.relative(rulesDir, mdcPath);
|
|
106
|
+
presetName = command.presetName;
|
|
107
|
+
found = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (found) {
|
|
114
|
+
// Build the final asset name with preset namespace if applicable
|
|
115
|
+
const finalAssetName = presetName
|
|
116
|
+
? node_path_1.default.posix.join(...(0, install_1.extractNamespaceFromPresetPath)(presetName), mdcAssetName.replace(/\\/g, "/"))
|
|
117
|
+
: mdcAssetName.replace(/\\/g, "/");
|
|
118
|
+
// Create an AssetFile entry for the .mdc file
|
|
119
|
+
mdcAssets.push({
|
|
120
|
+
name: finalAssetName,
|
|
121
|
+
content: Buffer.from(content),
|
|
122
|
+
sourcePath: mdcPath,
|
|
123
|
+
source: presetName ? "preset" : "local",
|
|
124
|
+
presetName,
|
|
125
|
+
});
|
|
126
|
+
// Copy to root .cursor/rules/aicm/
|
|
127
|
+
const cursorRulesDir = node_path_1.default.join(rootDir, ".cursor", "rules", "aicm");
|
|
128
|
+
const targetPath = node_path_1.default.join(cursorRulesDir, finalAssetName);
|
|
129
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(targetPath));
|
|
130
|
+
fs_extra_1.default.writeFileSync(targetPath, content);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return mdcAssets;
|
|
134
|
+
}
|
|
135
|
+
function mergeWorkspaceCommands(packages) {
|
|
136
|
+
var _a;
|
|
137
|
+
const commands = [];
|
|
138
|
+
const seenPresetCommands = new Set();
|
|
139
|
+
for (const pkg of packages) {
|
|
140
|
+
const hasCursorTarget = pkg.config.config.targets.includes("cursor");
|
|
141
|
+
if (!hasCursorTarget) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (const command of (_a = pkg.config.commands) !== null && _a !== void 0 ? _a : []) {
|
|
145
|
+
if (command.presetName) {
|
|
146
|
+
const presetKey = `${command.presetName}::${command.name}`;
|
|
147
|
+
if (seenPresetCommands.has(presetKey)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
seenPresetCommands.add(presetKey);
|
|
151
|
+
}
|
|
152
|
+
commands.push(command);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return commands;
|
|
156
|
+
}
|
|
157
|
+
function collectWorkspaceCommandTargets(packages) {
|
|
158
|
+
const targets = new Set();
|
|
159
|
+
for (const pkg of packages) {
|
|
160
|
+
if (pkg.config.config.targets.includes("cursor")) {
|
|
161
|
+
targets.add("cursor");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return Array.from(targets);
|
|
165
|
+
}
|
|
166
|
+
function mergeWorkspaceMcpServers(packages) {
|
|
167
|
+
const merged = {};
|
|
168
|
+
const info = {};
|
|
169
|
+
for (const pkg of packages) {
|
|
170
|
+
for (const [key, value] of Object.entries(pkg.config.mcpServers)) {
|
|
171
|
+
if (value === false)
|
|
172
|
+
continue;
|
|
173
|
+
const json = JSON.stringify(value);
|
|
174
|
+
if (!info[key]) {
|
|
175
|
+
info[key] = {
|
|
176
|
+
configs: new Set([json]),
|
|
177
|
+
packages: [pkg.relativePath],
|
|
178
|
+
chosen: pkg.relativePath,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
info[key].packages.push(pkg.relativePath);
|
|
183
|
+
info[key].configs.add(json);
|
|
184
|
+
info[key].chosen = pkg.relativePath;
|
|
185
|
+
}
|
|
186
|
+
merged[key] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const conflicts = [];
|
|
190
|
+
for (const [key, data] of Object.entries(info)) {
|
|
191
|
+
if (data.configs.size > 1) {
|
|
192
|
+
conflicts.push({ key, packages: data.packages, chosen: data.chosen });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { merged, conflicts };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Install aicm configurations for all packages in a workspace
|
|
199
|
+
*/
|
|
200
|
+
async function installWorkspacesPackages(packages, options = {}) {
|
|
201
|
+
const results = [];
|
|
202
|
+
let totalRuleCount = 0;
|
|
203
|
+
let totalCommandCount = 0;
|
|
204
|
+
let totalAssetCount = 0;
|
|
205
|
+
// Install packages sequentially for now (can be parallelized later)
|
|
206
|
+
for (const pkg of packages) {
|
|
207
|
+
const packagePath = pkg.absolutePath;
|
|
208
|
+
try {
|
|
209
|
+
const result = await (0, install_1.installPackage)({
|
|
210
|
+
...options,
|
|
211
|
+
cwd: packagePath,
|
|
212
|
+
config: pkg.config,
|
|
213
|
+
});
|
|
214
|
+
totalRuleCount += result.installedRuleCount;
|
|
215
|
+
totalCommandCount += result.installedCommandCount;
|
|
216
|
+
totalAssetCount += result.installedAssetCount;
|
|
217
|
+
results.push({
|
|
218
|
+
path: pkg.relativePath,
|
|
219
|
+
success: result.success,
|
|
220
|
+
error: result.error,
|
|
221
|
+
installedRuleCount: result.installedRuleCount,
|
|
222
|
+
installedCommandCount: result.installedCommandCount,
|
|
223
|
+
installedAssetCount: result.installedAssetCount,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
results.push({
|
|
228
|
+
path: pkg.relativePath,
|
|
229
|
+
success: false,
|
|
230
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
231
|
+
installedRuleCount: 0,
|
|
232
|
+
installedCommandCount: 0,
|
|
233
|
+
installedAssetCount: 0,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const failedPackages = results.filter((r) => !r.success);
|
|
238
|
+
return {
|
|
239
|
+
success: failedPackages.length === 0,
|
|
240
|
+
packages: results,
|
|
241
|
+
totalRuleCount,
|
|
242
|
+
totalCommandCount,
|
|
243
|
+
totalAssetCount,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Install rules across multiple packages in a workspace
|
|
248
|
+
*/
|
|
249
|
+
async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = false) {
|
|
250
|
+
return (0, working_directory_1.withWorkingDirectory)(cwd, async () => {
|
|
251
|
+
if (verbose) {
|
|
252
|
+
console.log(chalk_1.default.blue("🔍 Discovering packages..."));
|
|
253
|
+
}
|
|
254
|
+
const allPackages = await (0, workspace_discovery_1.discoverPackagesWithAicm)(cwd);
|
|
255
|
+
const packages = allPackages.filter((pkg) => {
|
|
256
|
+
if (pkg.config.config.skipInstall === true) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const isRoot = pkg.relativePath === ".";
|
|
260
|
+
if (!isRoot)
|
|
261
|
+
return true;
|
|
262
|
+
// For root directories, only keep if it has rules, commands, or presets
|
|
263
|
+
const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
|
|
264
|
+
const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
|
|
265
|
+
const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
|
|
266
|
+
return hasRules || hasCommands || hasPresets;
|
|
267
|
+
});
|
|
268
|
+
if (packages.length === 0) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: new Error("No packages with aicm configurations found"),
|
|
272
|
+
installedRuleCount: 0,
|
|
273
|
+
installedCommandCount: 0,
|
|
274
|
+
installedAssetCount: 0,
|
|
275
|
+
packagesCount: 0,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (verbose) {
|
|
279
|
+
console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations:`));
|
|
280
|
+
packages.forEach((pkg) => {
|
|
281
|
+
console.log(chalk_1.default.gray(` - ${pkg.relativePath}`));
|
|
282
|
+
});
|
|
283
|
+
console.log(chalk_1.default.blue(`📦 Installing configurations...`));
|
|
284
|
+
}
|
|
285
|
+
const result = await installWorkspacesPackages(packages, {
|
|
286
|
+
installOnCI,
|
|
287
|
+
verbose,
|
|
288
|
+
dryRun,
|
|
289
|
+
});
|
|
290
|
+
const workspaceCommands = mergeWorkspaceCommands(packages);
|
|
291
|
+
const workspaceCommandTargets = collectWorkspaceCommandTargets(packages);
|
|
292
|
+
if (workspaceCommands.length > 0) {
|
|
293
|
+
(0, install_1.warnPresetCommandCollisions)(workspaceCommands);
|
|
294
|
+
}
|
|
295
|
+
if (!dryRun &&
|
|
296
|
+
workspaceCommands.length > 0 &&
|
|
297
|
+
workspaceCommandTargets.length > 0) {
|
|
298
|
+
const dedupedWorkspaceCommands = (0, install_1.dedupeCommandsForInstall)(workspaceCommands);
|
|
299
|
+
// Collect all assets from packages for command path rewriting
|
|
300
|
+
const allAssets = packages.flatMap((pkg) => { var _a; return (_a = pkg.config.assets) !== null && _a !== void 0 ? _a : []; });
|
|
301
|
+
// Copy assets to root so root commands can reference them
|
|
302
|
+
(0, install_1.writeAssetsToTargets)(allAssets, workspaceCommandTargets);
|
|
303
|
+
// Extract and process .mdc file references from commands
|
|
304
|
+
const mdcFilesSet = new Set();
|
|
305
|
+
for (const command of dedupedWorkspaceCommands) {
|
|
306
|
+
const mdcRefs = extractMdcReferences(command.content, command.sourcePath);
|
|
307
|
+
mdcRefs.forEach((ref) => mdcFilesSet.add(ref));
|
|
308
|
+
}
|
|
309
|
+
const mdcAssets = processMdcFilesForWorkspace(mdcFilesSet, dedupedWorkspaceCommands, packages, cwd);
|
|
310
|
+
// Merge .mdc assets with regular assets for link rewriting
|
|
311
|
+
const allAssetsWithMdc = [...allAssets, ...mdcAssets];
|
|
312
|
+
(0, install_1.writeCommandsToTargets)(dedupedWorkspaceCommands, allAssetsWithMdc, workspaceCommandTargets);
|
|
313
|
+
}
|
|
314
|
+
const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
|
|
315
|
+
const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
|
|
316
|
+
if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
|
|
317
|
+
const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json");
|
|
318
|
+
(0, install_1.writeMcpServersToFile)(rootMcp, mcpPath);
|
|
319
|
+
}
|
|
320
|
+
for (const conflict of conflicts) {
|
|
321
|
+
console.warn(`Warning: MCP configuration conflict detected\n Key: "${conflict.key}"\n Packages: ${conflict.packages.join(", ")}\n Using configuration from: ${conflict.chosen}`);
|
|
322
|
+
}
|
|
323
|
+
if (verbose) {
|
|
324
|
+
result.packages.forEach((pkg) => {
|
|
325
|
+
if (pkg.success) {
|
|
326
|
+
const summaryParts = [`${pkg.installedRuleCount} rules`];
|
|
327
|
+
if (pkg.installedCommandCount > 0) {
|
|
328
|
+
summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const failedPackages = result.packages.filter((r) => !r.success);
|
|
338
|
+
if (failedPackages.length > 0) {
|
|
339
|
+
console.log(chalk_1.default.yellow(`Installation completed with errors`));
|
|
340
|
+
if (verbose) {
|
|
341
|
+
const commandSummary = result.totalCommandCount > 0
|
|
342
|
+
? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
|
|
343
|
+
: "";
|
|
344
|
+
console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary})`));
|
|
345
|
+
console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
|
|
346
|
+
}
|
|
347
|
+
const errorDetails = failedPackages
|
|
348
|
+
.map((p) => `${p.path}: ${p.error}`)
|
|
349
|
+
.join("; ");
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
|
|
353
|
+
installedRuleCount: result.totalRuleCount,
|
|
354
|
+
installedCommandCount: result.totalCommandCount,
|
|
355
|
+
installedAssetCount: result.totalAssetCount,
|
|
356
|
+
packagesCount: result.packages.length,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
success: true,
|
|
361
|
+
installedRuleCount: result.totalRuleCount,
|
|
362
|
+
installedCommandCount: result.totalCommandCount,
|
|
363
|
+
installedAssetCount: result.totalAssetCount,
|
|
364
|
+
packagesCount: result.packages.length,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResolvedConfig } from "../utils/config";
|
|
1
|
+
import { ResolvedConfig, CommandFile, AssetFile, MCPServers, SupportedTarget } from "../utils/config";
|
|
2
2
|
export interface InstallOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Base directory to use instead of process.cwd()
|
|
@@ -50,6 +50,15 @@ export interface InstallResult {
|
|
|
50
50
|
*/
|
|
51
51
|
packagesCount: number;
|
|
52
52
|
}
|
|
53
|
+
export declare function extractNamespaceFromPresetPath(presetPath: string): string[];
|
|
54
|
+
export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
|
|
55
|
+
export declare function writeCommandsToTargets(commands: CommandFile[], assets: AssetFile[], targets: SupportedTarget[]): void;
|
|
56
|
+
export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
|
|
57
|
+
export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
|
|
58
|
+
/**
|
|
59
|
+
* Write MCP servers configuration to a specific file
|
|
60
|
+
*/
|
|
61
|
+
export declare function writeMcpServersToFile(mcpServers: MCPServers, mcpPath: string): void;
|
|
53
62
|
/**
|
|
54
63
|
* Install rules for a single package (used within workspaces and standalone installs)
|
|
55
64
|
*/
|
package/dist/commands/install.js
CHANGED
|
@@ -3,6 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
7
|
+
exports.writeAssetsToTargets = writeAssetsToTargets;
|
|
8
|
+
exports.writeCommandsToTargets = writeCommandsToTargets;
|
|
9
|
+
exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
|
|
10
|
+
exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
|
|
11
|
+
exports.writeMcpServersToFile = writeMcpServersToFile;
|
|
6
12
|
exports.installPackage = installPackage;
|
|
7
13
|
exports.install = install;
|
|
8
14
|
exports.installCommand = installCommand;
|
|
@@ -13,7 +19,7 @@ const config_1 = require("../utils/config");
|
|
|
13
19
|
const working_directory_1 = require("../utils/working-directory");
|
|
14
20
|
const is_ci_1 = require("../utils/is-ci");
|
|
15
21
|
const rules_file_writer_1 = require("../utils/rules-file-writer");
|
|
16
|
-
const
|
|
22
|
+
const install_workspaces_1 = require("./install-workspaces");
|
|
17
23
|
function getTargetPaths() {
|
|
18
24
|
const projectDir = process.cwd();
|
|
19
25
|
return {
|
|
@@ -62,7 +68,19 @@ function writeCursorCommands(commands, cursorCommandsDir, assets) {
|
|
|
62
68
|
}
|
|
63
69
|
function rewriteCommandRelativeLinks(content, commandSourcePath, assets) {
|
|
64
70
|
const commandDir = node_path_1.default.dirname(commandSourcePath);
|
|
65
|
-
const assetMap = new Map(assets.map((a) =>
|
|
71
|
+
const assetMap = new Map(assets.map((a) => {
|
|
72
|
+
let targetPath;
|
|
73
|
+
if (a.presetName) {
|
|
74
|
+
const namespace = extractNamespaceFromPresetPath(a.presetName);
|
|
75
|
+
// Use posix paths for URLs/links (always forward slashes)
|
|
76
|
+
targetPath = node_path_1.default.posix.join(...namespace, a.name);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Normalize to posix for consistent forward slashes in links
|
|
80
|
+
targetPath = a.name.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
|
|
81
|
+
}
|
|
82
|
+
return [node_path_1.default.normalize(a.sourcePath), targetPath];
|
|
83
|
+
}));
|
|
66
84
|
return content.replace(/\.\.[/\\][\w\-/\\.]+/g, (match) => {
|
|
67
85
|
const resolved = node_path_1.default.normalize(node_path_1.default.resolve(commandDir, match));
|
|
68
86
|
return assetMap.has(resolved)
|
|
@@ -227,37 +245,6 @@ function dedupeCommandsForInstall(commands) {
|
|
|
227
245
|
}
|
|
228
246
|
return Array.from(unique.values());
|
|
229
247
|
}
|
|
230
|
-
function mergeWorkspaceCommands(packages) {
|
|
231
|
-
var _a;
|
|
232
|
-
const commands = [];
|
|
233
|
-
const seenPresetCommands = new Set();
|
|
234
|
-
for (const pkg of packages) {
|
|
235
|
-
const hasCursorTarget = pkg.config.config.targets.includes("cursor");
|
|
236
|
-
if (!hasCursorTarget) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
for (const command of (_a = pkg.config.commands) !== null && _a !== void 0 ? _a : []) {
|
|
240
|
-
if (command.presetName) {
|
|
241
|
-
const presetKey = `${command.presetName}::${command.name}`;
|
|
242
|
-
if (seenPresetCommands.has(presetKey)) {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
seenPresetCommands.add(presetKey);
|
|
246
|
-
}
|
|
247
|
-
commands.push(command);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return commands;
|
|
251
|
-
}
|
|
252
|
-
function collectWorkspaceCommandTargets(packages) {
|
|
253
|
-
const targets = new Set();
|
|
254
|
-
for (const pkg of packages) {
|
|
255
|
-
if (pkg.config.config.targets.includes("cursor")) {
|
|
256
|
-
targets.add("cursor");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return Array.from(targets);
|
|
260
|
-
}
|
|
261
248
|
/**
|
|
262
249
|
* Write MCP servers configuration to IDE targets
|
|
263
250
|
*/
|
|
@@ -311,37 +298,6 @@ function writeMcpServersToFile(mcpServers, mcpPath) {
|
|
|
311
298
|
};
|
|
312
299
|
fs_extra_1.default.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 });
|
|
313
300
|
}
|
|
314
|
-
function mergeWorkspaceMcpServers(packages) {
|
|
315
|
-
const merged = {};
|
|
316
|
-
const info = {};
|
|
317
|
-
for (const pkg of packages) {
|
|
318
|
-
for (const [key, value] of Object.entries(pkg.config.mcpServers)) {
|
|
319
|
-
if (value === false)
|
|
320
|
-
continue;
|
|
321
|
-
const json = JSON.stringify(value);
|
|
322
|
-
if (!info[key]) {
|
|
323
|
-
info[key] = {
|
|
324
|
-
configs: new Set([json]),
|
|
325
|
-
packages: [pkg.relativePath],
|
|
326
|
-
chosen: pkg.relativePath,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
info[key].packages.push(pkg.relativePath);
|
|
331
|
-
info[key].configs.add(json);
|
|
332
|
-
info[key].chosen = pkg.relativePath;
|
|
333
|
-
}
|
|
334
|
-
merged[key] = value;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
const conflicts = [];
|
|
338
|
-
for (const [key, data] of Object.entries(info)) {
|
|
339
|
-
if (data.configs.size > 1) {
|
|
340
|
-
conflicts.push({ key, packages: data.packages, chosen: data.chosen });
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return { merged, conflicts };
|
|
344
|
-
}
|
|
345
301
|
/**
|
|
346
302
|
* Install rules for a single package (used within workspaces and standalone installs)
|
|
347
303
|
*/
|
|
@@ -407,166 +363,6 @@ async function installPackage(options = {}) {
|
|
|
407
363
|
}
|
|
408
364
|
});
|
|
409
365
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Install aicm configurations for all packages in a workspace
|
|
412
|
-
*/
|
|
413
|
-
async function installWorkspacesPackages(packages, options = {}) {
|
|
414
|
-
const results = [];
|
|
415
|
-
let totalRuleCount = 0;
|
|
416
|
-
let totalCommandCount = 0;
|
|
417
|
-
let totalAssetCount = 0;
|
|
418
|
-
// Install packages sequentially for now (can be parallelized later)
|
|
419
|
-
for (const pkg of packages) {
|
|
420
|
-
const packagePath = pkg.absolutePath;
|
|
421
|
-
try {
|
|
422
|
-
const result = await installPackage({
|
|
423
|
-
...options,
|
|
424
|
-
cwd: packagePath,
|
|
425
|
-
config: pkg.config,
|
|
426
|
-
});
|
|
427
|
-
totalRuleCount += result.installedRuleCount;
|
|
428
|
-
totalCommandCount += result.installedCommandCount;
|
|
429
|
-
totalAssetCount += result.installedAssetCount;
|
|
430
|
-
results.push({
|
|
431
|
-
path: pkg.relativePath,
|
|
432
|
-
success: result.success,
|
|
433
|
-
error: result.error,
|
|
434
|
-
installedRuleCount: result.installedRuleCount,
|
|
435
|
-
installedCommandCount: result.installedCommandCount,
|
|
436
|
-
installedAssetCount: result.installedAssetCount,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
results.push({
|
|
441
|
-
path: pkg.relativePath,
|
|
442
|
-
success: false,
|
|
443
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
444
|
-
installedRuleCount: 0,
|
|
445
|
-
installedCommandCount: 0,
|
|
446
|
-
installedAssetCount: 0,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const failedPackages = results.filter((r) => !r.success);
|
|
451
|
-
return {
|
|
452
|
-
success: failedPackages.length === 0,
|
|
453
|
-
packages: results,
|
|
454
|
-
totalRuleCount,
|
|
455
|
-
totalCommandCount,
|
|
456
|
-
totalAssetCount,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Install rules across multiple packages in a workspace
|
|
461
|
-
*/
|
|
462
|
-
async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = false) {
|
|
463
|
-
return (0, working_directory_1.withWorkingDirectory)(cwd, async () => {
|
|
464
|
-
if (verbose) {
|
|
465
|
-
console.log(chalk_1.default.blue("🔍 Discovering packages..."));
|
|
466
|
-
}
|
|
467
|
-
const allPackages = await (0, workspace_discovery_1.discoverPackagesWithAicm)(cwd);
|
|
468
|
-
const packages = allPackages.filter((pkg) => {
|
|
469
|
-
if (pkg.config.config.skipInstall === true) {
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
const isRoot = pkg.relativePath === ".";
|
|
473
|
-
if (!isRoot)
|
|
474
|
-
return true;
|
|
475
|
-
// For root directories, only keep if it has rules, commands, or presets
|
|
476
|
-
const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
|
|
477
|
-
const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
|
|
478
|
-
const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
|
|
479
|
-
return hasRules || hasCommands || hasPresets;
|
|
480
|
-
});
|
|
481
|
-
if (packages.length === 0) {
|
|
482
|
-
return {
|
|
483
|
-
success: false,
|
|
484
|
-
error: new Error("No packages with aicm configurations found"),
|
|
485
|
-
installedRuleCount: 0,
|
|
486
|
-
installedCommandCount: 0,
|
|
487
|
-
installedAssetCount: 0,
|
|
488
|
-
packagesCount: 0,
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
if (verbose) {
|
|
492
|
-
console.log(chalk_1.default.blue(`Found ${packages.length} packages with aicm configurations:`));
|
|
493
|
-
packages.forEach((pkg) => {
|
|
494
|
-
console.log(chalk_1.default.gray(` - ${pkg.relativePath}`));
|
|
495
|
-
});
|
|
496
|
-
console.log(chalk_1.default.blue(`📦 Installing configurations...`));
|
|
497
|
-
}
|
|
498
|
-
const result = await installWorkspacesPackages(packages, {
|
|
499
|
-
installOnCI,
|
|
500
|
-
verbose,
|
|
501
|
-
dryRun,
|
|
502
|
-
});
|
|
503
|
-
const workspaceCommands = mergeWorkspaceCommands(packages);
|
|
504
|
-
const workspaceCommandTargets = collectWorkspaceCommandTargets(packages);
|
|
505
|
-
if (workspaceCommands.length > 0) {
|
|
506
|
-
warnPresetCommandCollisions(workspaceCommands);
|
|
507
|
-
}
|
|
508
|
-
if (!dryRun &&
|
|
509
|
-
workspaceCommands.length > 0 &&
|
|
510
|
-
workspaceCommandTargets.length > 0) {
|
|
511
|
-
const dedupedWorkspaceCommands = dedupeCommandsForInstall(workspaceCommands);
|
|
512
|
-
// Collect all assets from packages for command path rewriting
|
|
513
|
-
const allAssets = packages.flatMap((pkg) => { var _a; return (_a = pkg.config.assets) !== null && _a !== void 0 ? _a : []; });
|
|
514
|
-
writeCommandsToTargets(dedupedWorkspaceCommands, allAssets, workspaceCommandTargets);
|
|
515
|
-
}
|
|
516
|
-
const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
|
|
517
|
-
const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
|
|
518
|
-
if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
|
|
519
|
-
const mcpPath = node_path_1.default.join(cwd, ".cursor", "mcp.json");
|
|
520
|
-
writeMcpServersToFile(rootMcp, mcpPath);
|
|
521
|
-
}
|
|
522
|
-
for (const conflict of conflicts) {
|
|
523
|
-
console.warn(`Warning: MCP configuration conflict detected\n Key: "${conflict.key}"\n Packages: ${conflict.packages.join(", ")}\n Using configuration from: ${conflict.chosen}`);
|
|
524
|
-
}
|
|
525
|
-
if (verbose) {
|
|
526
|
-
result.packages.forEach((pkg) => {
|
|
527
|
-
if (pkg.success) {
|
|
528
|
-
const summaryParts = [`${pkg.installedRuleCount} rules`];
|
|
529
|
-
if (pkg.installedCommandCount > 0) {
|
|
530
|
-
summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
|
|
531
|
-
}
|
|
532
|
-
console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
const failedPackages = result.packages.filter((r) => !r.success);
|
|
540
|
-
if (failedPackages.length > 0) {
|
|
541
|
-
console.log(chalk_1.default.yellow(`Installation completed with errors`));
|
|
542
|
-
if (verbose) {
|
|
543
|
-
const commandSummary = result.totalCommandCount > 0
|
|
544
|
-
? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
|
|
545
|
-
: "";
|
|
546
|
-
console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary})`));
|
|
547
|
-
console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
|
|
548
|
-
}
|
|
549
|
-
const errorDetails = failedPackages
|
|
550
|
-
.map((p) => `${p.path}: ${p.error}`)
|
|
551
|
-
.join("; ");
|
|
552
|
-
return {
|
|
553
|
-
success: false,
|
|
554
|
-
error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
|
|
555
|
-
installedRuleCount: result.totalRuleCount,
|
|
556
|
-
installedCommandCount: result.totalCommandCount,
|
|
557
|
-
installedAssetCount: result.totalAssetCount,
|
|
558
|
-
packagesCount: result.packages.length,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
success: true,
|
|
563
|
-
installedRuleCount: result.totalRuleCount,
|
|
564
|
-
installedCommandCount: result.totalCommandCount,
|
|
565
|
-
installedAssetCount: result.totalAssetCount,
|
|
566
|
-
packagesCount: result.packages.length,
|
|
567
|
-
};
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
366
|
/**
|
|
571
367
|
* Core implementation of the rule installation logic
|
|
572
368
|
*/
|
|
@@ -595,7 +391,7 @@ async function install(options = {}) {
|
|
|
595
391
|
const shouldUseWorkspaces = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.config.workspaces) ||
|
|
596
392
|
(!resolvedConfig && (0, config_1.detectWorkspacesFromPackageJson)(cwd));
|
|
597
393
|
if (shouldUseWorkspaces) {
|
|
598
|
-
return await installWorkspaces(cwd, installOnCI, options.verbose, options.dryRun);
|
|
394
|
+
return await (0, install_workspaces_1.installWorkspaces)(cwd, installOnCI, options.verbose, options.dryRun);
|
|
599
395
|
}
|
|
600
396
|
return installPackage(options);
|
|
601
397
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"format:check": "prettier --check .",
|
|
64
64
|
"lint": "eslint",
|
|
65
65
|
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
66
|
-
"release": "np"
|
|
66
|
+
"release": "np --no-tests"
|
|
67
67
|
}
|
|
68
68
|
}
|