aicm 0.19.0 → 0.20.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 +114 -4
- package/dist/api.d.ts +6 -0
- package/dist/api.js +10 -0
- package/dist/bin/aicm.js +0 -0
- package/dist/commands/clean.js +56 -3
- package/dist/commands/install-workspaces.js +76 -3
- package/dist/commands/install.d.ts +17 -1
- package/dist/commands/install.js +167 -10
- package/dist/utils/config.d.ts +18 -0
- package/dist/utils/config.js +66 -4
- package/package.json +18 -16
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A CLI tool for managing Agentic configurations across projects.
|
|
|
16
16
|
- [Features](#features)
|
|
17
17
|
- [Rules](#rules)
|
|
18
18
|
- [Commands](#commands)
|
|
19
|
+
- [Skills](#skills)
|
|
19
20
|
- [Hooks](#hooks)
|
|
20
21
|
- [MCP Servers](#mcp-servers)
|
|
21
22
|
- [Assets](#assets)
|
|
@@ -23,6 +24,7 @@ A CLI tool for managing Agentic configurations across projects.
|
|
|
23
24
|
- [Configuration](#configuration)
|
|
24
25
|
- [CLI Commands](#cli-commands)
|
|
25
26
|
- [Node.js API](#nodejs-api)
|
|
27
|
+
- [FAQ](#faq)
|
|
26
28
|
|
|
27
29
|
## Why
|
|
28
30
|
|
|
@@ -85,6 +87,7 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
|
|
|
85
87
|
│ ├── typescript.mdc
|
|
86
88
|
│ └── react.mdc
|
|
87
89
|
├── commands/ # Command files (.md) [optional]
|
|
90
|
+
├── skills/ # Agent Skills [optional]
|
|
88
91
|
├── assets/ # Auxiliary files [optional]
|
|
89
92
|
└── hooks.json # Hook configuration [optional]
|
|
90
93
|
```
|
|
@@ -139,7 +142,7 @@ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are con
|
|
|
139
142
|
### Notes
|
|
140
143
|
|
|
141
144
|
- Generated files are always placed in subdirectories for deterministic cleanup and easy gitignore.
|
|
142
|
-
- Users should add `.cursor/*/aicm/` to `.gitignore` to avoid tracking generated files.
|
|
145
|
+
- Users should add `.cursor/*/aicm/`, `.cursor/skills/`, `.claude/`, and `.codex/` to `.gitignore` to avoid tracking generated files.
|
|
143
146
|
|
|
144
147
|
## Features
|
|
145
148
|
|
|
@@ -193,6 +196,65 @@ Configure your `aicm.json`:
|
|
|
193
196
|
|
|
194
197
|
Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
|
|
195
198
|
|
|
199
|
+
### Skills
|
|
200
|
+
|
|
201
|
+
aicm supports [Agent Skills](https://agentskills.io) - a standard format for giving AI agents new capabilities and expertise. Skills are folders containing instructions, scripts, and resources that agents can discover and use.
|
|
202
|
+
|
|
203
|
+
Create a `skills/` directory where each subdirectory is a skill (containing a `SKILL.md` file):
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
my-project/
|
|
207
|
+
├── aicm.json
|
|
208
|
+
└── skills/
|
|
209
|
+
├── pdf-processing/
|
|
210
|
+
│ ├── SKILL.md
|
|
211
|
+
│ ├── scripts/
|
|
212
|
+
│ │ └── extract.py
|
|
213
|
+
│ └── references/
|
|
214
|
+
│ └── REFERENCE.md
|
|
215
|
+
└── code-review/
|
|
216
|
+
└── SKILL.md
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Each skill must have a `SKILL.md` file with YAML frontmatter:
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
---
|
|
223
|
+
name: pdf-processing
|
|
224
|
+
description: Extract text and tables from PDF files, fill forms, merge documents.
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
# PDF Processing Skill
|
|
228
|
+
|
|
229
|
+
This skill enables working with PDF documents.
|
|
230
|
+
|
|
231
|
+
## Usage
|
|
232
|
+
|
|
233
|
+
Run the extraction script:
|
|
234
|
+
scripts/extract.py
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Configure your `aicm.json`:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"rootDir": "./",
|
|
242
|
+
"targets": ["cursor"]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Skills are installed to different locations based on the target:
|
|
247
|
+
|
|
248
|
+
| Target | Skills Location |
|
|
249
|
+
| ---------- | ----------------- |
|
|
250
|
+
| **Cursor** | `.cursor/skills/` |
|
|
251
|
+
| **Claude** | `.claude/skills/` |
|
|
252
|
+
| **Codex** | `.codex/skills/` |
|
|
253
|
+
|
|
254
|
+
When installed, each skill directory is copied in its entirety (including `scripts/`, `references/`, `assets/` subdirectories). A `.aicm.json` file is added inside each installed skill to track that it's managed by aicm.
|
|
255
|
+
|
|
256
|
+
In workspace mode, skills are installed both to each package and merged at the root level, similar to commands.
|
|
257
|
+
|
|
196
258
|
### Hooks
|
|
197
259
|
|
|
198
260
|
aicm provides first-class support for [Cursor Agent Hooks](https://docs.cursor.com/advanced/hooks), allowing you to intercept and extend the agent's behavior. Hooks enable you to run custom scripts before/after shell execution, file edits, MCP calls, and more.
|
|
@@ -369,9 +431,10 @@ aicm automatically detects workspaces if your `package.json` contains a `workspa
|
|
|
369
431
|
### How It Works
|
|
370
432
|
|
|
371
433
|
1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository.
|
|
372
|
-
2. **Install per package**: Install rules and
|
|
434
|
+
2. **Install per package**: Install rules, commands, and skills for each package individually in their respective directories.
|
|
373
435
|
3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package.
|
|
374
436
|
4. **Merge commands**: Write a merged `.cursor/commands/aicm/` at the repository root containing all commands from every package.
|
|
437
|
+
5. **Merge skills**: Write merged skills to the repository root (e.g., `.cursor/skills/`) containing all skills from every package.
|
|
375
438
|
|
|
376
439
|
For example, in a workspace structure like:
|
|
377
440
|
|
|
@@ -429,7 +492,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
|
|
|
429
492
|
|
|
430
493
|
### Configuration Options
|
|
431
494
|
|
|
432
|
-
- **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
|
|
495
|
+
- **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `skills/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
|
|
433
496
|
- **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`. Supported targets: `cursor`, `windsurf`, `codex`, `claude`.
|
|
434
497
|
- **presets**: List of preset packages or paths to include.
|
|
435
498
|
- **mcpServers**: MCP server configurations.
|
|
@@ -471,11 +534,14 @@ aicm uses a convention-based directory structure:
|
|
|
471
534
|
```
|
|
472
535
|
my-project/
|
|
473
536
|
├── aicm.json
|
|
474
|
-
├── rules/ # Rule files (.mdc) [
|
|
537
|
+
├── rules/ # Rule files (.mdc) [optional]
|
|
475
538
|
│ ├── api.mdc
|
|
476
539
|
│ └── testing.mdc
|
|
477
540
|
├── commands/ # Command files (.md) [optional]
|
|
478
541
|
│ └── generate.md
|
|
542
|
+
├── skills/ # Agent Skills [optional]
|
|
543
|
+
│ └── my-skill/
|
|
544
|
+
│ └── SKILL.md
|
|
479
545
|
├── assets/ # Auxiliary files [optional]
|
|
480
546
|
│ ├── schema.json
|
|
481
547
|
│ └── examples/
|
|
@@ -559,6 +625,50 @@ install({
|
|
|
559
625
|
|
|
560
626
|
To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
|
|
561
627
|
|
|
628
|
+
## FAQ
|
|
629
|
+
|
|
630
|
+
### Can I reference rules from commands or vice versa?
|
|
631
|
+
|
|
632
|
+
**No, direct references between rules and commands are not supported.** This is because:
|
|
633
|
+
|
|
634
|
+
- **Commands are hoisted** to the root level in workspace mode (`.cursor/commands/aicm/`)
|
|
635
|
+
- **Rules remain nested** at the package level (`package-a/.cursor/rules/aicm/`)
|
|
636
|
+
- This creates broken relative paths when commands try to reference rules
|
|
637
|
+
|
|
638
|
+
**❌ Don't do this:**
|
|
639
|
+
|
|
640
|
+
```markdown
|
|
641
|
+
<!-- commands/validate.md -->
|
|
642
|
+
|
|
643
|
+
Follow the rules in [api-rule.mdc](../rules/api-rule.mdc) <!-- BROKEN! -->
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**✅ Do this instead:**
|
|
647
|
+
|
|
648
|
+
```markdown
|
|
649
|
+
<!-- Put shared content in assets/coding-standards.md -->
|
|
650
|
+
|
|
651
|
+
# Coding Standards
|
|
652
|
+
|
|
653
|
+
- Use TypeScript for all new code
|
|
654
|
+
- Follow ESLint rules
|
|
655
|
+
- Write unit tests for all functions
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
```markdown
|
|
659
|
+
<!-- rules/api-rule.mdc -->
|
|
660
|
+
|
|
661
|
+
Follow the coding standards in [coding-standards.md](../assets/coding-standards.md).
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
```markdown
|
|
665
|
+
<!-- commands/validate.md -->
|
|
666
|
+
|
|
667
|
+
Validate against our [coding standards](../assets/coding-standards.md).
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Use shared assets for content that needs to be referenced by both rules and commands. Assets are properly rewritten and work in all modes.
|
|
671
|
+
|
|
562
672
|
## Contributing
|
|
563
673
|
|
|
564
674
|
Contributions are welcome! Please feel free to open an issue or submit a Pull Request.
|
package/dist/api.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { InstallOptions, InstallResult } from "./commands/install";
|
|
|
5
5
|
* @returns Result of the install operation
|
|
6
6
|
*/
|
|
7
7
|
export declare function install(options?: InstallOptions): Promise<InstallResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
10
|
+
* @param cwd Current working directory (optional, defaults to process.cwd())
|
|
11
|
+
* @returns True if workspaces mode is enabled
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
|
|
8
14
|
export type { InstallOptions, InstallResult } from "./commands/install";
|
|
9
15
|
export type { ResolvedConfig, Config, RuleFile, CommandFile, MCPServers, } from "./utils/config";
|
|
10
16
|
export type { HookFile, HooksJson, HookType, HookCommand } from "./utils/hooks";
|
package/dist/api.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.install = install;
|
|
4
|
+
exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
|
|
4
5
|
const install_1 = require("./commands/install");
|
|
6
|
+
const config_1 = require("./utils/config");
|
|
5
7
|
/**
|
|
6
8
|
* Install AICM rules based on configuration
|
|
7
9
|
* @param options Installation options
|
|
@@ -10,3 +12,11 @@ const install_1 = require("./commands/install");
|
|
|
10
12
|
async function install(options = {}) {
|
|
11
13
|
return (0, install_1.install)(options);
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
17
|
+
* @param cwd Current working directory (optional, defaults to process.cwd())
|
|
18
|
+
* @returns True if workspaces mode is enabled
|
|
19
|
+
*/
|
|
20
|
+
async function checkWorkspacesEnabled(cwd) {
|
|
21
|
+
return (0, config_1.checkWorkspacesEnabled)(cwd);
|
|
22
|
+
}
|
package/dist/bin/aicm.js
CHANGED
|
File without changes
|
package/dist/commands/clean.js
CHANGED
|
@@ -152,6 +152,54 @@ function cleanHooks(cwd, verbose) {
|
|
|
152
152
|
}
|
|
153
153
|
return hasChanges;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Clean aicm-managed skills from a skills directory
|
|
157
|
+
* Only removes skills that have .aicm.json (presence indicates aicm management)
|
|
158
|
+
*/
|
|
159
|
+
function cleanSkills(cwd, verbose) {
|
|
160
|
+
let cleanedCount = 0;
|
|
161
|
+
// Skills directories for each target
|
|
162
|
+
const skillsDirs = [
|
|
163
|
+
node_path_1.default.join(cwd, ".cursor", "skills"),
|
|
164
|
+
node_path_1.default.join(cwd, ".claude", "skills"),
|
|
165
|
+
node_path_1.default.join(cwd, ".codex", "skills"),
|
|
166
|
+
];
|
|
167
|
+
for (const skillsDir of skillsDirs) {
|
|
168
|
+
if (!fs_extra_1.default.existsSync(skillsDir)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const entries = fs_extra_1.default.readdirSync(skillsDir, { withFileTypes: true });
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (!entry.isDirectory()) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const skillPath = node_path_1.default.join(skillsDir, entry.name);
|
|
178
|
+
const metadataPath = node_path_1.default.join(skillPath, ".aicm.json");
|
|
179
|
+
// Only clean skills that have .aicm.json (presence indicates aicm management)
|
|
180
|
+
if (fs_extra_1.default.existsSync(metadataPath)) {
|
|
181
|
+
fs_extra_1.default.removeSync(skillPath);
|
|
182
|
+
if (verbose) {
|
|
183
|
+
console.log(chalk_1.default.gray(` Removed skill ${skillPath}`));
|
|
184
|
+
}
|
|
185
|
+
cleanedCount++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Remove the skills directory if it's now empty
|
|
189
|
+
const remainingEntries = fs_extra_1.default.readdirSync(skillsDir);
|
|
190
|
+
if (remainingEntries.length === 0) {
|
|
191
|
+
fs_extra_1.default.removeSync(skillsDir);
|
|
192
|
+
if (verbose) {
|
|
193
|
+
console.log(chalk_1.default.gray(` Removed empty directory ${skillsDir}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (_a) {
|
|
198
|
+
console.warn(chalk_1.default.yellow(`Warning: Failed to clean skills in ${skillsDir}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return cleanedCount;
|
|
202
|
+
}
|
|
155
203
|
function cleanEmptyDirectories(cwd, verbose) {
|
|
156
204
|
let cleanedCount = 0;
|
|
157
205
|
const dirsToCheck = [
|
|
@@ -159,7 +207,12 @@ function cleanEmptyDirectories(cwd, verbose) {
|
|
|
159
207
|
node_path_1.default.join(cwd, ".cursor", "commands"),
|
|
160
208
|
node_path_1.default.join(cwd, ".cursor", "assets"),
|
|
161
209
|
node_path_1.default.join(cwd, ".cursor", "hooks"),
|
|
210
|
+
node_path_1.default.join(cwd, ".cursor", "skills"),
|
|
162
211
|
node_path_1.default.join(cwd, ".cursor"),
|
|
212
|
+
node_path_1.default.join(cwd, ".claude", "skills"),
|
|
213
|
+
node_path_1.default.join(cwd, ".claude"),
|
|
214
|
+
node_path_1.default.join(cwd, ".codex", "skills"),
|
|
215
|
+
node_path_1.default.join(cwd, ".codex"),
|
|
163
216
|
];
|
|
164
217
|
for (const dir of dirsToCheck) {
|
|
165
218
|
if (fs_extra_1.default.existsSync(dir)) {
|
|
@@ -211,6 +264,8 @@ async function cleanPackage(options = {}) {
|
|
|
211
264
|
// Clean hooks
|
|
212
265
|
if (cleanHooks(cwd, verbose))
|
|
213
266
|
cleanedCount++;
|
|
267
|
+
// Clean skills
|
|
268
|
+
cleanedCount += cleanSkills(cwd, verbose);
|
|
214
269
|
// Clean empty directories
|
|
215
270
|
cleanedCount += cleanEmptyDirectories(cwd, verbose);
|
|
216
271
|
return {
|
|
@@ -251,11 +306,9 @@ async function cleanWorkspaces(cwd, verbose = false) {
|
|
|
251
306
|
};
|
|
252
307
|
}
|
|
253
308
|
async function clean(options = {}) {
|
|
254
|
-
var _a;
|
|
255
309
|
const cwd = options.cwd || process.cwd();
|
|
256
310
|
const verbose = options.verbose || false;
|
|
257
|
-
const shouldUseWorkspaces =
|
|
258
|
-
(0, config_1.detectWorkspacesFromPackageJson)(cwd);
|
|
311
|
+
const shouldUseWorkspaces = await (0, config_1.checkWorkspacesEnabled)(cwd);
|
|
259
312
|
if (shouldUseWorkspaces) {
|
|
260
313
|
return cleanWorkspaces(cwd, verbose);
|
|
261
314
|
}
|
|
@@ -41,6 +41,52 @@ function collectWorkspaceCommandTargets(packages) {
|
|
|
41
41
|
}
|
|
42
42
|
return Array.from(targets);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Merge skills from multiple workspace packages
|
|
46
|
+
* Skills are merged flat (not namespaced by preset)
|
|
47
|
+
* Dedupes preset skills that appear in multiple packages
|
|
48
|
+
*/
|
|
49
|
+
function mergeWorkspaceSkills(packages) {
|
|
50
|
+
var _a;
|
|
51
|
+
const skills = [];
|
|
52
|
+
const seenPresetSkills = new Set();
|
|
53
|
+
for (const pkg of packages) {
|
|
54
|
+
// Skills are supported by cursor, claude, and codex targets
|
|
55
|
+
const hasSkillsTarget = pkg.config.config.targets.includes("cursor") ||
|
|
56
|
+
pkg.config.config.targets.includes("claude") ||
|
|
57
|
+
pkg.config.config.targets.includes("codex");
|
|
58
|
+
if (!hasSkillsTarget) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const skill of (_a = pkg.config.skills) !== null && _a !== void 0 ? _a : []) {
|
|
62
|
+
if (skill.presetName) {
|
|
63
|
+
// Dedupe preset skills by preset+name combination
|
|
64
|
+
const presetKey = `${skill.presetName}::${skill.name}`;
|
|
65
|
+
if (seenPresetSkills.has(presetKey)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
seenPresetSkills.add(presetKey);
|
|
69
|
+
}
|
|
70
|
+
skills.push(skill);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return skills;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Collect all targets that support skills from workspace packages
|
|
77
|
+
*/
|
|
78
|
+
function collectWorkspaceSkillTargets(packages) {
|
|
79
|
+
const targets = new Set();
|
|
80
|
+
for (const pkg of packages) {
|
|
81
|
+
for (const target of pkg.config.config.targets) {
|
|
82
|
+
// Skills are supported by cursor, claude, and codex
|
|
83
|
+
if (target === "cursor" || target === "claude" || target === "codex") {
|
|
84
|
+
targets.add(target);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Array.from(targets);
|
|
89
|
+
}
|
|
44
90
|
function mergeWorkspaceMcpServers(packages) {
|
|
45
91
|
const merged = {};
|
|
46
92
|
const info = {};
|
|
@@ -101,6 +147,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
101
147
|
let totalCommandCount = 0;
|
|
102
148
|
let totalAssetCount = 0;
|
|
103
149
|
let totalHookCount = 0;
|
|
150
|
+
let totalSkillCount = 0;
|
|
104
151
|
// Install packages sequentially for now (can be parallelized later)
|
|
105
152
|
for (const pkg of packages) {
|
|
106
153
|
const packagePath = pkg.absolutePath;
|
|
@@ -114,6 +161,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
114
161
|
totalCommandCount += result.installedCommandCount;
|
|
115
162
|
totalAssetCount += result.installedAssetCount;
|
|
116
163
|
totalHookCount += result.installedHookCount;
|
|
164
|
+
totalSkillCount += result.installedSkillCount;
|
|
117
165
|
results.push({
|
|
118
166
|
path: pkg.relativePath,
|
|
119
167
|
success: result.success,
|
|
@@ -122,6 +170,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
122
170
|
installedCommandCount: result.installedCommandCount,
|
|
123
171
|
installedAssetCount: result.installedAssetCount,
|
|
124
172
|
installedHookCount: result.installedHookCount,
|
|
173
|
+
installedSkillCount: result.installedSkillCount,
|
|
125
174
|
});
|
|
126
175
|
}
|
|
127
176
|
catch (error) {
|
|
@@ -133,6 +182,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
133
182
|
installedCommandCount: 0,
|
|
134
183
|
installedAssetCount: 0,
|
|
135
184
|
installedHookCount: 0,
|
|
185
|
+
installedSkillCount: 0,
|
|
136
186
|
});
|
|
137
187
|
}
|
|
138
188
|
}
|
|
@@ -144,6 +194,7 @@ async function installWorkspacesPackages(packages, options = {}) {
|
|
|
144
194
|
totalCommandCount,
|
|
145
195
|
totalAssetCount,
|
|
146
196
|
totalHookCount,
|
|
197
|
+
totalSkillCount,
|
|
147
198
|
};
|
|
148
199
|
}
|
|
149
200
|
/**
|
|
@@ -162,11 +213,12 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
162
213
|
const isRoot = pkg.relativePath === ".";
|
|
163
214
|
if (!isRoot)
|
|
164
215
|
return true;
|
|
165
|
-
// For root directories, only keep if it has rules, commands, or presets
|
|
216
|
+
// For root directories, only keep if it has rules, commands, skills, or presets
|
|
166
217
|
const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
|
|
167
218
|
const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
|
|
219
|
+
const hasSkills = pkg.config.skills && pkg.config.skills.length > 0;
|
|
168
220
|
const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
|
|
169
|
-
return hasRules || hasCommands || hasPresets;
|
|
221
|
+
return hasRules || hasCommands || hasSkills || hasPresets;
|
|
170
222
|
});
|
|
171
223
|
if (packages.length === 0) {
|
|
172
224
|
return {
|
|
@@ -176,6 +228,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
176
228
|
installedCommandCount: 0,
|
|
177
229
|
installedAssetCount: 0,
|
|
178
230
|
installedHookCount: 0,
|
|
231
|
+
installedSkillCount: 0,
|
|
179
232
|
packagesCount: 0,
|
|
180
233
|
};
|
|
181
234
|
}
|
|
@@ -206,6 +259,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
206
259
|
(0, install_1.writeAssetsToTargets)(allAssets, workspaceCommandTargets);
|
|
207
260
|
(0, install_1.writeCommandsToTargets)(dedupedWorkspaceCommands, workspaceCommandTargets);
|
|
208
261
|
}
|
|
262
|
+
// Merge and write skills for workspace
|
|
263
|
+
const workspaceSkills = mergeWorkspaceSkills(packages);
|
|
264
|
+
const workspaceSkillTargets = collectWorkspaceSkillTargets(packages);
|
|
265
|
+
if (workspaceSkills.length > 0) {
|
|
266
|
+
(0, install_1.warnPresetSkillCollisions)(workspaceSkills);
|
|
267
|
+
}
|
|
268
|
+
if (!dryRun &&
|
|
269
|
+
workspaceSkills.length > 0 &&
|
|
270
|
+
workspaceSkillTargets.length > 0) {
|
|
271
|
+
const dedupedWorkspaceSkills = (0, install_1.dedupeSkillsForInstall)(workspaceSkills);
|
|
272
|
+
(0, install_1.writeSkillsToTargets)(dedupedWorkspaceSkills, workspaceSkillTargets);
|
|
273
|
+
}
|
|
209
274
|
const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
|
|
210
275
|
const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
|
|
211
276
|
if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
|
|
@@ -231,6 +296,9 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
231
296
|
if (pkg.installedHookCount > 0) {
|
|
232
297
|
summaryParts.push(`${pkg.installedHookCount} hook${pkg.installedHookCount === 1 ? "" : "s"}`);
|
|
233
298
|
}
|
|
299
|
+
if (pkg.installedSkillCount > 0) {
|
|
300
|
+
summaryParts.push(`${pkg.installedSkillCount} skill${pkg.installedSkillCount === 1 ? "" : "s"}`);
|
|
301
|
+
}
|
|
234
302
|
console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
|
|
235
303
|
}
|
|
236
304
|
else {
|
|
@@ -248,7 +316,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
248
316
|
const hookSummary = result.totalHookCount > 0
|
|
249
317
|
? `, ${result.totalHookCount} hook${result.totalHookCount === 1 ? "" : "s"} total`
|
|
250
318
|
: "";
|
|
251
|
-
|
|
319
|
+
const skillSummary = result.totalSkillCount > 0
|
|
320
|
+
? `, ${result.totalSkillCount} skill${result.totalSkillCount === 1 ? "" : "s"} total`
|
|
321
|
+
: "";
|
|
322
|
+
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}${hookSummary}${skillSummary})`));
|
|
252
323
|
console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
|
|
253
324
|
}
|
|
254
325
|
const errorDetails = failedPackages
|
|
@@ -261,6 +332,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
261
332
|
installedCommandCount: result.totalCommandCount,
|
|
262
333
|
installedAssetCount: result.totalAssetCount,
|
|
263
334
|
installedHookCount: result.totalHookCount,
|
|
335
|
+
installedSkillCount: result.totalSkillCount,
|
|
264
336
|
packagesCount: result.packages.length,
|
|
265
337
|
};
|
|
266
338
|
}
|
|
@@ -270,6 +342,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
|
|
|
270
342
|
installedCommandCount: result.totalCommandCount,
|
|
271
343
|
installedAssetCount: result.totalAssetCount,
|
|
272
344
|
installedHookCount: result.totalHookCount,
|
|
345
|
+
installedSkillCount: result.totalSkillCount,
|
|
273
346
|
packagesCount: result.packages.length,
|
|
274
347
|
};
|
|
275
348
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResolvedConfig, CommandFile, AssetFile, MCPServers, SupportedTarget } from "../utils/config";
|
|
1
|
+
import { ResolvedConfig, CommandFile, AssetFile, SkillFile, MCPServers, SupportedTarget } from "../utils/config";
|
|
2
2
|
export interface InstallOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Base directory to use instead of process.cwd()
|
|
@@ -49,6 +49,10 @@ export interface InstallResult {
|
|
|
49
49
|
* Number of hooks installed
|
|
50
50
|
*/
|
|
51
51
|
installedHookCount: number;
|
|
52
|
+
/**
|
|
53
|
+
* Number of skills installed
|
|
54
|
+
*/
|
|
55
|
+
installedSkillCount: number;
|
|
52
56
|
/**
|
|
53
57
|
* Number of packages installed
|
|
54
58
|
*/
|
|
@@ -56,6 +60,18 @@ export interface InstallResult {
|
|
|
56
60
|
}
|
|
57
61
|
export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
|
|
58
62
|
export declare function writeCommandsToTargets(commands: CommandFile[], targets: SupportedTarget[]): void;
|
|
63
|
+
/**
|
|
64
|
+
* Write skills to all supported target directories
|
|
65
|
+
*/
|
|
66
|
+
export declare function writeSkillsToTargets(skills: SkillFile[], targets: SupportedTarget[]): void;
|
|
67
|
+
/**
|
|
68
|
+
* Warn about skill name collisions from different presets
|
|
69
|
+
*/
|
|
70
|
+
export declare function warnPresetSkillCollisions(skills: SkillFile[]): void;
|
|
71
|
+
/**
|
|
72
|
+
* Dedupe skills by name (last one wins)
|
|
73
|
+
*/
|
|
74
|
+
export declare function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[];
|
|
59
75
|
export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
|
|
60
76
|
export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
|
|
61
77
|
/**
|
package/dist/commands/install.js
CHANGED
|
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.writeAssetsToTargets = writeAssetsToTargets;
|
|
7
7
|
exports.writeCommandsToTargets = writeCommandsToTargets;
|
|
8
|
+
exports.writeSkillsToTargets = writeSkillsToTargets;
|
|
9
|
+
exports.warnPresetSkillCollisions = warnPresetSkillCollisions;
|
|
10
|
+
exports.dedupeSkillsForInstall = dedupeSkillsForInstall;
|
|
8
11
|
exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
|
|
9
12
|
exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
|
|
10
13
|
exports.writeMcpServersToFile = writeMcpServersToFile;
|
|
@@ -23,11 +26,27 @@ const install_workspaces_1 = require("./install-workspaces");
|
|
|
23
26
|
/**
|
|
24
27
|
* Rewrite asset references from source paths to installation paths
|
|
25
28
|
* Only rewrites the ../assets/ pattern - everything else is preserved
|
|
29
|
+
*
|
|
30
|
+
* @param content - The file content to rewrite
|
|
31
|
+
* @param presetName - The preset name if this file is from a preset
|
|
32
|
+
* @param fileInstallDepth - The depth of the file's installation directory relative to .cursor/
|
|
33
|
+
* For example: .cursor/commands/aicm/file.md has depth 2 (commands, aicm)
|
|
34
|
+
* .cursor/rules/aicm/preset/file.mdc has depth 3 (rules, aicm, preset)
|
|
26
35
|
*/
|
|
27
|
-
function rewriteAssetReferences(content) {
|
|
28
|
-
//
|
|
36
|
+
function rewriteAssetReferences(content, presetName, fileInstallDepth = 2) {
|
|
37
|
+
// Calculate the relative path from the file to .cursor/assets/aicm/
|
|
38
|
+
// We need to go up fileInstallDepth levels to reach .cursor/, then down to assets/aicm/
|
|
39
|
+
const upLevels = "../".repeat(fileInstallDepth);
|
|
40
|
+
// If this is from a preset, include the preset namespace in the asset path
|
|
41
|
+
let assetBasePath = "assets/aicm/";
|
|
42
|
+
if (presetName) {
|
|
43
|
+
const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
|
|
44
|
+
assetBasePath = node_path_1.default.posix.join("assets", "aicm", ...namespace) + "/";
|
|
45
|
+
}
|
|
46
|
+
const targetPath = upLevels + assetBasePath;
|
|
47
|
+
// Replace ../assets/ with the calculated target path
|
|
29
48
|
// Handles both forward slashes and backslashes for cross-platform compatibility
|
|
30
|
-
return content.replace(/\.\.[\\/]assets[\\/]/g,
|
|
49
|
+
return content.replace(/\.\.[\\/]assets[\\/]/g, targetPath);
|
|
31
50
|
}
|
|
32
51
|
function getTargetPaths() {
|
|
33
52
|
const projectDir = process.cwd();
|
|
@@ -54,8 +73,18 @@ function writeCursorRules(rules, cursorRulesDir) {
|
|
|
54
73
|
}
|
|
55
74
|
const ruleFile = rulePath + ".mdc";
|
|
56
75
|
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
|
|
76
|
+
// Calculate the depth for asset path rewriting
|
|
77
|
+
// cursorRulesDir is .cursor/rules/aicm (depth 2 from .cursor)
|
|
78
|
+
// Add namespace depth if present
|
|
79
|
+
let fileInstallDepth = 2; // rules, aicm
|
|
80
|
+
if (rule.presetName) {
|
|
81
|
+
const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
|
|
82
|
+
fileInstallDepth += namespace.length;
|
|
83
|
+
}
|
|
84
|
+
// Add any subdirectories in the rule name
|
|
85
|
+
fileInstallDepth += ruleNameParts.length - 1; // -1 because the last part is the filename
|
|
57
86
|
// Rewrite asset references before writing
|
|
58
|
-
const content = rewriteAssetReferences(rule.content);
|
|
87
|
+
const content = rewriteAssetReferences(rule.content, rule.presetName, fileInstallDepth);
|
|
59
88
|
fs_extra_1.default.writeFileSync(ruleFile, content);
|
|
60
89
|
}
|
|
61
90
|
}
|
|
@@ -69,8 +98,14 @@ function writeCursorCommands(commands, cursorCommandsDir) {
|
|
|
69
98
|
const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
|
|
70
99
|
const commandFile = commandPath + ".md";
|
|
71
100
|
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
|
|
101
|
+
// Calculate the depth for asset path rewriting
|
|
102
|
+
// cursorCommandsDir is .cursor/commands/aicm (depth 2 from .cursor)
|
|
103
|
+
// Commands are NOT namespaced by preset, but we still need to account for subdirectories
|
|
104
|
+
let fileInstallDepth = 2; // commands, aicm
|
|
105
|
+
// Add any subdirectories in the command name
|
|
106
|
+
fileInstallDepth += commandNameParts.length - 1; // -1 because the last part is the filename
|
|
72
107
|
// Rewrite asset references before writing
|
|
73
|
-
const content = rewriteAssetReferences(command.content);
|
|
108
|
+
const content = rewriteAssetReferences(command.content, command.presetName, fileInstallDepth);
|
|
74
109
|
fs_extra_1.default.writeFileSync(commandFile, content);
|
|
75
110
|
}
|
|
76
111
|
}
|
|
@@ -92,8 +127,12 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
|
|
|
92
127
|
// For local rules, maintain the original flat structure
|
|
93
128
|
rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
|
|
94
129
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
130
|
+
// For windsurf/codex/claude, assets are installed at the same namespace level as rules
|
|
131
|
+
// Example: .aicm/my-preset/rule.md and .aicm/my-preset/asset.json
|
|
132
|
+
// So we need to remove the 'assets/' part from the path
|
|
133
|
+
// ../assets/file.json -> ../file.json
|
|
134
|
+
// ../../assets/file.json -> ../../file.json
|
|
135
|
+
const content = rule.content.replace(/(\.\.[/\\])assets[/\\]/g, "$1");
|
|
97
136
|
const physicalRulePath = rulePath + ".md";
|
|
98
137
|
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
|
|
99
138
|
fs_extra_1.default.writeFileSync(physicalRulePath, content);
|
|
@@ -192,6 +231,105 @@ function writeCommandsToTargets(commands, targets) {
|
|
|
192
231
|
// Other targets do not support commands yet
|
|
193
232
|
}
|
|
194
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Get the skills installation path for a target
|
|
236
|
+
* Returns null for targets that don't support skills
|
|
237
|
+
*/
|
|
238
|
+
function getSkillsTargetPath(target) {
|
|
239
|
+
const projectDir = process.cwd();
|
|
240
|
+
switch (target) {
|
|
241
|
+
case "cursor":
|
|
242
|
+
return node_path_1.default.join(projectDir, ".cursor", "skills");
|
|
243
|
+
case "claude":
|
|
244
|
+
return node_path_1.default.join(projectDir, ".claude", "skills");
|
|
245
|
+
case "codex":
|
|
246
|
+
return node_path_1.default.join(projectDir, ".codex", "skills");
|
|
247
|
+
case "windsurf":
|
|
248
|
+
// Windsurf does not support skills
|
|
249
|
+
return null;
|
|
250
|
+
default:
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Write a single skill to the target directory
|
|
256
|
+
* Copies the entire skill directory and writes .aicm.json metadata
|
|
257
|
+
*/
|
|
258
|
+
function writeSkillToTarget(skill, targetSkillsDir) {
|
|
259
|
+
const skillTargetPath = node_path_1.default.join(targetSkillsDir, skill.name);
|
|
260
|
+
// Remove existing skill directory if it exists (to ensure clean install)
|
|
261
|
+
if (fs_extra_1.default.existsSync(skillTargetPath)) {
|
|
262
|
+
fs_extra_1.default.removeSync(skillTargetPath);
|
|
263
|
+
}
|
|
264
|
+
// Copy the entire skill directory
|
|
265
|
+
fs_extra_1.default.copySync(skill.sourcePath, skillTargetPath);
|
|
266
|
+
// Write .aicm.json metadata file
|
|
267
|
+
// The presence of this file indicates the skill is managed by aicm
|
|
268
|
+
const metadata = {
|
|
269
|
+
source: skill.source,
|
|
270
|
+
};
|
|
271
|
+
if (skill.presetName) {
|
|
272
|
+
metadata.presetName = skill.presetName;
|
|
273
|
+
}
|
|
274
|
+
const metadataPath = node_path_1.default.join(skillTargetPath, ".aicm.json");
|
|
275
|
+
fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Write skills to all supported target directories
|
|
279
|
+
*/
|
|
280
|
+
function writeSkillsToTargets(skills, targets) {
|
|
281
|
+
if (skills.length === 0)
|
|
282
|
+
return;
|
|
283
|
+
for (const target of targets) {
|
|
284
|
+
const targetSkillsDir = getSkillsTargetPath(target);
|
|
285
|
+
if (!targetSkillsDir) {
|
|
286
|
+
// Target doesn't support skills
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Ensure the skills directory exists
|
|
290
|
+
fs_extra_1.default.ensureDirSync(targetSkillsDir);
|
|
291
|
+
for (const skill of skills) {
|
|
292
|
+
writeSkillToTarget(skill, targetSkillsDir);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Warn about skill name collisions from different presets
|
|
298
|
+
*/
|
|
299
|
+
function warnPresetSkillCollisions(skills) {
|
|
300
|
+
const collisions = new Map();
|
|
301
|
+
for (const skill of skills) {
|
|
302
|
+
if (!skill.presetName)
|
|
303
|
+
continue;
|
|
304
|
+
const entry = collisions.get(skill.name);
|
|
305
|
+
if (entry) {
|
|
306
|
+
entry.presets.add(skill.presetName);
|
|
307
|
+
entry.lastPreset = skill.presetName;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
collisions.set(skill.name, {
|
|
311
|
+
presets: new Set([skill.presetName]),
|
|
312
|
+
lastPreset: skill.presetName,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const [skillName, { presets, lastPreset }] of collisions) {
|
|
317
|
+
if (presets.size > 1) {
|
|
318
|
+
const presetList = Array.from(presets).sort().join(", ");
|
|
319
|
+
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Dedupe skills by name (last one wins)
|
|
325
|
+
*/
|
|
326
|
+
function dedupeSkillsForInstall(skills) {
|
|
327
|
+
const unique = new Map();
|
|
328
|
+
for (const skill of skills) {
|
|
329
|
+
unique.set(skill.name, skill);
|
|
330
|
+
}
|
|
331
|
+
return Array.from(unique.values());
|
|
332
|
+
}
|
|
195
333
|
function warnPresetCommandCollisions(commands) {
|
|
196
334
|
const collisions = new Map();
|
|
197
335
|
for (const command of commands) {
|
|
@@ -312,10 +450,11 @@ async function installPackage(options = {}) {
|
|
|
312
450
|
installedCommandCount: 0,
|
|
313
451
|
installedAssetCount: 0,
|
|
314
452
|
installedHookCount: 0,
|
|
453
|
+
installedSkillCount: 0,
|
|
315
454
|
packagesCount: 0,
|
|
316
455
|
};
|
|
317
456
|
}
|
|
318
|
-
const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
|
|
457
|
+
const { config, rules, commands, assets, skills, mcpServers, hooks, hookFiles, } = resolvedConfig;
|
|
319
458
|
if (config.skipInstall === true) {
|
|
320
459
|
return {
|
|
321
460
|
success: true,
|
|
@@ -323,15 +462,19 @@ async function installPackage(options = {}) {
|
|
|
323
462
|
installedCommandCount: 0,
|
|
324
463
|
installedAssetCount: 0,
|
|
325
464
|
installedHookCount: 0,
|
|
465
|
+
installedSkillCount: 0,
|
|
326
466
|
packagesCount: 0,
|
|
327
467
|
};
|
|
328
468
|
}
|
|
329
469
|
warnPresetCommandCollisions(commands);
|
|
330
470
|
const commandsToInstall = dedupeCommandsForInstall(commands);
|
|
471
|
+
warnPresetSkillCollisions(skills);
|
|
472
|
+
const skillsToInstall = dedupeSkillsForInstall(skills);
|
|
331
473
|
try {
|
|
332
474
|
if (!options.dryRun) {
|
|
333
475
|
writeRulesToTargets(rules, assets, config.targets);
|
|
334
476
|
writeCommandsToTargets(commandsToInstall, config.targets);
|
|
477
|
+
writeSkillsToTargets(skillsToInstall, config.targets);
|
|
335
478
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
336
479
|
writeMcpServersToTargets(mcpServers, config.targets, cwd);
|
|
337
480
|
}
|
|
@@ -342,12 +485,14 @@ async function installPackage(options = {}) {
|
|
|
342
485
|
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
|
|
343
486
|
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
|
|
344
487
|
const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
|
|
488
|
+
const uniqueSkillCount = skillsToInstall.length;
|
|
345
489
|
return {
|
|
346
490
|
success: true,
|
|
347
491
|
installedRuleCount: uniqueRuleCount,
|
|
348
492
|
installedCommandCount: uniqueCommandCount,
|
|
349
493
|
installedAssetCount: assets.length,
|
|
350
494
|
installedHookCount: uniqueHookCount,
|
|
495
|
+
installedSkillCount: uniqueSkillCount,
|
|
351
496
|
packagesCount: 1,
|
|
352
497
|
};
|
|
353
498
|
}
|
|
@@ -359,6 +504,7 @@ async function installPackage(options = {}) {
|
|
|
359
504
|
installedCommandCount: 0,
|
|
360
505
|
installedAssetCount: 0,
|
|
361
506
|
installedHookCount: 0,
|
|
507
|
+
installedSkillCount: 0,
|
|
362
508
|
packagesCount: 0,
|
|
363
509
|
};
|
|
364
510
|
}
|
|
@@ -379,6 +525,7 @@ async function install(options = {}) {
|
|
|
379
525
|
installedCommandCount: 0,
|
|
380
526
|
installedAssetCount: 0,
|
|
381
527
|
installedHookCount: 0,
|
|
528
|
+
installedSkillCount: 0,
|
|
382
529
|
packagesCount: 0,
|
|
383
530
|
};
|
|
384
531
|
}
|
|
@@ -411,11 +558,15 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
411
558
|
const ruleCount = result.installedRuleCount;
|
|
412
559
|
const commandCount = result.installedCommandCount;
|
|
413
560
|
const hookCount = result.installedHookCount;
|
|
561
|
+
const skillCount = result.installedSkillCount;
|
|
414
562
|
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
|
|
415
563
|
const commandMessage = commandCount > 0
|
|
416
564
|
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
|
|
417
565
|
: null;
|
|
418
566
|
const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
|
|
567
|
+
const skillMessage = skillCount > 0
|
|
568
|
+
? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
|
|
569
|
+
: null;
|
|
419
570
|
const countsParts = [];
|
|
420
571
|
if (ruleMessage) {
|
|
421
572
|
countsParts.push(ruleMessage);
|
|
@@ -426,6 +577,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
426
577
|
if (hookMessage) {
|
|
427
578
|
countsParts.push(hookMessage);
|
|
428
579
|
}
|
|
580
|
+
if (skillMessage) {
|
|
581
|
+
countsParts.push(skillMessage);
|
|
582
|
+
}
|
|
429
583
|
const countsMessage = countsParts.length > 0
|
|
430
584
|
? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
|
|
431
585
|
: "0 rules";
|
|
@@ -437,8 +591,11 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
437
591
|
console.log(`Dry run: validated ${countsMessage}`);
|
|
438
592
|
}
|
|
439
593
|
}
|
|
440
|
-
else if (ruleCount === 0 &&
|
|
441
|
-
|
|
594
|
+
else if (ruleCount === 0 &&
|
|
595
|
+
commandCount === 0 &&
|
|
596
|
+
hookCount === 0 &&
|
|
597
|
+
skillCount === 0) {
|
|
598
|
+
console.log("No rules, commands, hooks, or skills installed");
|
|
442
599
|
}
|
|
443
600
|
else if (result.packagesCount > 1) {
|
|
444
601
|
console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -46,6 +46,12 @@ export interface AssetFile {
|
|
|
46
46
|
}
|
|
47
47
|
export type RuleFile = ManagedFile;
|
|
48
48
|
export type CommandFile = ManagedFile;
|
|
49
|
+
export interface SkillFile {
|
|
50
|
+
name: string;
|
|
51
|
+
sourcePath: string;
|
|
52
|
+
source: "local" | "preset";
|
|
53
|
+
presetName?: string;
|
|
54
|
+
}
|
|
49
55
|
export interface RuleCollection {
|
|
50
56
|
[target: string]: RuleFile[];
|
|
51
57
|
}
|
|
@@ -54,6 +60,7 @@ export interface ResolvedConfig {
|
|
|
54
60
|
rules: RuleFile[];
|
|
55
61
|
commands: CommandFile[];
|
|
56
62
|
assets: AssetFile[];
|
|
63
|
+
skills: SkillFile[];
|
|
57
64
|
mcpServers: MCPServers;
|
|
58
65
|
hooks: HooksJson;
|
|
59
66
|
hookFiles: HookFile[];
|
|
@@ -68,6 +75,11 @@ export declare function validateConfig(config: unknown, configFilePath: string,
|
|
|
68
75
|
export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
|
|
69
76
|
export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
|
|
70
77
|
export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Load skills from a skills/ directory
|
|
80
|
+
* Each direct subdirectory containing a SKILL.md file is considered a skill
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadSkillsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<SkillFile[]>;
|
|
71
83
|
/**
|
|
72
84
|
* Extract namespace from preset path for directory structure
|
|
73
85
|
* Handles both npm packages and local paths consistently
|
|
@@ -82,10 +94,16 @@ export declare function loadAllRules(config: Config, cwd: string): Promise<{
|
|
|
82
94
|
rules: RuleFile[];
|
|
83
95
|
commands: CommandFile[];
|
|
84
96
|
assets: AssetFile[];
|
|
97
|
+
skills: SkillFile[];
|
|
85
98
|
mcpServers: MCPServers;
|
|
86
99
|
hooks: HooksJson;
|
|
87
100
|
hookFiles: HookFile[];
|
|
88
101
|
}>;
|
|
89
102
|
export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
105
|
+
* This is useful for commands that only need to know the workspace setting
|
|
106
|
+
*/
|
|
107
|
+
export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
|
|
90
108
|
export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
|
|
91
109
|
export declare function saveConfig(config: Config, cwd?: string): boolean;
|
package/dist/utils/config.js
CHANGED
|
@@ -11,11 +11,13 @@ exports.validateConfig = validateConfig;
|
|
|
11
11
|
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
12
|
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
13
13
|
exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
|
|
14
|
+
exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
|
|
14
15
|
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
15
16
|
exports.resolvePresetPath = resolvePresetPath;
|
|
16
17
|
exports.loadPreset = loadPreset;
|
|
17
18
|
exports.loadAllRules = loadAllRules;
|
|
18
19
|
exports.loadConfigFile = loadConfigFile;
|
|
20
|
+
exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
|
|
19
21
|
exports.loadConfig = loadConfig;
|
|
20
22
|
exports.saveConfig = saveConfig;
|
|
21
23
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -95,14 +97,16 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
95
97
|
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
96
98
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
97
99
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
|
|
100
|
+
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "skills"));
|
|
98
101
|
// In workspace mode, root config doesn't need these directories
|
|
99
102
|
// since packages will have their own configurations
|
|
100
103
|
if (!isWorkspaceMode &&
|
|
101
104
|
!hasRules &&
|
|
102
105
|
!hasCommands &&
|
|
103
106
|
!hasHooks &&
|
|
107
|
+
!hasSkills &&
|
|
104
108
|
!hasPresets) {
|
|
105
|
-
throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
|
|
109
|
+
throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, hooks.json, or have presets configured`);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
else if (!isWorkspaceMode && !hasPresets) {
|
|
@@ -201,6 +205,36 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
|
201
205
|
}
|
|
202
206
|
return assets;
|
|
203
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Load skills from a skills/ directory
|
|
210
|
+
* Each direct subdirectory containing a SKILL.md file is considered a skill
|
|
211
|
+
*/
|
|
212
|
+
async function loadSkillsFromDirectory(directoryPath, source, presetName) {
|
|
213
|
+
const skills = [];
|
|
214
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
215
|
+
return skills;
|
|
216
|
+
}
|
|
217
|
+
// Get all direct subdirectories
|
|
218
|
+
const entries = await fs_extra_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (!entry.isDirectory()) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const skillPath = node_path_1.default.join(directoryPath, entry.name);
|
|
224
|
+
const skillMdPath = node_path_1.default.join(skillPath, "SKILL.md");
|
|
225
|
+
// Only include directories that contain a SKILL.md file
|
|
226
|
+
if (!fs_extra_1.default.existsSync(skillMdPath)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
skills.push({
|
|
230
|
+
name: entry.name,
|
|
231
|
+
sourcePath: skillPath,
|
|
232
|
+
source,
|
|
233
|
+
presetName,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return skills;
|
|
237
|
+
}
|
|
204
238
|
/**
|
|
205
239
|
* Extract namespace from preset path for directory structure
|
|
206
240
|
* Handles both npm packages and local paths consistently
|
|
@@ -257,8 +291,9 @@ async function loadPreset(presetPath, cwd) {
|
|
|
257
291
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
258
292
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
259
293
|
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
260
|
-
|
|
261
|
-
|
|
294
|
+
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
|
|
295
|
+
if (!hasRules && !hasCommands && !hasHooks && !hasAssets && !hasSkills) {
|
|
296
|
+
throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, hooks.json, or assets/`);
|
|
262
297
|
}
|
|
263
298
|
return {
|
|
264
299
|
config: presetConfig,
|
|
@@ -269,6 +304,7 @@ async function loadAllRules(config, cwd) {
|
|
|
269
304
|
const allRules = [];
|
|
270
305
|
const allCommands = [];
|
|
271
306
|
const allAssets = [];
|
|
307
|
+
const allSkills = [];
|
|
272
308
|
const allHookFiles = [];
|
|
273
309
|
const allHooksConfigs = [];
|
|
274
310
|
let mergedMcpServers = { ...config.mcpServers };
|
|
@@ -300,6 +336,12 @@ async function loadAllRules(config, cwd) {
|
|
|
300
336
|
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
301
337
|
allAssets.push(...localAssets);
|
|
302
338
|
}
|
|
339
|
+
// Load skills from skills/ subdirectory
|
|
340
|
+
const skillsPath = node_path_1.default.join(rootPath, "skills");
|
|
341
|
+
if (fs_extra_1.default.existsSync(skillsPath)) {
|
|
342
|
+
const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
|
|
343
|
+
allSkills.push(...localSkills);
|
|
344
|
+
}
|
|
303
345
|
}
|
|
304
346
|
// Load presets
|
|
305
347
|
if (config.presets) {
|
|
@@ -331,6 +373,12 @@ async function loadAllRules(config, cwd) {
|
|
|
331
373
|
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
332
374
|
allAssets.push(...presetAssets);
|
|
333
375
|
}
|
|
376
|
+
// Load preset skills from skills/ subdirectory
|
|
377
|
+
const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
|
|
378
|
+
if (fs_extra_1.default.existsSync(presetSkillsPath)) {
|
|
379
|
+
const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
|
|
380
|
+
allSkills.push(...presetSkills);
|
|
381
|
+
}
|
|
334
382
|
// Merge MCP servers from preset
|
|
335
383
|
if (preset.config.mcpServers) {
|
|
336
384
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
@@ -343,6 +391,7 @@ async function loadAllRules(config, cwd) {
|
|
|
343
391
|
rules: allRules,
|
|
344
392
|
commands: allCommands,
|
|
345
393
|
assets: allAssets,
|
|
394
|
+
skills: allSkills,
|
|
346
395
|
mcpServers: mergedMcpServers,
|
|
347
396
|
hooks: mergedHooks,
|
|
348
397
|
hookFiles: allHookFiles,
|
|
@@ -380,6 +429,18 @@ async function loadConfigFile(searchFrom) {
|
|
|
380
429
|
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
381
430
|
}
|
|
382
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
434
|
+
* This is useful for commands that only need to know the workspace setting
|
|
435
|
+
*/
|
|
436
|
+
async function checkWorkspacesEnabled(cwd) {
|
|
437
|
+
const workingDir = cwd || process.cwd();
|
|
438
|
+
const configResult = await loadConfigFile(workingDir);
|
|
439
|
+
if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
|
|
440
|
+
return detectWorkspacesFromPackageJson(workingDir);
|
|
441
|
+
}
|
|
442
|
+
return resolveWorkspaces(configResult.config, configResult.filepath, workingDir);
|
|
443
|
+
}
|
|
383
444
|
async function loadConfig(cwd) {
|
|
384
445
|
const workingDir = cwd || process.cwd();
|
|
385
446
|
const configResult = await loadConfigFile(workingDir);
|
|
@@ -390,12 +451,13 @@ async function loadConfig(cwd) {
|
|
|
390
451
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
391
452
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
392
453
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
393
|
-
const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
454
|
+
const { rules, commands, assets, skills, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
394
455
|
return {
|
|
395
456
|
config: configWithDefaults,
|
|
396
457
|
rules,
|
|
397
458
|
commands,
|
|
398
459
|
assets,
|
|
460
|
+
skills,
|
|
399
461
|
mcpServers,
|
|
400
462
|
hooks,
|
|
401
463
|
hookFiles,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "A TypeScript CLI tool for managing AI IDE rules across different projects and teams",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"types": "dist/api.d.ts",
|
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
"README.md",
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"watch": "tsc --watch",
|
|
18
|
+
"start": "node dist/bin/aicm.js",
|
|
19
|
+
"dev": "ts-node src/bin/aicm.ts",
|
|
20
|
+
"test": "pnpm build && jest",
|
|
21
|
+
"test:watch": "jest --watch",
|
|
22
|
+
"test:all": "npm run build && npm run test",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"lint": "eslint",
|
|
26
|
+
"prepare": "husky install && npx ts-node src/bin/aicm.ts install",
|
|
27
|
+
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
28
|
+
"release": "np --no-tests"
|
|
29
|
+
},
|
|
15
30
|
"keywords": [
|
|
16
31
|
"ai",
|
|
17
32
|
"ide",
|
|
@@ -51,18 +66,5 @@
|
|
|
51
66
|
"*.{js,ts,json,md,mjs}": "prettier --write",
|
|
52
67
|
"*.ts": "eslint"
|
|
53
68
|
},
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"watch": "tsc --watch",
|
|
57
|
-
"start": "node dist/bin/aicm.js",
|
|
58
|
-
"dev": "ts-node src/bin/aicm.ts",
|
|
59
|
-
"test": "pnpm build && jest",
|
|
60
|
-
"test:watch": "jest --watch",
|
|
61
|
-
"test:all": "npm run build && npm run test",
|
|
62
|
-
"format": "prettier --write .",
|
|
63
|
-
"format:check": "prettier --check .",
|
|
64
|
-
"lint": "eslint",
|
|
65
|
-
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
66
|
-
"release": "np --no-tests"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
+
"packageManager": "pnpm@10.18.3"
|
|
70
|
+
}
|