aicm 0.15.0 → 0.16.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 +52 -1
- package/dist/api.d.ts +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +114 -16
- package/dist/commands/list.js +21 -7
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +107 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ Modern AI-powered IDEs like Cursor and Agents like Codex enable developers to wr
|
|
|
17
17
|
aicm accepts Cursor's `.mdc` format as it provides the most comprehensive feature set. For other AI tools and IDEs, aicm automatically generates compatible formats:
|
|
18
18
|
|
|
19
19
|
- **Cursor**: Native `.mdc` files with full feature support
|
|
20
|
-
- **Windsurf
|
|
20
|
+
- **Windsurf**: Generates `.windsurfrules` file
|
|
21
|
+
- **Codex**: Generates `AGENTS.md` file
|
|
22
|
+
- **Claude**: Generates `CLAUDE.md` file
|
|
21
23
|
|
|
22
24
|
This approach ensures you write your rules once in the richest format available, while maintaining compatibility across different AI development environments.
|
|
23
25
|
|
|
@@ -93,6 +95,53 @@ For project-specific rules, you can specify `rulesDir` in your `aicm.json` confi
|
|
|
93
95
|
}
|
|
94
96
|
```
|
|
95
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
|
+
|
|
96
145
|
### Notes
|
|
97
146
|
|
|
98
147
|
- Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
|
|
@@ -207,6 +256,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
|
|
|
207
256
|
```json
|
|
208
257
|
{
|
|
209
258
|
"rulesDir": "./rules",
|
|
259
|
+
"commandsDir": "./commands",
|
|
210
260
|
"targets": ["cursor"],
|
|
211
261
|
"presets": [],
|
|
212
262
|
"overrides": {},
|
|
@@ -216,6 +266,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
|
|
|
216
266
|
```
|
|
217
267
|
|
|
218
268
|
- **rulesDir**: Directory containing all rule files.
|
|
269
|
+
- **commandsDir**: Directory containing Cursor command files.
|
|
219
270
|
- **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`.
|
|
220
271
|
- **presets**: List of preset packages or paths to include.
|
|
221
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
|
@@ -20,6 +20,7 @@ function getTargetPaths() {
|
|
|
20
20
|
cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
|
|
21
21
|
windsurf: node_path_1.default.join(projectDir, ".aicm"),
|
|
22
22
|
codex: node_path_1.default.join(projectDir, ".aicm"),
|
|
23
|
+
claude: node_path_1.default.join(projectDir, ".aicm"),
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
function writeCursorRules(rules, cursorRulesDir) {
|
|
@@ -42,6 +43,19 @@ function writeCursorRules(rules, cursorRulesDir) {
|
|
|
42
43
|
fs_extra_1.default.writeFileSync(ruleFile, rule.content);
|
|
43
44
|
}
|
|
44
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
|
+
}
|
|
45
59
|
function extractNamespaceFromPresetPath(presetPath) {
|
|
46
60
|
// Special case: npm package names always use forward slashes, regardless of platform
|
|
47
61
|
if (presetPath.startsWith("@")) {
|
|
@@ -49,7 +63,7 @@ function extractNamespaceFromPresetPath(presetPath) {
|
|
|
49
63
|
return presetPath.split("/");
|
|
50
64
|
}
|
|
51
65
|
const parts = presetPath.split(node_path_1.default.sep);
|
|
52
|
-
return parts.filter((part) => part.length > 0
|
|
66
|
+
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
55
69
|
* Write rules to a shared directory and update the given rules file
|
|
@@ -116,8 +130,55 @@ function writeRulesToTargets(rules, targets) {
|
|
|
116
130
|
writeRulesForFile(rules, targetPaths.codex, "AGENTS.md");
|
|
117
131
|
}
|
|
118
132
|
break;
|
|
133
|
+
case "claude":
|
|
134
|
+
if (rules.length > 0) {
|
|
135
|
+
writeRulesForFile(rules, targetPaths.claude, "CLAUDE.md");
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
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);
|
|
119
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);
|
|
120
180
|
}
|
|
181
|
+
return Array.from(unique.values());
|
|
121
182
|
}
|
|
122
183
|
/**
|
|
123
184
|
* Write MCP servers configuration to IDE targets
|
|
@@ -264,29 +325,35 @@ async function installPackage(options = {}) {
|
|
|
264
325
|
success: false,
|
|
265
326
|
error: new Error("Configuration file not found"),
|
|
266
327
|
installedRuleCount: 0,
|
|
328
|
+
installedCommandCount: 0,
|
|
267
329
|
packagesCount: 0,
|
|
268
330
|
};
|
|
269
331
|
}
|
|
270
|
-
const { config, rules, mcpServers } = resolvedConfig;
|
|
332
|
+
const { config, rules, commands, mcpServers } = resolvedConfig;
|
|
271
333
|
if (config.skipInstall === true) {
|
|
272
334
|
return {
|
|
273
335
|
success: true,
|
|
274
336
|
installedRuleCount: 0,
|
|
337
|
+
installedCommandCount: 0,
|
|
275
338
|
packagesCount: 0,
|
|
276
339
|
};
|
|
277
340
|
}
|
|
341
|
+
warnPresetCommandCollisions(commands);
|
|
342
|
+
const commandsToInstall = dedupeCommandsForInstall(commands);
|
|
278
343
|
try {
|
|
279
344
|
if (!options.dryRun) {
|
|
280
|
-
// Write rules to targets
|
|
281
345
|
writeRulesToTargets(rules, config.targets);
|
|
282
|
-
|
|
346
|
+
writeCommandsToTargets(commandsToInstall, config.targets);
|
|
283
347
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
284
348
|
writeMcpServersToTargets(mcpServers, config.targets, cwd);
|
|
285
349
|
}
|
|
286
350
|
}
|
|
351
|
+
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
|
|
352
|
+
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
|
|
287
353
|
return {
|
|
288
354
|
success: true,
|
|
289
|
-
installedRuleCount:
|
|
355
|
+
installedRuleCount: uniqueRuleCount,
|
|
356
|
+
installedCommandCount: uniqueCommandCount,
|
|
290
357
|
packagesCount: 1,
|
|
291
358
|
};
|
|
292
359
|
}
|
|
@@ -295,6 +362,7 @@ async function installPackage(options = {}) {
|
|
|
295
362
|
success: false,
|
|
296
363
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
297
364
|
installedRuleCount: 0,
|
|
365
|
+
installedCommandCount: 0,
|
|
298
366
|
packagesCount: 0,
|
|
299
367
|
};
|
|
300
368
|
}
|
|
@@ -306,6 +374,7 @@ async function installPackage(options = {}) {
|
|
|
306
374
|
async function installWorkspacesPackages(packages, options = {}) {
|
|
307
375
|
const results = [];
|
|
308
376
|
let totalRuleCount = 0;
|
|
377
|
+
let totalCommandCount = 0;
|
|
309
378
|
// Install packages sequentially for now (can be parallelized later)
|
|
310
379
|
for (const pkg of packages) {
|
|
311
380
|
const packagePath = pkg.absolutePath;
|
|
@@ -316,11 +385,13 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
316
385
|
config: pkg.config,
|
|
317
386
|
});
|
|
318
387
|
totalRuleCount += result.installedRuleCount;
|
|
388
|
+
totalCommandCount += result.installedCommandCount;
|
|
319
389
|
results.push({
|
|
320
390
|
path: pkg.relativePath,
|
|
321
391
|
success: result.success,
|
|
322
392
|
error: result.error,
|
|
323
393
|
installedRuleCount: result.installedRuleCount,
|
|
394
|
+
installedCommandCount: result.installedCommandCount,
|
|
324
395
|
});
|
|
325
396
|
}
|
|
326
397
|
catch (error) {
|
|
@@ -329,6 +400,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
329
400
|
success: false,
|
|
330
401
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
331
402
|
installedRuleCount: 0,
|
|
403
|
+
installedCommandCount: 0,
|
|
332
404
|
});
|
|
333
405
|
}
|
|
334
406
|
}
|
|
@@ -337,6 +409,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
337
409
|
success: failedPackages.length === 0,
|
|
338
410
|
packages: results,
|
|
339
411
|
totalRuleCount,
|
|
412
|
+
totalCommandCount,
|
|
340
413
|
};
|
|
341
414
|
}
|
|
342
415
|
/**
|
|
@@ -355,16 +428,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
355
428
|
const isRoot = pkg.relativePath === ".";
|
|
356
429
|
if (!isRoot)
|
|
357
430
|
return true;
|
|
358
|
-
// For root directories, only keep if it has rules or presets
|
|
431
|
+
// For root directories, only keep if it has rules, commands, or presets
|
|
359
432
|
const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
|
|
433
|
+
const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
|
|
360
434
|
const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
|
|
361
|
-
return hasRules || hasPresets;
|
|
435
|
+
return hasRules || hasCommands || hasPresets;
|
|
362
436
|
});
|
|
363
437
|
if (packages.length === 0) {
|
|
364
438
|
return {
|
|
365
439
|
success: false,
|
|
366
440
|
error: new Error("No packages with aicm configurations found"),
|
|
367
441
|
installedRuleCount: 0,
|
|
442
|
+
installedCommandCount: 0,
|
|
368
443
|
packagesCount: 0,
|
|
369
444
|
};
|
|
370
445
|
}
|
|
@@ -392,7 +467,11 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
392
467
|
if (verbose) {
|
|
393
468
|
result.packages.forEach((pkg) => {
|
|
394
469
|
if (pkg.success) {
|
|
395
|
-
|
|
470
|
+
const summaryParts = [`${pkg.installedRuleCount} rules`];
|
|
471
|
+
if (pkg.installedCommandCount > 0) {
|
|
472
|
+
summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
|
|
473
|
+
}
|
|
474
|
+
console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
|
|
396
475
|
}
|
|
397
476
|
else {
|
|
398
477
|
console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
|
|
@@ -403,7 +482,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
403
482
|
if (failedPackages.length > 0) {
|
|
404
483
|
console.log(chalk_1.default.yellow(`Installation completed with errors`));
|
|
405
484
|
if (verbose) {
|
|
406
|
-
|
|
485
|
+
const commandSummary = result.totalCommandCount > 0
|
|
486
|
+
? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
|
|
487
|
+
: "";
|
|
488
|
+
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})`));
|
|
407
489
|
console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
|
|
408
490
|
}
|
|
409
491
|
const errorDetails = failedPackages
|
|
@@ -413,12 +495,14 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
413
495
|
success: false,
|
|
414
496
|
error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
|
|
415
497
|
installedRuleCount: result.totalRuleCount,
|
|
498
|
+
installedCommandCount: result.totalCommandCount,
|
|
416
499
|
packagesCount: result.packages.length,
|
|
417
500
|
};
|
|
418
501
|
}
|
|
419
502
|
return {
|
|
420
503
|
success: true,
|
|
421
504
|
installedRuleCount: result.totalRuleCount,
|
|
505
|
+
installedCommandCount: result.totalCommandCount,
|
|
422
506
|
packagesCount: result.packages.length,
|
|
423
507
|
};
|
|
424
508
|
});
|
|
@@ -435,6 +519,7 @@ async function install(options = {}) {
|
|
|
435
519
|
return {
|
|
436
520
|
success: true,
|
|
437
521
|
installedRuleCount: 0,
|
|
522
|
+
installedCommandCount: 0,
|
|
438
523
|
packagesCount: 0,
|
|
439
524
|
};
|
|
440
525
|
}
|
|
@@ -464,23 +549,36 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
464
549
|
throw (_a = result.error) !== null && _a !== void 0 ? _a : new Error("Installation failed with unknown error");
|
|
465
550
|
}
|
|
466
551
|
else {
|
|
467
|
-
const
|
|
552
|
+
const ruleCount = result.installedRuleCount;
|
|
553
|
+
const commandCount = result.installedCommandCount;
|
|
554
|
+
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
|
|
555
|
+
const commandMessage = commandCount > 0
|
|
556
|
+
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
|
|
557
|
+
: null;
|
|
558
|
+
const countsParts = [];
|
|
559
|
+
if (ruleMessage) {
|
|
560
|
+
countsParts.push(ruleMessage);
|
|
561
|
+
}
|
|
562
|
+
if (commandMessage) {
|
|
563
|
+
countsParts.push(commandMessage);
|
|
564
|
+
}
|
|
565
|
+
const countsMessage = countsParts.length > 0 ? countsParts.join(" and ") : "0 rules";
|
|
468
566
|
if (dryRun) {
|
|
469
567
|
if (result.packagesCount > 1) {
|
|
470
|
-
console.log(`Dry run: validated ${
|
|
568
|
+
console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
|
|
471
569
|
}
|
|
472
570
|
else {
|
|
473
|
-
console.log(`Dry run: validated ${
|
|
571
|
+
console.log(`Dry run: validated ${countsMessage}`);
|
|
474
572
|
}
|
|
475
573
|
}
|
|
476
|
-
else if (
|
|
477
|
-
console.log("No rules installed");
|
|
574
|
+
else if (ruleCount === 0 && commandCount === 0) {
|
|
575
|
+
console.log("No rules or commands installed");
|
|
478
576
|
}
|
|
479
577
|
else if (result.packagesCount > 1) {
|
|
480
|
-
console.log(`Successfully installed ${
|
|
578
|
+
console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
|
|
481
579
|
}
|
|
482
580
|
else {
|
|
483
|
-
console.log(`Successfully installed ${
|
|
581
|
+
console.log(`Successfully installed ${countsMessage}`);
|
|
484
582
|
}
|
|
485
583
|
}
|
|
486
584
|
}
|
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,39 +33,45 @@ 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"];
|
|
50
|
-
export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex"];
|
|
54
|
+
export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "commandsDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
|
|
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;
|
|
53
58
|
export declare function resolveWorkspaces(config: unknown, configFilePath: string, cwd: string): boolean;
|
|
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",
|
|
@@ -29,7 +31,12 @@ exports.ALLOWED_CONFIG_KEYS = [
|
|
|
29
31
|
"workspaces",
|
|
30
32
|
"skipInstall",
|
|
31
33
|
];
|
|
32
|
-
exports.SUPPORTED_TARGETS = [
|
|
34
|
+
exports.SUPPORTED_TARGETS = [
|
|
35
|
+
"cursor",
|
|
36
|
+
"windsurf",
|
|
37
|
+
"codex",
|
|
38
|
+
"claude",
|
|
39
|
+
];
|
|
33
40
|
function detectWorkspacesFromPackageJson(cwd) {
|
|
34
41
|
try {
|
|
35
42
|
const packageJsonPath = node_path_1.default.join(cwd, "package.json");
|
|
@@ -56,6 +63,7 @@ function resolveWorkspaces(config, configFilePath, cwd) {
|
|
|
56
63
|
function applyDefaults(config, workspaces) {
|
|
57
64
|
return {
|
|
58
65
|
rulesDir: config.rulesDir,
|
|
66
|
+
commandsDir: config.commandsDir,
|
|
59
67
|
targets: config.targets || ["cursor"],
|
|
60
68
|
presets: config.presets || [],
|
|
61
69
|
overrides: config.overrides || {},
|
|
@@ -74,13 +82,14 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
74
82
|
}
|
|
75
83
|
// Validate that either rulesDir or presets is provided
|
|
76
84
|
const hasRulesDir = "rulesDir" in config && typeof config.rulesDir === "string";
|
|
85
|
+
const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
|
|
77
86
|
const hasPresets = "presets" in config &&
|
|
78
87
|
Array.isArray(config.presets) &&
|
|
79
88
|
config.presets.length > 0;
|
|
80
89
|
// In workspace mode, root config doesn't need rulesDir or presets
|
|
81
90
|
// since packages will have their own configurations
|
|
82
|
-
if (!isWorkspaceMode && !hasRulesDir && !hasPresets) {
|
|
83
|
-
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}`);
|
|
84
93
|
}
|
|
85
94
|
// Validate rulesDir if provided
|
|
86
95
|
if (hasRulesDir) {
|
|
@@ -92,6 +101,15 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
92
101
|
throw new Error(`Rules path is not a directory: ${rulesPath}`);
|
|
93
102
|
}
|
|
94
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
|
+
}
|
|
95
113
|
if ("targets" in config) {
|
|
96
114
|
if (!Array.isArray(config.targets)) {
|
|
97
115
|
throw new Error(`targets must be an array in config at ${configFilePath}`);
|
|
@@ -132,6 +150,31 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
|
|
|
132
150
|
}
|
|
133
151
|
return rules;
|
|
134
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
|
+
}
|
|
135
178
|
function resolvePresetPath(presetPath, cwd) {
|
|
136
179
|
// Support specifying aicm.json directory and load the config from it
|
|
137
180
|
if (!presetPath.endsWith(".json")) {
|
|
@@ -168,20 +211,25 @@ async function loadPreset(presetPath, cwd) {
|
|
|
168
211
|
catch (error) {
|
|
169
212
|
throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
170
213
|
}
|
|
171
|
-
// Validate that preset has rulesDir
|
|
172
|
-
if (!presetConfig.rulesDir) {
|
|
173
|
-
throw new Error(`Preset "${presetPath}" must have a rulesDir specified`);
|
|
174
|
-
}
|
|
175
|
-
// Resolve preset's rules directory relative to the preset file
|
|
176
214
|
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
177
|
-
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
|
+
}
|
|
178
224
|
return {
|
|
179
225
|
config: presetConfig,
|
|
180
226
|
rulesDir: presetRulesDir,
|
|
227
|
+
commandsDir: presetCommandsDir,
|
|
181
228
|
};
|
|
182
229
|
}
|
|
183
230
|
async function loadAllRules(config, cwd) {
|
|
184
231
|
const allRules = [];
|
|
232
|
+
const allCommands = [];
|
|
185
233
|
let mergedMcpServers = { ...config.mcpServers };
|
|
186
234
|
// Load local rules only if rulesDir is provided
|
|
187
235
|
if (config.rulesDir) {
|
|
@@ -189,11 +237,22 @@ async function loadAllRules(config, cwd) {
|
|
|
189
237
|
const localRules = await loadRulesFromDirectory(localRulesPath, "local");
|
|
190
238
|
allRules.push(...localRules);
|
|
191
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
|
+
}
|
|
192
245
|
if (config.presets) {
|
|
193
246
|
for (const presetPath of config.presets) {
|
|
194
247
|
const preset = await loadPreset(presetPath, cwd);
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
256
|
// Merge MCP servers from preset
|
|
198
257
|
if (preset.config.mcpServers) {
|
|
199
258
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
@@ -202,41 +261,43 @@ async function loadAllRules(config, cwd) {
|
|
|
202
261
|
}
|
|
203
262
|
return {
|
|
204
263
|
rules: allRules,
|
|
264
|
+
commands: allCommands,
|
|
205
265
|
mcpServers: mergedMcpServers,
|
|
206
266
|
};
|
|
207
267
|
}
|
|
208
|
-
function applyOverrides(
|
|
209
|
-
// Validate that all override
|
|
210
|
-
for (const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (!rules.some((rule) => rule.name === ruleName)) {
|
|
214
|
-
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`);
|
|
215
273
|
}
|
|
216
274
|
}
|
|
217
|
-
const
|
|
218
|
-
for (const
|
|
219
|
-
|
|
275
|
+
const fileMap = new Map();
|
|
276
|
+
for (const file of files) {
|
|
277
|
+
fileMap.set(file.name, file);
|
|
220
278
|
}
|
|
221
|
-
for (const [
|
|
279
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
222
280
|
if (override === false) {
|
|
223
|
-
|
|
281
|
+
fileMap.delete(name);
|
|
224
282
|
}
|
|
225
283
|
else if (typeof override === "string") {
|
|
226
284
|
const overridePath = node_path_1.default.resolve(cwd, override);
|
|
227
285
|
if (!fs_extra_1.default.existsSync(overridePath)) {
|
|
228
|
-
throw new Error(`Override
|
|
286
|
+
throw new Error(`Override file not found: ${override} in ${cwd}`);
|
|
229
287
|
}
|
|
230
288
|
const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
|
|
231
|
-
|
|
232
|
-
|
|
289
|
+
const existing = fileMap.get(name);
|
|
290
|
+
fileMap.set(name, {
|
|
291
|
+
...(existing !== null && existing !== void 0 ? existing : {}),
|
|
292
|
+
name,
|
|
233
293
|
content,
|
|
234
294
|
sourcePath: overridePath,
|
|
235
295
|
source: "local",
|
|
296
|
+
presetName: undefined,
|
|
236
297
|
});
|
|
237
298
|
}
|
|
238
299
|
}
|
|
239
|
-
return Array.from(
|
|
300
|
+
return Array.from(fileMap.values());
|
|
240
301
|
}
|
|
241
302
|
/**
|
|
242
303
|
* Merge preset MCP servers with local config MCP servers
|
|
@@ -280,14 +341,31 @@ async function loadConfig(cwd) {
|
|
|
280
341
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
281
342
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
282
343
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
283
|
-
const { rules, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
344
|
+
const { rules, commands, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
284
345
|
let rulesWithOverrides = rules;
|
|
346
|
+
let commandsWithOverrides = commands;
|
|
285
347
|
if (configWithDefaults.overrides) {
|
|
286
|
-
|
|
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
|
+
}
|
|
287
364
|
}
|
|
288
365
|
return {
|
|
289
366
|
config: configWithDefaults,
|
|
290
367
|
rules: rulesWithOverrides,
|
|
368
|
+
commands: commandsWithOverrides,
|
|
291
369
|
mcpServers,
|
|
292
370
|
};
|
|
293
371
|
}
|