aicm 0.19.1 → 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/bin/aicm.js +0 -0
- package/dist/commands/clean.js +55 -0
- package/dist/commands/install-workspaces.js +76 -3
- package/dist/commands/install.d.ts +17 -1
- package/dist/commands/install.js +124 -3
- package/dist/utils/config.d.ts +13 -0
- package/dist/utils/config.js +53 -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/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 {
|
|
@@ -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;
|
|
@@ -228,6 +231,105 @@ function writeCommandsToTargets(commands, targets) {
|
|
|
228
231
|
// Other targets do not support commands yet
|
|
229
232
|
}
|
|
230
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
|
+
}
|
|
231
333
|
function warnPresetCommandCollisions(commands) {
|
|
232
334
|
const collisions = new Map();
|
|
233
335
|
for (const command of commands) {
|
|
@@ -348,10 +450,11 @@ async function installPackage(options = {}) {
|
|
|
348
450
|
installedCommandCount: 0,
|
|
349
451
|
installedAssetCount: 0,
|
|
350
452
|
installedHookCount: 0,
|
|
453
|
+
installedSkillCount: 0,
|
|
351
454
|
packagesCount: 0,
|
|
352
455
|
};
|
|
353
456
|
}
|
|
354
|
-
const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
|
|
457
|
+
const { config, rules, commands, assets, skills, mcpServers, hooks, hookFiles, } = resolvedConfig;
|
|
355
458
|
if (config.skipInstall === true) {
|
|
356
459
|
return {
|
|
357
460
|
success: true,
|
|
@@ -359,15 +462,19 @@ async function installPackage(options = {}) {
|
|
|
359
462
|
installedCommandCount: 0,
|
|
360
463
|
installedAssetCount: 0,
|
|
361
464
|
installedHookCount: 0,
|
|
465
|
+
installedSkillCount: 0,
|
|
362
466
|
packagesCount: 0,
|
|
363
467
|
};
|
|
364
468
|
}
|
|
365
469
|
warnPresetCommandCollisions(commands);
|
|
366
470
|
const commandsToInstall = dedupeCommandsForInstall(commands);
|
|
471
|
+
warnPresetSkillCollisions(skills);
|
|
472
|
+
const skillsToInstall = dedupeSkillsForInstall(skills);
|
|
367
473
|
try {
|
|
368
474
|
if (!options.dryRun) {
|
|
369
475
|
writeRulesToTargets(rules, assets, config.targets);
|
|
370
476
|
writeCommandsToTargets(commandsToInstall, config.targets);
|
|
477
|
+
writeSkillsToTargets(skillsToInstall, config.targets);
|
|
371
478
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
372
479
|
writeMcpServersToTargets(mcpServers, config.targets, cwd);
|
|
373
480
|
}
|
|
@@ -378,12 +485,14 @@ async function installPackage(options = {}) {
|
|
|
378
485
|
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
|
|
379
486
|
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
|
|
380
487
|
const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
|
|
488
|
+
const uniqueSkillCount = skillsToInstall.length;
|
|
381
489
|
return {
|
|
382
490
|
success: true,
|
|
383
491
|
installedRuleCount: uniqueRuleCount,
|
|
384
492
|
installedCommandCount: uniqueCommandCount,
|
|
385
493
|
installedAssetCount: assets.length,
|
|
386
494
|
installedHookCount: uniqueHookCount,
|
|
495
|
+
installedSkillCount: uniqueSkillCount,
|
|
387
496
|
packagesCount: 1,
|
|
388
497
|
};
|
|
389
498
|
}
|
|
@@ -395,6 +504,7 @@ async function installPackage(options = {}) {
|
|
|
395
504
|
installedCommandCount: 0,
|
|
396
505
|
installedAssetCount: 0,
|
|
397
506
|
installedHookCount: 0,
|
|
507
|
+
installedSkillCount: 0,
|
|
398
508
|
packagesCount: 0,
|
|
399
509
|
};
|
|
400
510
|
}
|
|
@@ -415,6 +525,7 @@ async function install(options = {}) {
|
|
|
415
525
|
installedCommandCount: 0,
|
|
416
526
|
installedAssetCount: 0,
|
|
417
527
|
installedHookCount: 0,
|
|
528
|
+
installedSkillCount: 0,
|
|
418
529
|
packagesCount: 0,
|
|
419
530
|
};
|
|
420
531
|
}
|
|
@@ -447,11 +558,15 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
447
558
|
const ruleCount = result.installedRuleCount;
|
|
448
559
|
const commandCount = result.installedCommandCount;
|
|
449
560
|
const hookCount = result.installedHookCount;
|
|
561
|
+
const skillCount = result.installedSkillCount;
|
|
450
562
|
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
|
|
451
563
|
const commandMessage = commandCount > 0
|
|
452
564
|
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
|
|
453
565
|
: null;
|
|
454
566
|
const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
|
|
567
|
+
const skillMessage = skillCount > 0
|
|
568
|
+
? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
|
|
569
|
+
: null;
|
|
455
570
|
const countsParts = [];
|
|
456
571
|
if (ruleMessage) {
|
|
457
572
|
countsParts.push(ruleMessage);
|
|
@@ -462,6 +577,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
462
577
|
if (hookMessage) {
|
|
463
578
|
countsParts.push(hookMessage);
|
|
464
579
|
}
|
|
580
|
+
if (skillMessage) {
|
|
581
|
+
countsParts.push(skillMessage);
|
|
582
|
+
}
|
|
465
583
|
const countsMessage = countsParts.length > 0
|
|
466
584
|
? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
|
|
467
585
|
: "0 rules";
|
|
@@ -473,8 +591,11 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
473
591
|
console.log(`Dry run: validated ${countsMessage}`);
|
|
474
592
|
}
|
|
475
593
|
}
|
|
476
|
-
else if (ruleCount === 0 &&
|
|
477
|
-
|
|
594
|
+
else if (ruleCount === 0 &&
|
|
595
|
+
commandCount === 0 &&
|
|
596
|
+
hookCount === 0 &&
|
|
597
|
+
skillCount === 0) {
|
|
598
|
+
console.log("No rules, commands, hooks, or skills installed");
|
|
478
599
|
}
|
|
479
600
|
else if (result.packagesCount > 1) {
|
|
480
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,6 +94,7 @@ 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[];
|
package/dist/utils/config.js
CHANGED
|
@@ -11,6 +11,7 @@ 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;
|
|
@@ -96,14 +97,16 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
96
97
|
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
97
98
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
98
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"));
|
|
99
101
|
// In workspace mode, root config doesn't need these directories
|
|
100
102
|
// since packages will have their own configurations
|
|
101
103
|
if (!isWorkspaceMode &&
|
|
102
104
|
!hasRules &&
|
|
103
105
|
!hasCommands &&
|
|
104
106
|
!hasHooks &&
|
|
107
|
+
!hasSkills &&
|
|
105
108
|
!hasPresets) {
|
|
106
|
-
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`);
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
else if (!isWorkspaceMode && !hasPresets) {
|
|
@@ -202,6 +205,36 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
|
202
205
|
}
|
|
203
206
|
return assets;
|
|
204
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
|
+
}
|
|
205
238
|
/**
|
|
206
239
|
* Extract namespace from preset path for directory structure
|
|
207
240
|
* Handles both npm packages and local paths consistently
|
|
@@ -258,8 +291,9 @@ async function loadPreset(presetPath, cwd) {
|
|
|
258
291
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
259
292
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
260
293
|
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
261
|
-
|
|
262
|
-
|
|
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/`);
|
|
263
297
|
}
|
|
264
298
|
return {
|
|
265
299
|
config: presetConfig,
|
|
@@ -270,6 +304,7 @@ async function loadAllRules(config, cwd) {
|
|
|
270
304
|
const allRules = [];
|
|
271
305
|
const allCommands = [];
|
|
272
306
|
const allAssets = [];
|
|
307
|
+
const allSkills = [];
|
|
273
308
|
const allHookFiles = [];
|
|
274
309
|
const allHooksConfigs = [];
|
|
275
310
|
let mergedMcpServers = { ...config.mcpServers };
|
|
@@ -301,6 +336,12 @@ async function loadAllRules(config, cwd) {
|
|
|
301
336
|
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
302
337
|
allAssets.push(...localAssets);
|
|
303
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
|
+
}
|
|
304
345
|
}
|
|
305
346
|
// Load presets
|
|
306
347
|
if (config.presets) {
|
|
@@ -332,6 +373,12 @@ async function loadAllRules(config, cwd) {
|
|
|
332
373
|
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
333
374
|
allAssets.push(...presetAssets);
|
|
334
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
|
+
}
|
|
335
382
|
// Merge MCP servers from preset
|
|
336
383
|
if (preset.config.mcpServers) {
|
|
337
384
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
@@ -344,6 +391,7 @@ async function loadAllRules(config, cwd) {
|
|
|
344
391
|
rules: allRules,
|
|
345
392
|
commands: allCommands,
|
|
346
393
|
assets: allAssets,
|
|
394
|
+
skills: allSkills,
|
|
347
395
|
mcpServers: mergedMcpServers,
|
|
348
396
|
hooks: mergedHooks,
|
|
349
397
|
hookFiles: allHookFiles,
|
|
@@ -403,12 +451,13 @@ async function loadConfig(cwd) {
|
|
|
403
451
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
404
452
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
405
453
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
406
|
-
const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
454
|
+
const { rules, commands, assets, skills, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
407
455
|
return {
|
|
408
456
|
config: configWithDefaults,
|
|
409
457
|
rules,
|
|
410
458
|
commands,
|
|
411
459
|
assets,
|
|
460
|
+
skills,
|
|
412
461
|
mcpServers,
|
|
413
462
|
hooks,
|
|
414
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
|
+
}
|