aicm 0.15.1 → 0.16.1
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 +49 -0
- package/dist/api.d.ts +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +150 -16
- package/dist/commands/list.js +21 -7
- package/dist/utils/config.d.ts +12 -4
- package/dist/utils/config.js +101 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,53 @@ For project-specific rules, you can specify `rulesDir` in your `aicm.json` confi
|
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
### Using Commands
|
|
99
|
+
|
|
100
|
+
Cursor supports custom commands that can be invoked directly in the chat interface. aicm can manage these command files
|
|
101
|
+
alongside your rules and MCP configurations so they install automatically into Cursor.
|
|
102
|
+
|
|
103
|
+
#### Local Commands
|
|
104
|
+
|
|
105
|
+
Add a commands directory to your project configuration:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"commandsDir": "./commands",
|
|
110
|
+
"targets": ["cursor"]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
|
|
115
|
+
|
|
116
|
+
#### Commands in Presets
|
|
117
|
+
|
|
118
|
+
Presets can ship reusable command libraries in addition to rules:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"rulesDir": "rules",
|
|
123
|
+
"commandsDir": "commands"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Preset command files install alongside local ones in `.cursor/commands/aicm/` so they appear with concise paths inside Cursor.
|
|
128
|
+
If multiple presets provide a command at the same relative path, aicm will warn during installation and use the version from the
|
|
129
|
+
last preset listed in your configuration. Use `overrides` to explicitly choose a different definition when needed.
|
|
130
|
+
|
|
131
|
+
#### Command Overrides
|
|
132
|
+
|
|
133
|
+
Use the existing `overrides` field to customize or disable commands provided by presets:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"presets": ["@team/dev-preset"],
|
|
138
|
+
"overrides": {
|
|
139
|
+
"legacy-command": false,
|
|
140
|
+
"custom-test": "./commands/test.md"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
98
145
|
### Notes
|
|
99
146
|
|
|
100
147
|
- Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
|
|
@@ -209,6 +256,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
|
|
|
209
256
|
```json
|
|
210
257
|
{
|
|
211
258
|
"rulesDir": "./rules",
|
|
259
|
+
"commandsDir": "./commands",
|
|
212
260
|
"targets": ["cursor"],
|
|
213
261
|
"presets": [],
|
|
214
262
|
"overrides": {},
|
|
@@ -218,6 +266,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
|
|
|
218
266
|
```
|
|
219
267
|
|
|
220
268
|
- **rulesDir**: Directory containing all rule files.
|
|
269
|
+
- **commandsDir**: Directory containing Cursor command files.
|
|
221
270
|
- **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`.
|
|
222
271
|
- **presets**: List of preset packages or paths to include.
|
|
223
272
|
- **overrides**: Map of rule names to `false` (disable) or a replacement file path.
|
package/dist/api.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ import { InstallOptions, InstallResult } from "./commands/install";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function install(options?: InstallOptions): Promise<InstallResult>;
|
|
8
8
|
export type { InstallOptions, InstallResult } from "./commands/install";
|
|
9
|
-
export type { ResolvedConfig, Config, RuleFile, MCPServers, } from "./utils/config";
|
|
9
|
+
export type { ResolvedConfig, Config, RuleFile, CommandFile, MCPServers, } from "./utils/config";
|
package/dist/commands/install.js
CHANGED
|
@@ -43,6 +43,19 @@ function writeCursorRules(rules, cursorRulesDir) {
|
|
|
43
43
|
fs_extra_1.default.writeFileSync(ruleFile, rule.content);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
function writeCursorCommands(commands, cursorCommandsDir) {
|
|
47
|
+
fs_extra_1.default.removeSync(cursorCommandsDir);
|
|
48
|
+
for (const command of commands) {
|
|
49
|
+
const commandNameParts = command.name
|
|
50
|
+
.replace(/\\/g, "/")
|
|
51
|
+
.split("/")
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
|
|
54
|
+
const commandFile = commandPath + ".md";
|
|
55
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
|
|
56
|
+
fs_extra_1.default.writeFileSync(commandFile, command.content);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
46
59
|
function extractNamespaceFromPresetPath(presetPath) {
|
|
47
60
|
// Special case: npm package names always use forward slashes, regardless of platform
|
|
48
61
|
if (presetPath.startsWith("@")) {
|
|
@@ -50,7 +63,7 @@ function extractNamespaceFromPresetPath(presetPath) {
|
|
|
50
63
|
return presetPath.split("/");
|
|
51
64
|
}
|
|
52
65
|
const parts = presetPath.split(node_path_1.default.sep);
|
|
53
|
-
return parts.filter((part) => part.length > 0
|
|
66
|
+
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
54
67
|
}
|
|
55
68
|
/**
|
|
56
69
|
* Write rules to a shared directory and update the given rules file
|
|
@@ -125,6 +138,79 @@ function writeRulesToTargets(rules, targets) {
|
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
140
|
}
|
|
141
|
+
function writeCommandsToTargets(commands, targets) {
|
|
142
|
+
const projectDir = process.cwd();
|
|
143
|
+
const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
|
|
144
|
+
for (const target of targets) {
|
|
145
|
+
if (target === "cursor") {
|
|
146
|
+
const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
|
|
147
|
+
writeCursorCommands(commands, commandsDir);
|
|
148
|
+
}
|
|
149
|
+
// Other targets do not support commands yet
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function warnPresetCommandCollisions(commands) {
|
|
153
|
+
const collisions = new Map();
|
|
154
|
+
for (const command of commands) {
|
|
155
|
+
if (!command.presetName)
|
|
156
|
+
continue;
|
|
157
|
+
const entry = collisions.get(command.name);
|
|
158
|
+
if (entry) {
|
|
159
|
+
entry.presets.add(command.presetName);
|
|
160
|
+
entry.lastPreset = command.presetName;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
collisions.set(command.name, {
|
|
164
|
+
presets: new Set([command.presetName]),
|
|
165
|
+
lastPreset: command.presetName,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const [commandName, { presets, lastPreset }] of collisions) {
|
|
170
|
+
if (presets.size > 1) {
|
|
171
|
+
const presetList = Array.from(presets).sort().join(", ");
|
|
172
|
+
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${commandName}" command (${presetList}). Using definition from ${lastPreset}.`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function dedupeCommandsForInstall(commands) {
|
|
177
|
+
const unique = new Map();
|
|
178
|
+
for (const command of commands) {
|
|
179
|
+
unique.set(command.name, command);
|
|
180
|
+
}
|
|
181
|
+
return Array.from(unique.values());
|
|
182
|
+
}
|
|
183
|
+
function mergeWorkspaceCommands(packages) {
|
|
184
|
+
var _a;
|
|
185
|
+
const commands = [];
|
|
186
|
+
const seenPresetCommands = new Set();
|
|
187
|
+
for (const pkg of packages) {
|
|
188
|
+
const hasCursorTarget = pkg.config.config.targets.includes("cursor");
|
|
189
|
+
if (!hasCursorTarget) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const command of (_a = pkg.config.commands) !== null && _a !== void 0 ? _a : []) {
|
|
193
|
+
if (command.presetName) {
|
|
194
|
+
const presetKey = `${command.presetName}::${command.name}`;
|
|
195
|
+
if (seenPresetCommands.has(presetKey)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
seenPresetCommands.add(presetKey);
|
|
199
|
+
}
|
|
200
|
+
commands.push(command);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return commands;
|
|
204
|
+
}
|
|
205
|
+
function collectWorkspaceCommandTargets(packages) {
|
|
206
|
+
const targets = new Set();
|
|
207
|
+
for (const pkg of packages) {
|
|
208
|
+
if (pkg.config.config.targets.includes("cursor")) {
|
|
209
|
+
targets.add("cursor");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Array.from(targets);
|
|
213
|
+
}
|
|
128
214
|
/**
|
|
129
215
|
* Write MCP servers configuration to IDE targets
|
|
130
216
|
*/
|
|
@@ -270,29 +356,35 @@ async function installPackage(options = {}) {
|
|
|
270
356
|
success: false,
|
|
271
357
|
error: new Error("Configuration file not found"),
|
|
272
358
|
installedRuleCount: 0,
|
|
359
|
+
installedCommandCount: 0,
|
|
273
360
|
packagesCount: 0,
|
|
274
361
|
};
|
|
275
362
|
}
|
|
276
|
-
const { config, rules, mcpServers } = resolvedConfig;
|
|
363
|
+
const { config, rules, commands, mcpServers } = resolvedConfig;
|
|
277
364
|
if (config.skipInstall === true) {
|
|
278
365
|
return {
|
|
279
366
|
success: true,
|
|
280
367
|
installedRuleCount: 0,
|
|
368
|
+
installedCommandCount: 0,
|
|
281
369
|
packagesCount: 0,
|
|
282
370
|
};
|
|
283
371
|
}
|
|
372
|
+
warnPresetCommandCollisions(commands);
|
|
373
|
+
const commandsToInstall = dedupeCommandsForInstall(commands);
|
|
284
374
|
try {
|
|
285
375
|
if (!options.dryRun) {
|
|
286
|
-
// Write rules to targets
|
|
287
376
|
writeRulesToTargets(rules, config.targets);
|
|
288
|
-
|
|
377
|
+
writeCommandsToTargets(commandsToInstall, config.targets);
|
|
289
378
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
290
379
|
writeMcpServersToTargets(mcpServers, config.targets, cwd);
|
|
291
380
|
}
|
|
292
381
|
}
|
|
382
|
+
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
|
|
383
|
+
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
|
|
293
384
|
return {
|
|
294
385
|
success: true,
|
|
295
|
-
installedRuleCount:
|
|
386
|
+
installedRuleCount: uniqueRuleCount,
|
|
387
|
+
installedCommandCount: uniqueCommandCount,
|
|
296
388
|
packagesCount: 1,
|
|
297
389
|
};
|
|
298
390
|
}
|
|
@@ -301,6 +393,7 @@ async function installPackage(options = {}) {
|
|
|
301
393
|
success: false,
|
|
302
394
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
303
395
|
installedRuleCount: 0,
|
|
396
|
+
installedCommandCount: 0,
|
|
304
397
|
packagesCount: 0,
|
|
305
398
|
};
|
|
306
399
|
}
|
|
@@ -312,6 +405,7 @@ async function installPackage(options = {}) {
|
|
|
312
405
|
async function installWorkspacesPackages(packages, options = {}) {
|
|
313
406
|
const results = [];
|
|
314
407
|
let totalRuleCount = 0;
|
|
408
|
+
let totalCommandCount = 0;
|
|
315
409
|
// Install packages sequentially for now (can be parallelized later)
|
|
316
410
|
for (const pkg of packages) {
|
|
317
411
|
const packagePath = pkg.absolutePath;
|
|
@@ -322,11 +416,13 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
322
416
|
config: pkg.config,
|
|
323
417
|
});
|
|
324
418
|
totalRuleCount += result.installedRuleCount;
|
|
419
|
+
totalCommandCount += result.installedCommandCount;
|
|
325
420
|
results.push({
|
|
326
421
|
path: pkg.relativePath,
|
|
327
422
|
success: result.success,
|
|
328
423
|
error: result.error,
|
|
329
424
|
installedRuleCount: result.installedRuleCount,
|
|
425
|
+
installedCommandCount: result.installedCommandCount,
|
|
330
426
|
});
|
|
331
427
|
}
|
|
332
428
|
catch (error) {
|
|
@@ -335,6 +431,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
335
431
|
success: false,
|
|
336
432
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
337
433
|
installedRuleCount: 0,
|
|
434
|
+
installedCommandCount: 0,
|
|
338
435
|
});
|
|
339
436
|
}
|
|
340
437
|
}
|
|
@@ -343,6 +440,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
343
440
|
success: failedPackages.length === 0,
|
|
344
441
|
packages: results,
|
|
345
442
|
totalRuleCount,
|
|
443
|
+
totalCommandCount,
|
|
346
444
|
};
|
|
347
445
|
}
|
|
348
446
|
/**
|
|
@@ -361,16 +459,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
361
459
|
const isRoot = pkg.relativePath === ".";
|
|
362
460
|
if (!isRoot)
|
|
363
461
|
return true;
|
|
364
|
-
// For root directories, only keep if it has rules or presets
|
|
462
|
+
// For root directories, only keep if it has rules, commands, or presets
|
|
365
463
|
const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
|
|
464
|
+
const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
|
|
366
465
|
const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
|
|
367
|
-
return hasRules || hasPresets;
|
|
466
|
+
return hasRules || hasCommands || hasPresets;
|
|
368
467
|
});
|
|
369
468
|
if (packages.length === 0) {
|
|
370
469
|
return {
|
|
371
470
|
success: false,
|
|
372
471
|
error: new Error("No packages with aicm configurations found"),
|
|
373
472
|
installedRuleCount: 0,
|
|
473
|
+
installedCommandCount: 0,
|
|
374
474
|
packagesCount: 0,
|
|
375
475
|
};
|
|
376
476
|
}
|
|
@@ -386,6 +486,17 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
386
486
|
verbose,
|
|
387
487
|
dryRun,
|
|
388
488
|
});
|
|
489
|
+
const workspaceCommands = mergeWorkspaceCommands(packages);
|
|
490
|
+
const workspaceCommandTargets = collectWorkspaceCommandTargets(packages);
|
|
491
|
+
if (workspaceCommands.length > 0) {
|
|
492
|
+
warnPresetCommandCollisions(workspaceCommands);
|
|
493
|
+
}
|
|
494
|
+
if (!dryRun &&
|
|
495
|
+
workspaceCommands.length > 0 &&
|
|
496
|
+
workspaceCommandTargets.length > 0) {
|
|
497
|
+
const dedupedWorkspaceCommands = dedupeCommandsForInstall(workspaceCommands);
|
|
498
|
+
writeCommandsToTargets(dedupedWorkspaceCommands, workspaceCommandTargets);
|
|
499
|
+
}
|
|
389
500
|
const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
|
|
390
501
|
const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
|
|
391
502
|
if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
|
|
@@ -398,7 +509,11 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
398
509
|
if (verbose) {
|
|
399
510
|
result.packages.forEach((pkg) => {
|
|
400
511
|
if (pkg.success) {
|
|
401
|
-
|
|
512
|
+
const summaryParts = [`${pkg.installedRuleCount} rules`];
|
|
513
|
+
if (pkg.installedCommandCount > 0) {
|
|
514
|
+
summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
|
|
515
|
+
}
|
|
516
|
+
console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
|
|
402
517
|
}
|
|
403
518
|
else {
|
|
404
519
|
console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
|
|
@@ -409,7 +524,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
409
524
|
if (failedPackages.length > 0) {
|
|
410
525
|
console.log(chalk_1.default.yellow(`Installation completed with errors`));
|
|
411
526
|
if (verbose) {
|
|
412
|
-
|
|
527
|
+
const commandSummary = result.totalCommandCount > 0
|
|
528
|
+
? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
|
|
529
|
+
: "";
|
|
530
|
+
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})`));
|
|
413
531
|
console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
|
|
414
532
|
}
|
|
415
533
|
const errorDetails = failedPackages
|
|
@@ -419,12 +537,14 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
419
537
|
success: false,
|
|
420
538
|
error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
|
|
421
539
|
installedRuleCount: result.totalRuleCount,
|
|
540
|
+
installedCommandCount: result.totalCommandCount,
|
|
422
541
|
packagesCount: result.packages.length,
|
|
423
542
|
};
|
|
424
543
|
}
|
|
425
544
|
return {
|
|
426
545
|
success: true,
|
|
427
546
|
installedRuleCount: result.totalRuleCount,
|
|
547
|
+
installedCommandCount: result.totalCommandCount,
|
|
428
548
|
packagesCount: result.packages.length,
|
|
429
549
|
};
|
|
430
550
|
});
|
|
@@ -441,6 +561,7 @@ async function install(options = {}) {
|
|
|
441
561
|
return {
|
|
442
562
|
success: true,
|
|
443
563
|
installedRuleCount: 0,
|
|
564
|
+
installedCommandCount: 0,
|
|
444
565
|
packagesCount: 0,
|
|
445
566
|
};
|
|
446
567
|
}
|
|
@@ -470,23 +591,36 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
470
591
|
throw (_a = result.error) !== null && _a !== void 0 ? _a : new Error("Installation failed with unknown error");
|
|
471
592
|
}
|
|
472
593
|
else {
|
|
473
|
-
const
|
|
594
|
+
const ruleCount = result.installedRuleCount;
|
|
595
|
+
const commandCount = result.installedCommandCount;
|
|
596
|
+
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
|
|
597
|
+
const commandMessage = commandCount > 0
|
|
598
|
+
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
|
|
599
|
+
: null;
|
|
600
|
+
const countsParts = [];
|
|
601
|
+
if (ruleMessage) {
|
|
602
|
+
countsParts.push(ruleMessage);
|
|
603
|
+
}
|
|
604
|
+
if (commandMessage) {
|
|
605
|
+
countsParts.push(commandMessage);
|
|
606
|
+
}
|
|
607
|
+
const countsMessage = countsParts.length > 0 ? countsParts.join(" and ") : "0 rules";
|
|
474
608
|
if (dryRun) {
|
|
475
609
|
if (result.packagesCount > 1) {
|
|
476
|
-
console.log(`Dry run: validated ${
|
|
610
|
+
console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
|
|
477
611
|
}
|
|
478
612
|
else {
|
|
479
|
-
console.log(`Dry run: validated ${
|
|
613
|
+
console.log(`Dry run: validated ${countsMessage}`);
|
|
480
614
|
}
|
|
481
615
|
}
|
|
482
|
-
else if (
|
|
483
|
-
console.log("No rules installed");
|
|
616
|
+
else if (ruleCount === 0 && commandCount === 0) {
|
|
617
|
+
console.log("No rules or commands installed");
|
|
484
618
|
}
|
|
485
619
|
else if (result.packagesCount > 1) {
|
|
486
|
-
console.log(`Successfully installed ${
|
|
620
|
+
console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
|
|
487
621
|
}
|
|
488
622
|
else {
|
|
489
|
-
console.log(`Successfully installed ${
|
|
623
|
+
console.log(`Successfully installed ${countsMessage}`);
|
|
490
624
|
}
|
|
491
625
|
}
|
|
492
626
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -13,14 +13,28 @@ async function listCommand() {
|
|
|
13
13
|
console.log(`Run ${chalk_1.default.blue("npx aicm init")} to create one.`);
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const hasRules = config.rules && config.rules.length > 0;
|
|
17
|
+
const hasCommands = config.commands && config.commands.length > 0;
|
|
18
|
+
if (!hasRules && !hasCommands) {
|
|
19
|
+
console.log(chalk_1.default.yellow("No rules or commands defined in configuration."));
|
|
20
|
+
console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules or commands.`);
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if (hasRules) {
|
|
24
|
+
console.log(chalk_1.default.blue("Configured Rules:"));
|
|
25
|
+
console.log(chalk_1.default.dim("─".repeat(50)));
|
|
26
|
+
for (const rule of config.rules) {
|
|
27
|
+
console.log(`${chalk_1.default.bold(rule.name)} - ${rule.sourcePath} ${rule.presetName ? `[${rule.presetName}]` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (hasCommands) {
|
|
31
|
+
if (hasRules) {
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk_1.default.blue("Configured Commands:"));
|
|
35
|
+
console.log(chalk_1.default.dim("─".repeat(50)));
|
|
36
|
+
for (const command of config.commands) {
|
|
37
|
+
console.log(`${chalk_1.default.bold(command.name)} - ${command.sourcePath} ${command.presetName ? `[${command.presetName}]` : ""}`);
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
40
|
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CosmiconfigResult } from "cosmiconfig";
|
|
2
2
|
export interface RawConfig {
|
|
3
3
|
rulesDir?: string;
|
|
4
|
+
commandsDir?: string;
|
|
4
5
|
targets?: string[];
|
|
5
6
|
presets?: string[];
|
|
6
7
|
overrides?: Record<string, string | false>;
|
|
@@ -10,6 +11,7 @@ export interface RawConfig {
|
|
|
10
11
|
}
|
|
11
12
|
export interface Config {
|
|
12
13
|
rulesDir?: string;
|
|
14
|
+
commandsDir?: string;
|
|
13
15
|
targets: string[];
|
|
14
16
|
presets?: string[];
|
|
15
17
|
overrides?: Record<string, string | false>;
|
|
@@ -31,22 +33,25 @@ export type MCPServer = {
|
|
|
31
33
|
export interface MCPServers {
|
|
32
34
|
[serverName: string]: MCPServer;
|
|
33
35
|
}
|
|
34
|
-
export interface
|
|
36
|
+
export interface ManagedFile {
|
|
35
37
|
name: string;
|
|
36
38
|
content: string;
|
|
37
39
|
sourcePath: string;
|
|
38
40
|
source: "local" | "preset";
|
|
39
41
|
presetName?: string;
|
|
40
42
|
}
|
|
43
|
+
export type RuleFile = ManagedFile;
|
|
44
|
+
export type CommandFile = ManagedFile;
|
|
41
45
|
export interface RuleCollection {
|
|
42
46
|
[target: string]: RuleFile[];
|
|
43
47
|
}
|
|
44
48
|
export interface ResolvedConfig {
|
|
45
49
|
config: Config;
|
|
46
50
|
rules: RuleFile[];
|
|
51
|
+
commands: CommandFile[];
|
|
47
52
|
mcpServers: MCPServers;
|
|
48
53
|
}
|
|
49
|
-
export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
|
|
54
|
+
export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "commandsDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
|
|
50
55
|
export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex", "claude"];
|
|
51
56
|
export type SupportedTarget = (typeof SUPPORTED_TARGETS)[number];
|
|
52
57
|
export declare function detectWorkspacesFromPackageJson(cwd: string): boolean;
|
|
@@ -54,16 +59,19 @@ export declare function resolveWorkspaces(config: unknown, configFilePath: strin
|
|
|
54
59
|
export declare function applyDefaults(config: RawConfig, workspaces: boolean): Config;
|
|
55
60
|
export declare function validateConfig(config: unknown, configFilePath: string, cwd: string, isWorkspaceMode?: boolean): asserts config is Config;
|
|
56
61
|
export declare function loadRulesFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
|
|
62
|
+
export declare function loadCommandsFromDirectory(commandsDir: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
|
|
57
63
|
export declare function resolvePresetPath(presetPath: string, cwd: string): string | null;
|
|
58
64
|
export declare function loadPreset(presetPath: string, cwd: string): Promise<{
|
|
59
65
|
config: Config;
|
|
60
|
-
rulesDir
|
|
66
|
+
rulesDir?: string;
|
|
67
|
+
commandsDir?: string;
|
|
61
68
|
}>;
|
|
62
69
|
export declare function loadAllRules(config: Config, cwd: string): Promise<{
|
|
63
70
|
rules: RuleFile[];
|
|
71
|
+
commands: CommandFile[];
|
|
64
72
|
mcpServers: MCPServers;
|
|
65
73
|
}>;
|
|
66
|
-
export declare function applyOverrides(
|
|
74
|
+
export declare function applyOverrides<T extends ManagedFile>(files: T[], overrides: Record<string, string | false>, cwd: string): T[];
|
|
67
75
|
export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
|
|
68
76
|
export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
|
|
69
77
|
export declare function saveConfig(config: Config, cwd?: string): boolean;
|
package/dist/utils/config.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.resolveWorkspaces = resolveWorkspaces;
|
|
|
9
9
|
exports.applyDefaults = applyDefaults;
|
|
10
10
|
exports.validateConfig = validateConfig;
|
|
11
11
|
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
|
+
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
12
13
|
exports.resolvePresetPath = resolvePresetPath;
|
|
13
14
|
exports.loadPreset = loadPreset;
|
|
14
15
|
exports.loadAllRules = loadAllRules;
|
|
@@ -22,6 +23,7 @@ const cosmiconfig_1 = require("cosmiconfig");
|
|
|
22
23
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
23
24
|
exports.ALLOWED_CONFIG_KEYS = [
|
|
24
25
|
"rulesDir",
|
|
26
|
+
"commandsDir",
|
|
25
27
|
"targets",
|
|
26
28
|
"presets",
|
|
27
29
|
"overrides",
|
|
@@ -61,6 +63,7 @@ function resolveWorkspaces(config, configFilePath, cwd) {
|
|
|
61
63
|
function applyDefaults(config, workspaces) {
|
|
62
64
|
return {
|
|
63
65
|
rulesDir: config.rulesDir,
|
|
66
|
+
commandsDir: config.commandsDir,
|
|
64
67
|
targets: config.targets || ["cursor"],
|
|
65
68
|
presets: config.presets || [],
|
|
66
69
|
overrides: config.overrides || {},
|
|
@@ -79,13 +82,14 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
79
82
|
}
|
|
80
83
|
// Validate that either rulesDir or presets is provided
|
|
81
84
|
const hasRulesDir = "rulesDir" in config && typeof config.rulesDir === "string";
|
|
85
|
+
const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
|
|
82
86
|
const hasPresets = "presets" in config &&
|
|
83
87
|
Array.isArray(config.presets) &&
|
|
84
88
|
config.presets.length > 0;
|
|
85
89
|
// In workspace mode, root config doesn't need rulesDir or presets
|
|
86
90
|
// since packages will have their own configurations
|
|
87
|
-
if (!isWorkspaceMode && !hasRulesDir && !hasPresets) {
|
|
88
|
-
throw new Error(`
|
|
91
|
+
if (!isWorkspaceMode && !hasRulesDir && !hasPresets && !hasCommandsDir) {
|
|
92
|
+
throw new Error(`At least one of rulesDir, commandsDir, or presets must be specified in config at ${configFilePath}`);
|
|
89
93
|
}
|
|
90
94
|
// Validate rulesDir if provided
|
|
91
95
|
if (hasRulesDir) {
|
|
@@ -97,6 +101,15 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
97
101
|
throw new Error(`Rules path is not a directory: ${rulesPath}`);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
104
|
+
if (hasCommandsDir) {
|
|
105
|
+
const commandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
|
|
106
|
+
if (!fs_extra_1.default.existsSync(commandsPath)) {
|
|
107
|
+
throw new Error(`Commands directory does not exist: ${commandsPath}`);
|
|
108
|
+
}
|
|
109
|
+
if (!fs_extra_1.default.statSync(commandsPath).isDirectory()) {
|
|
110
|
+
throw new Error(`Commands path is not a directory: ${commandsPath}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
100
113
|
if ("targets" in config) {
|
|
101
114
|
if (!Array.isArray(config.targets)) {
|
|
102
115
|
throw new Error(`targets must be an array in config at ${configFilePath}`);
|
|
@@ -137,6 +150,31 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
|
|
|
137
150
|
}
|
|
138
151
|
return rules;
|
|
139
152
|
}
|
|
153
|
+
async function loadCommandsFromDirectory(commandsDir, source, presetName) {
|
|
154
|
+
const commands = [];
|
|
155
|
+
if (!fs_extra_1.default.existsSync(commandsDir)) {
|
|
156
|
+
return commands;
|
|
157
|
+
}
|
|
158
|
+
const pattern = node_path_1.default.join(commandsDir, "**/*.md").replace(/\\/g, "/");
|
|
159
|
+
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
160
|
+
onlyFiles: true,
|
|
161
|
+
absolute: true,
|
|
162
|
+
});
|
|
163
|
+
filePaths.sort();
|
|
164
|
+
for (const filePath of filePaths) {
|
|
165
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
166
|
+
const relativePath = node_path_1.default.relative(commandsDir, filePath);
|
|
167
|
+
const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
168
|
+
commands.push({
|
|
169
|
+
name: commandName,
|
|
170
|
+
content,
|
|
171
|
+
sourcePath: filePath,
|
|
172
|
+
source,
|
|
173
|
+
presetName,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return commands;
|
|
177
|
+
}
|
|
140
178
|
function resolvePresetPath(presetPath, cwd) {
|
|
141
179
|
// Support specifying aicm.json directory and load the config from it
|
|
142
180
|
if (!presetPath.endsWith(".json")) {
|
|
@@ -173,20 +211,25 @@ async function loadPreset(presetPath, cwd) {
|
|
|
173
211
|
catch (error) {
|
|
174
212
|
throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
175
213
|
}
|
|
176
|
-
// Validate that preset has rulesDir
|
|
177
|
-
if (!presetConfig.rulesDir) {
|
|
178
|
-
throw new Error(`Preset "${presetPath}" must have a rulesDir specified`);
|
|
179
|
-
}
|
|
180
|
-
// Resolve preset's rules directory relative to the preset file
|
|
181
214
|
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
182
|
-
const presetRulesDir =
|
|
215
|
+
const presetRulesDir = presetConfig.rulesDir
|
|
216
|
+
? node_path_1.default.resolve(presetDir, presetConfig.rulesDir)
|
|
217
|
+
: undefined;
|
|
218
|
+
const presetCommandsDir = presetConfig.commandsDir
|
|
219
|
+
? node_path_1.default.resolve(presetDir, presetConfig.commandsDir)
|
|
220
|
+
: undefined;
|
|
221
|
+
if (!presetRulesDir && !presetCommandsDir) {
|
|
222
|
+
throw new Error(`Preset "${presetPath}" must have a rulesDir or commandsDir specified`);
|
|
223
|
+
}
|
|
183
224
|
return {
|
|
184
225
|
config: presetConfig,
|
|
185
226
|
rulesDir: presetRulesDir,
|
|
227
|
+
commandsDir: presetCommandsDir,
|
|
186
228
|
};
|
|
187
229
|
}
|
|
188
230
|
async function loadAllRules(config, cwd) {
|
|
189
231
|
const allRules = [];
|
|
232
|
+
const allCommands = [];
|
|
190
233
|
let mergedMcpServers = { ...config.mcpServers };
|
|
191
234
|
// Load local rules only if rulesDir is provided
|
|
192
235
|
if (config.rulesDir) {
|
|
@@ -194,11 +237,22 @@ async function loadAllRules(config, cwd) {
|
|
|
194
237
|
const localRules = await loadRulesFromDirectory(localRulesPath, "local");
|
|
195
238
|
allRules.push(...localRules);
|
|
196
239
|
}
|
|
240
|
+
if (config.commandsDir) {
|
|
241
|
+
const localCommandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
|
|
242
|
+
const localCommands = await loadCommandsFromDirectory(localCommandsPath, "local");
|
|
243
|
+
allCommands.push(...localCommands);
|
|
244
|
+
}
|
|
197
245
|
if (config.presets) {
|
|
198
246
|
for (const presetPath of config.presets) {
|
|
199
247
|
const preset = await loadPreset(presetPath, cwd);
|
|
200
|
-
|
|
201
|
-
|
|
248
|
+
if (preset.rulesDir) {
|
|
249
|
+
const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
|
|
250
|
+
allRules.push(...presetRules);
|
|
251
|
+
}
|
|
252
|
+
if (preset.commandsDir) {
|
|
253
|
+
const presetCommands = await loadCommandsFromDirectory(preset.commandsDir, "preset", presetPath);
|
|
254
|
+
allCommands.push(...presetCommands);
|
|
255
|
+
}
|
|
202
256
|
// Merge MCP servers from preset
|
|
203
257
|
if (preset.config.mcpServers) {
|
|
204
258
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
@@ -207,41 +261,43 @@ async function loadAllRules(config, cwd) {
|
|
|
207
261
|
}
|
|
208
262
|
return {
|
|
209
263
|
rules: allRules,
|
|
264
|
+
commands: allCommands,
|
|
210
265
|
mcpServers: mergedMcpServers,
|
|
211
266
|
};
|
|
212
267
|
}
|
|
213
|
-
function applyOverrides(
|
|
214
|
-
// Validate that all override
|
|
215
|
-
for (const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!rules.some((rule) => rule.name === ruleName)) {
|
|
219
|
-
throw new Error(`Override rule "${ruleName}" does not exist in resolved rules`);
|
|
268
|
+
function applyOverrides(files, overrides, cwd) {
|
|
269
|
+
// Validate that all override names exist in the resolved files
|
|
270
|
+
for (const name of Object.keys(overrides)) {
|
|
271
|
+
if (!files.some((file) => file.name === name)) {
|
|
272
|
+
throw new Error(`Override entry "${name}" does not exist in resolved files`);
|
|
220
273
|
}
|
|
221
274
|
}
|
|
222
|
-
const
|
|
223
|
-
for (const
|
|
224
|
-
|
|
275
|
+
const fileMap = new Map();
|
|
276
|
+
for (const file of files) {
|
|
277
|
+
fileMap.set(file.name, file);
|
|
225
278
|
}
|
|
226
|
-
for (const [
|
|
279
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
227
280
|
if (override === false) {
|
|
228
|
-
|
|
281
|
+
fileMap.delete(name);
|
|
229
282
|
}
|
|
230
283
|
else if (typeof override === "string") {
|
|
231
284
|
const overridePath = node_path_1.default.resolve(cwd, override);
|
|
232
285
|
if (!fs_extra_1.default.existsSync(overridePath)) {
|
|
233
|
-
throw new Error(`Override
|
|
286
|
+
throw new Error(`Override file not found: ${override} in ${cwd}`);
|
|
234
287
|
}
|
|
235
288
|
const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
|
|
236
|
-
|
|
237
|
-
|
|
289
|
+
const existing = fileMap.get(name);
|
|
290
|
+
fileMap.set(name, {
|
|
291
|
+
...(existing !== null && existing !== void 0 ? existing : {}),
|
|
292
|
+
name,
|
|
238
293
|
content,
|
|
239
294
|
sourcePath: overridePath,
|
|
240
295
|
source: "local",
|
|
296
|
+
presetName: undefined,
|
|
241
297
|
});
|
|
242
298
|
}
|
|
243
299
|
}
|
|
244
|
-
return Array.from(
|
|
300
|
+
return Array.from(fileMap.values());
|
|
245
301
|
}
|
|
246
302
|
/**
|
|
247
303
|
* Merge preset MCP servers with local config MCP servers
|
|
@@ -285,14 +341,31 @@ async function loadConfig(cwd) {
|
|
|
285
341
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
286
342
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
287
343
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
288
|
-
const { rules, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
344
|
+
const { rules, commands, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
289
345
|
let rulesWithOverrides = rules;
|
|
346
|
+
let commandsWithOverrides = commands;
|
|
290
347
|
if (configWithDefaults.overrides) {
|
|
291
|
-
|
|
348
|
+
const overrides = configWithDefaults.overrides;
|
|
349
|
+
const ruleNames = new Set(rules.map((rule) => rule.name));
|
|
350
|
+
const commandNames = new Set(commands.map((command) => command.name));
|
|
351
|
+
for (const overrideName of Object.keys(overrides)) {
|
|
352
|
+
if (!ruleNames.has(overrideName) && !commandNames.has(overrideName)) {
|
|
353
|
+
throw new Error(`Override entry "${overrideName}" does not exist in resolved rules or commands`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const ruleOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => ruleNames.has(name)));
|
|
357
|
+
const commandOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => commandNames.has(name)));
|
|
358
|
+
if (Object.keys(ruleOverrides).length > 0) {
|
|
359
|
+
rulesWithOverrides = applyOverrides(rules, ruleOverrides, workingDir);
|
|
360
|
+
}
|
|
361
|
+
if (Object.keys(commandOverrides).length > 0) {
|
|
362
|
+
commandsWithOverrides = applyOverrides(commands, commandOverrides, workingDir);
|
|
363
|
+
}
|
|
292
364
|
}
|
|
293
365
|
return {
|
|
294
366
|
config: configWithDefaults,
|
|
295
367
|
rules: rulesWithOverrides,
|
|
368
|
+
commands: commandsWithOverrides,
|
|
296
369
|
mcpServers,
|
|
297
370
|
};
|
|
298
371
|
}
|