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 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";
@@ -37,6 +37,10 @@ export interface InstallResult {
37
37
  * Number of rules installed
38
38
  */
39
39
  installedRuleCount: number;
40
+ /**
41
+ * Number of commands installed
42
+ */
43
+ installedCommandCount: number;
40
44
  /**
41
45
  * Number of packages installed
42
46
  */
@@ -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); // Filter out empty segments
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
- // Write MCP servers
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: rules.length,
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
- console.log(chalk_1.default.green(`✅ ${pkg.path} (${pkg.installedRuleCount} rules)`));
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
- console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rules total)`));
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 rulesInstalledMessage = `${result.installedRuleCount} rule${result.installedRuleCount === 1 ? "" : "s"}`;
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 ${rulesInstalledMessage} across ${result.packagesCount} packages`);
610
+ console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
477
611
  }
478
612
  else {
479
- console.log(`Dry run: validated ${rulesInstalledMessage}`);
613
+ console.log(`Dry run: validated ${countsMessage}`);
480
614
  }
481
615
  }
482
- else if (result.installedRuleCount === 0) {
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 ${rulesInstalledMessage} across ${result.packagesCount} packages`);
620
+ console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
487
621
  }
488
622
  else {
489
- console.log(`Successfully installed ${rulesInstalledMessage}`);
623
+ console.log(`Successfully installed ${countsMessage}`);
490
624
  }
491
625
  }
492
626
  }
@@ -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
- if (!config.rules || config.rules.length === 0) {
17
- console.log(chalk_1.default.yellow("No rules defined in configuration."));
18
- console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules.`);
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
- console.log(chalk_1.default.blue("Configured Rules:"));
22
- console.log(chalk_1.default.dim("".repeat(50)));
23
- for (const rule of config.rules) {
24
- console.log(`${chalk_1.default.bold(rule.name)} - ${rule.sourcePath} ${rule.presetName ? `[${rule.presetName}]` : ""}`);
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
  }
@@ -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 RuleFile {
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: string;
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(rules: RuleFile[], overrides: Record<string, string | false>, cwd: string): RuleFile[];
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;
@@ -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(`Either rulesDir or presets must be specified in config at ${configFilePath}`);
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 = node_path_1.default.resolve(presetDir, presetConfig.rulesDir);
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
- const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
201
- allRules.push(...presetRules);
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(rules, overrides, cwd) {
214
- // Validate that all override rule names exist in the resolved rules
215
- for (const ruleName of Object.keys(overrides)) {
216
- // TODO: support better error messages with edit distance, helping the user in case of a typo
217
- // TODO: or shows a list of potential rules to override
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 ruleMap = new Map();
223
- for (const rule of rules) {
224
- ruleMap.set(rule.name, rule);
275
+ const fileMap = new Map();
276
+ for (const file of files) {
277
+ fileMap.set(file.name, file);
225
278
  }
226
- for (const [ruleName, override] of Object.entries(overrides)) {
279
+ for (const [name, override] of Object.entries(overrides)) {
227
280
  if (override === false) {
228
- ruleMap.delete(ruleName);
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 rule file not found: ${override} in ${cwd}`);
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
- ruleMap.set(ruleName, {
237
- name: ruleName,
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(ruleMap.values());
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
- rulesWithOverrides = applyOverrides(rules, configWithDefaults.overrides, workingDir);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
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",