aicm 0.20.5 → 0.20.6
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/dist/api.d.ts +16 -0
- package/dist/api.js +22 -0
- package/dist/bin/aicm.d.ts +2 -0
- package/dist/bin/aicm.js +5 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +102 -0
- package/dist/commands/clean.d.ts +19 -0
- package/dist/commands/clean.js +385 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +49 -0
- package/dist/commands/install-workspaces.d.ts +5 -0
- package/dist/commands/install-workspaces.js +421 -0
- package/dist/commands/install.d.ts +110 -0
- package/dist/commands/install.js +736 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +40 -0
- package/dist/utils/config.d.ts +118 -0
- package/dist/utils/config.js +582 -0
- package/dist/utils/hooks.d.ts +50 -0
- package/dist/utils/hooks.js +346 -0
- package/dist/utils/is-ci.d.ts +1 -0
- package/dist/utils/is-ci.js +8 -0
- package/dist/utils/rules-file-writer.d.ts +24 -0
- package/dist/utils/rules-file-writer.js +197 -0
- package/dist/utils/working-directory.d.ts +5 -0
- package/dist/utils/working-directory.js +21 -0
- package/dist/utils/workspace-discovery.d.ts +13 -0
- package/dist/utils/workspace-discovery.js +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SUPPORTED_TARGETS = exports.ALLOWED_CONFIG_KEYS = void 0;
|
|
7
|
+
exports.detectWorkspacesFromPackageJson = detectWorkspacesFromPackageJson;
|
|
8
|
+
exports.resolveWorkspaces = resolveWorkspaces;
|
|
9
|
+
exports.applyDefaults = applyDefaults;
|
|
10
|
+
exports.validateConfig = validateConfig;
|
|
11
|
+
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
|
+
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
13
|
+
exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
|
|
14
|
+
exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
|
|
15
|
+
exports.loadAgentsFromDirectory = loadAgentsFromDirectory;
|
|
16
|
+
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
17
|
+
exports.resolvePresetPath = resolvePresetPath;
|
|
18
|
+
exports.loadPreset = loadPreset;
|
|
19
|
+
exports.loadAllRules = loadAllRules;
|
|
20
|
+
exports.loadConfigFile = loadConfigFile;
|
|
21
|
+
exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
|
|
22
|
+
exports.loadConfig = loadConfig;
|
|
23
|
+
exports.saveConfig = saveConfig;
|
|
24
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
const cosmiconfig_1 = require("cosmiconfig");
|
|
27
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
28
|
+
const hooks_1 = require("./hooks");
|
|
29
|
+
exports.ALLOWED_CONFIG_KEYS = [
|
|
30
|
+
"rootDir",
|
|
31
|
+
"targets",
|
|
32
|
+
"presets",
|
|
33
|
+
"mcpServers",
|
|
34
|
+
"workspaces",
|
|
35
|
+
"skipInstall",
|
|
36
|
+
];
|
|
37
|
+
exports.SUPPORTED_TARGETS = [
|
|
38
|
+
"cursor",
|
|
39
|
+
"windsurf",
|
|
40
|
+
"codex",
|
|
41
|
+
"claude",
|
|
42
|
+
];
|
|
43
|
+
function detectWorkspacesFromPackageJson(cwd) {
|
|
44
|
+
try {
|
|
45
|
+
const packageJsonPath = node_path_1.default.join(cwd, "package.json");
|
|
46
|
+
if (!fs_extra_1.default.existsSync(packageJsonPath)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const packageJson = JSON.parse(fs_extra_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
50
|
+
return Boolean(packageJson.workspaces);
|
|
51
|
+
}
|
|
52
|
+
catch (_a) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveWorkspaces(config, configFilePath, cwd) {
|
|
57
|
+
const hasConfigWorkspaces = typeof config === "object" && config !== null && "workspaces" in config;
|
|
58
|
+
if (hasConfigWorkspaces) {
|
|
59
|
+
if (typeof config.workspaces === "boolean") {
|
|
60
|
+
return config.workspaces;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`workspaces must be a boolean in config at ${configFilePath}`);
|
|
63
|
+
}
|
|
64
|
+
return detectWorkspacesFromPackageJson(cwd);
|
|
65
|
+
}
|
|
66
|
+
function applyDefaults(config, workspaces) {
|
|
67
|
+
return {
|
|
68
|
+
rootDir: config.rootDir,
|
|
69
|
+
targets: config.targets || ["cursor"],
|
|
70
|
+
presets: config.presets || [],
|
|
71
|
+
mcpServers: config.mcpServers || {},
|
|
72
|
+
workspaces,
|
|
73
|
+
skipInstall: config.skipInstall || false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
77
|
+
if (typeof config !== "object" || config === null) {
|
|
78
|
+
throw new Error(`Config is not an object at ${configFilePath}`);
|
|
79
|
+
}
|
|
80
|
+
const unknownKeys = Object.keys(config).filter((key) => !exports.ALLOWED_CONFIG_KEYS.includes(key));
|
|
81
|
+
if (unknownKeys.length > 0) {
|
|
82
|
+
throw new Error(`Invalid configuration at ${configFilePath}: unknown keys: ${unknownKeys.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
// Validate rootDir
|
|
85
|
+
const hasRootDir = "rootDir" in config && typeof config.rootDir === "string";
|
|
86
|
+
const hasPresets = "presets" in config &&
|
|
87
|
+
Array.isArray(config.presets) &&
|
|
88
|
+
config.presets.length > 0;
|
|
89
|
+
if (hasRootDir) {
|
|
90
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
91
|
+
if (!fs_extra_1.default.existsSync(rootPath)) {
|
|
92
|
+
throw new Error(`Root directory does not exist: ${rootPath}`);
|
|
93
|
+
}
|
|
94
|
+
if (!fs_extra_1.default.statSync(rootPath).isDirectory()) {
|
|
95
|
+
throw new Error(`Root path is not a directory: ${rootPath}`);
|
|
96
|
+
}
|
|
97
|
+
// Check for at least one valid subdirectory or file
|
|
98
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
99
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
100
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
|
|
101
|
+
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "skills"));
|
|
102
|
+
const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "agents"));
|
|
103
|
+
// In workspace mode, root config doesn't need these directories
|
|
104
|
+
// since packages will have their own configurations
|
|
105
|
+
if (!isWorkspaceMode &&
|
|
106
|
+
!hasRules &&
|
|
107
|
+
!hasCommands &&
|
|
108
|
+
!hasHooks &&
|
|
109
|
+
!hasSkills &&
|
|
110
|
+
!hasAgents &&
|
|
111
|
+
!hasPresets) {
|
|
112
|
+
throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, agents/, hooks.json, or have presets configured`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (!isWorkspaceMode && !hasPresets) {
|
|
116
|
+
// If no rootDir specified and not in workspace mode, must have presets
|
|
117
|
+
throw new Error(`At least one of rootDir or presets must be specified in config at ${configFilePath}`);
|
|
118
|
+
}
|
|
119
|
+
if ("targets" in config) {
|
|
120
|
+
if (!Array.isArray(config.targets)) {
|
|
121
|
+
throw new Error(`targets must be an array in config at ${configFilePath}`);
|
|
122
|
+
}
|
|
123
|
+
if (config.targets.length === 0) {
|
|
124
|
+
throw new Error(`targets must not be empty in config at ${configFilePath}`);
|
|
125
|
+
}
|
|
126
|
+
for (const target of config.targets) {
|
|
127
|
+
if (!exports.SUPPORTED_TARGETS.includes(target)) {
|
|
128
|
+
throw new Error(`Unsupported target: ${target}. Supported targets: ${exports.SUPPORTED_TARGETS.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function loadRulesFromDirectory(directoryPath, source, presetName) {
|
|
134
|
+
const rules = [];
|
|
135
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
136
|
+
return rules;
|
|
137
|
+
}
|
|
138
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.mdc").replace(/\\/g, "/");
|
|
139
|
+
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
140
|
+
onlyFiles: true,
|
|
141
|
+
absolute: true,
|
|
142
|
+
});
|
|
143
|
+
for (const filePath of filePaths) {
|
|
144
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
145
|
+
// Preserve directory structure by using relative path from source directory
|
|
146
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
147
|
+
const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/");
|
|
148
|
+
rules.push({
|
|
149
|
+
name: ruleName,
|
|
150
|
+
content,
|
|
151
|
+
sourcePath: filePath,
|
|
152
|
+
source,
|
|
153
|
+
presetName,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return rules;
|
|
157
|
+
}
|
|
158
|
+
async function loadCommandsFromDirectory(directoryPath, source, presetName) {
|
|
159
|
+
const commands = [];
|
|
160
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
161
|
+
return commands;
|
|
162
|
+
}
|
|
163
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
|
|
164
|
+
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
165
|
+
onlyFiles: true,
|
|
166
|
+
absolute: true,
|
|
167
|
+
});
|
|
168
|
+
filePaths.sort();
|
|
169
|
+
for (const filePath of filePaths) {
|
|
170
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
171
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
172
|
+
const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
173
|
+
commands.push({
|
|
174
|
+
name: commandName,
|
|
175
|
+
content,
|
|
176
|
+
sourcePath: filePath,
|
|
177
|
+
source,
|
|
178
|
+
presetName,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return commands;
|
|
182
|
+
}
|
|
183
|
+
async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
184
|
+
const assets = [];
|
|
185
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
186
|
+
return assets;
|
|
187
|
+
}
|
|
188
|
+
// Find all files except .mdc files and hidden files
|
|
189
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*").replace(/\\/g, "/");
|
|
190
|
+
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
191
|
+
onlyFiles: true,
|
|
192
|
+
absolute: true,
|
|
193
|
+
ignore: ["**/*.mdc", "**/.*"],
|
|
194
|
+
});
|
|
195
|
+
for (const filePath of filePaths) {
|
|
196
|
+
const content = await fs_extra_1.default.readFile(filePath);
|
|
197
|
+
// Preserve directory structure by using relative path from source directory
|
|
198
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
199
|
+
// Keep extension for assets
|
|
200
|
+
const assetName = relativePath.replace(/\\/g, "/");
|
|
201
|
+
assets.push({
|
|
202
|
+
name: assetName,
|
|
203
|
+
content,
|
|
204
|
+
sourcePath: filePath,
|
|
205
|
+
source,
|
|
206
|
+
presetName,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return assets;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Load skills from a skills/ directory
|
|
213
|
+
* Each direct subdirectory containing a SKILL.md file is considered a skill
|
|
214
|
+
*/
|
|
215
|
+
async function loadSkillsFromDirectory(directoryPath, source, presetName) {
|
|
216
|
+
const skills = [];
|
|
217
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
218
|
+
return skills;
|
|
219
|
+
}
|
|
220
|
+
// Get all direct subdirectories
|
|
221
|
+
const entries = await fs_extra_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (!entry.isDirectory()) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const skillPath = node_path_1.default.join(directoryPath, entry.name);
|
|
227
|
+
const skillMdPath = node_path_1.default.join(skillPath, "SKILL.md");
|
|
228
|
+
// Only include directories that contain a SKILL.md file
|
|
229
|
+
if (!fs_extra_1.default.existsSync(skillMdPath)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
skills.push({
|
|
233
|
+
name: entry.name,
|
|
234
|
+
sourcePath: skillPath,
|
|
235
|
+
source,
|
|
236
|
+
presetName,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return skills;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Load agents from an agents/ directory
|
|
243
|
+
* Agents are markdown files (.md) with YAML frontmatter
|
|
244
|
+
*/
|
|
245
|
+
async function loadAgentsFromDirectory(directoryPath, source, presetName) {
|
|
246
|
+
const agents = [];
|
|
247
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
248
|
+
return agents;
|
|
249
|
+
}
|
|
250
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
|
|
251
|
+
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
252
|
+
onlyFiles: true,
|
|
253
|
+
absolute: true,
|
|
254
|
+
});
|
|
255
|
+
filePaths.sort();
|
|
256
|
+
for (const filePath of filePaths) {
|
|
257
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
258
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
259
|
+
const agentName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
260
|
+
agents.push({
|
|
261
|
+
name: agentName,
|
|
262
|
+
content,
|
|
263
|
+
sourcePath: filePath,
|
|
264
|
+
source,
|
|
265
|
+
presetName,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return agents;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Extract namespace from preset path for directory structure
|
|
272
|
+
* Handles both npm packages and local paths consistently
|
|
273
|
+
*/
|
|
274
|
+
function extractNamespaceFromPresetPath(presetPath) {
|
|
275
|
+
// Special case: npm package names always use forward slashes, regardless of platform
|
|
276
|
+
if (presetPath.startsWith("@")) {
|
|
277
|
+
// For scoped packages like @scope/package/subdir, create nested directories
|
|
278
|
+
return presetPath.split("/");
|
|
279
|
+
}
|
|
280
|
+
// Always split by forward slash since JSON config files use forward slashes on all platforms
|
|
281
|
+
const parts = presetPath.split(node_path_1.default.posix.sep);
|
|
282
|
+
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
283
|
+
}
|
|
284
|
+
function resolvePresetPath(presetPath, cwd) {
|
|
285
|
+
// Support specifying aicm.json directory and load the config from it
|
|
286
|
+
if (!presetPath.endsWith(".json")) {
|
|
287
|
+
presetPath = node_path_1.default.join(presetPath, "aicm.json");
|
|
288
|
+
}
|
|
289
|
+
// Support local or absolute paths
|
|
290
|
+
const absolutePath = node_path_1.default.isAbsolute(presetPath)
|
|
291
|
+
? presetPath
|
|
292
|
+
: node_path_1.default.resolve(cwd, presetPath);
|
|
293
|
+
if (fs_extra_1.default.existsSync(absolutePath)) {
|
|
294
|
+
return absolutePath;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
// Support npm packages
|
|
298
|
+
const resolvedPath = require.resolve(presetPath, {
|
|
299
|
+
paths: [cwd, __dirname],
|
|
300
|
+
});
|
|
301
|
+
return fs_extra_1.default.existsSync(resolvedPath) ? resolvedPath : null;
|
|
302
|
+
}
|
|
303
|
+
catch (_a) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function loadPreset(presetPath, cwd) {
|
|
308
|
+
const resolvedPresetPath = resolvePresetPath(presetPath, cwd);
|
|
309
|
+
if (!resolvedPresetPath) {
|
|
310
|
+
throw new Error(`Preset not found: "${presetPath}". Make sure the package is installed or the path is correct.`);
|
|
311
|
+
}
|
|
312
|
+
let presetConfig;
|
|
313
|
+
try {
|
|
314
|
+
const content = await fs_extra_1.default.readFile(resolvedPresetPath, "utf8");
|
|
315
|
+
presetConfig = JSON.parse(content);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
319
|
+
}
|
|
320
|
+
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
321
|
+
const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
|
|
322
|
+
// Check if preset has content or inherits from other presets
|
|
323
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
|
|
324
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
325
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
326
|
+
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
327
|
+
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
|
|
328
|
+
const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "agents"));
|
|
329
|
+
const hasNestedPresets = Array.isArray(presetConfig.presets) && presetConfig.presets.length > 0;
|
|
330
|
+
const hasAnyContent = hasRules ||
|
|
331
|
+
hasCommands ||
|
|
332
|
+
hasHooks ||
|
|
333
|
+
hasAssets ||
|
|
334
|
+
hasSkills ||
|
|
335
|
+
hasAgents ||
|
|
336
|
+
hasNestedPresets;
|
|
337
|
+
if (!hasAnyContent) {
|
|
338
|
+
throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, agents/, hooks.json, assets/, or presets`);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
config: presetConfig,
|
|
342
|
+
rootDir: presetRootDir,
|
|
343
|
+
resolvedPath: resolvedPresetPath,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Recursively load a preset and all its dependencies
|
|
348
|
+
* @param presetPath The original preset path (used for namespacing)
|
|
349
|
+
* @param cwd The current working directory for resolving paths
|
|
350
|
+
* @param visited Set of already visited preset paths (by resolved absolute path) for cycle detection
|
|
351
|
+
*/
|
|
352
|
+
async function loadPresetRecursively(presetPath, cwd, visited) {
|
|
353
|
+
const preset = await loadPreset(presetPath, cwd);
|
|
354
|
+
const presetRootDir = preset.rootDir;
|
|
355
|
+
const presetDir = node_path_1.default.dirname(preset.resolvedPath);
|
|
356
|
+
// Check for circular dependency
|
|
357
|
+
if (visited.has(preset.resolvedPath)) {
|
|
358
|
+
throw new Error(`Circular preset dependency detected: "${presetPath}" has already been loaded`);
|
|
359
|
+
}
|
|
360
|
+
visited.add(preset.resolvedPath);
|
|
361
|
+
const result = {
|
|
362
|
+
rules: [],
|
|
363
|
+
commands: [],
|
|
364
|
+
assets: [],
|
|
365
|
+
skills: [],
|
|
366
|
+
agents: [],
|
|
367
|
+
mcpServers: {},
|
|
368
|
+
hooksConfigs: [],
|
|
369
|
+
hookFiles: [],
|
|
370
|
+
};
|
|
371
|
+
// Load entities from this preset's rootDir
|
|
372
|
+
const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
|
|
373
|
+
if (fs_extra_1.default.existsSync(presetRulesPath)) {
|
|
374
|
+
const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
|
|
375
|
+
result.rules.push(...presetRules);
|
|
376
|
+
}
|
|
377
|
+
const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
|
|
378
|
+
if (fs_extra_1.default.existsSync(presetCommandsPath)) {
|
|
379
|
+
const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
|
|
380
|
+
result.commands.push(...presetCommands);
|
|
381
|
+
}
|
|
382
|
+
const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
|
|
383
|
+
if (fs_extra_1.default.existsSync(presetHooksFile)) {
|
|
384
|
+
const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
|
|
385
|
+
result.hooksConfigs.push(presetHooksConfig);
|
|
386
|
+
result.hookFiles.push(...presetHookFiles);
|
|
387
|
+
}
|
|
388
|
+
const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
|
|
389
|
+
if (fs_extra_1.default.existsSync(presetAssetsPath)) {
|
|
390
|
+
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
391
|
+
result.assets.push(...presetAssets);
|
|
392
|
+
}
|
|
393
|
+
const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
|
|
394
|
+
if (fs_extra_1.default.existsSync(presetSkillsPath)) {
|
|
395
|
+
const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
|
|
396
|
+
result.skills.push(...presetSkills);
|
|
397
|
+
}
|
|
398
|
+
const presetAgentsPath = node_path_1.default.join(presetRootDir, "agents");
|
|
399
|
+
if (fs_extra_1.default.existsSync(presetAgentsPath)) {
|
|
400
|
+
const presetAgents = await loadAgentsFromDirectory(presetAgentsPath, "preset", presetPath);
|
|
401
|
+
result.agents.push(...presetAgents);
|
|
402
|
+
}
|
|
403
|
+
// Add MCP servers from this preset
|
|
404
|
+
if (preset.config.mcpServers) {
|
|
405
|
+
result.mcpServers = { ...preset.config.mcpServers };
|
|
406
|
+
}
|
|
407
|
+
// Recursively load nested presets
|
|
408
|
+
if (preset.config.presets && preset.config.presets.length > 0) {
|
|
409
|
+
for (const nestedPresetPath of preset.config.presets) {
|
|
410
|
+
const nestedResult = await loadPresetRecursively(nestedPresetPath, presetDir, // Use preset's directory as cwd for relative paths
|
|
411
|
+
visited);
|
|
412
|
+
// Merge results from nested preset
|
|
413
|
+
result.rules.push(...nestedResult.rules);
|
|
414
|
+
result.commands.push(...nestedResult.commands);
|
|
415
|
+
result.assets.push(...nestedResult.assets);
|
|
416
|
+
result.skills.push(...nestedResult.skills);
|
|
417
|
+
result.agents.push(...nestedResult.agents);
|
|
418
|
+
result.hooksConfigs.push(...nestedResult.hooksConfigs);
|
|
419
|
+
result.hookFiles.push(...nestedResult.hookFiles);
|
|
420
|
+
// Merge MCP servers (current preset takes precedence over nested)
|
|
421
|
+
result.mcpServers = mergePresetMcpServers(result.mcpServers, nestedResult.mcpServers);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
async function loadAllRules(config, cwd) {
|
|
427
|
+
const allRules = [];
|
|
428
|
+
const allCommands = [];
|
|
429
|
+
const allAssets = [];
|
|
430
|
+
const allSkills = [];
|
|
431
|
+
const allAgents = [];
|
|
432
|
+
const allHookFiles = [];
|
|
433
|
+
const allHooksConfigs = [];
|
|
434
|
+
let mergedMcpServers = { ...config.mcpServers };
|
|
435
|
+
// Load local files from rootDir only if specified
|
|
436
|
+
if (config.rootDir) {
|
|
437
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
438
|
+
// Load rules from rules/ subdirectory
|
|
439
|
+
const rulesPath = node_path_1.default.join(rootPath, "rules");
|
|
440
|
+
if (fs_extra_1.default.existsSync(rulesPath)) {
|
|
441
|
+
const localRules = await loadRulesFromDirectory(rulesPath, "local");
|
|
442
|
+
allRules.push(...localRules);
|
|
443
|
+
}
|
|
444
|
+
// Load commands from commands/ subdirectory
|
|
445
|
+
const commandsPath = node_path_1.default.join(rootPath, "commands");
|
|
446
|
+
if (fs_extra_1.default.existsSync(commandsPath)) {
|
|
447
|
+
const localCommands = await loadCommandsFromDirectory(commandsPath, "local");
|
|
448
|
+
allCommands.push(...localCommands);
|
|
449
|
+
}
|
|
450
|
+
// Load hooks from hooks.json (sibling to hooks/ directory)
|
|
451
|
+
const hooksFilePath = node_path_1.default.join(rootPath, "hooks.json");
|
|
452
|
+
if (fs_extra_1.default.existsSync(hooksFilePath)) {
|
|
453
|
+
const { config: localHooksConfig, files: localHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(rootPath, "local");
|
|
454
|
+
allHooksConfigs.push(localHooksConfig);
|
|
455
|
+
allHookFiles.push(...localHookFiles);
|
|
456
|
+
}
|
|
457
|
+
// Load assets from assets/ subdirectory
|
|
458
|
+
const assetsPath = node_path_1.default.join(rootPath, "assets");
|
|
459
|
+
if (fs_extra_1.default.existsSync(assetsPath)) {
|
|
460
|
+
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
461
|
+
allAssets.push(...localAssets);
|
|
462
|
+
}
|
|
463
|
+
// Load skills from skills/ subdirectory
|
|
464
|
+
const skillsPath = node_path_1.default.join(rootPath, "skills");
|
|
465
|
+
if (fs_extra_1.default.existsSync(skillsPath)) {
|
|
466
|
+
const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
|
|
467
|
+
allSkills.push(...localSkills);
|
|
468
|
+
}
|
|
469
|
+
// Load agents from agents/ subdirectory
|
|
470
|
+
const agentsPath = node_path_1.default.join(rootPath, "agents");
|
|
471
|
+
if (fs_extra_1.default.existsSync(agentsPath)) {
|
|
472
|
+
const localAgents = await loadAgentsFromDirectory(agentsPath, "local");
|
|
473
|
+
allAgents.push(...localAgents);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Load presets recursively
|
|
477
|
+
if (config.presets && config.presets.length > 0) {
|
|
478
|
+
const visited = new Set();
|
|
479
|
+
for (const presetPath of config.presets) {
|
|
480
|
+
const presetResult = await loadPresetRecursively(presetPath, cwd, visited);
|
|
481
|
+
allRules.push(...presetResult.rules);
|
|
482
|
+
allCommands.push(...presetResult.commands);
|
|
483
|
+
allAssets.push(...presetResult.assets);
|
|
484
|
+
allSkills.push(...presetResult.skills);
|
|
485
|
+
allAgents.push(...presetResult.agents);
|
|
486
|
+
allHooksConfigs.push(...presetResult.hooksConfigs);
|
|
487
|
+
allHookFiles.push(...presetResult.hookFiles);
|
|
488
|
+
// Merge MCP servers (local config takes precedence)
|
|
489
|
+
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, presetResult.mcpServers);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Merge all hooks configurations
|
|
493
|
+
const mergedHooks = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
|
|
494
|
+
return {
|
|
495
|
+
rules: allRules,
|
|
496
|
+
commands: allCommands,
|
|
497
|
+
assets: allAssets,
|
|
498
|
+
skills: allSkills,
|
|
499
|
+
agents: allAgents,
|
|
500
|
+
mcpServers: mergedMcpServers,
|
|
501
|
+
hooks: mergedHooks,
|
|
502
|
+
hookFiles: allHookFiles,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Merge preset MCP servers with local config MCP servers
|
|
507
|
+
* Local config takes precedence over preset config
|
|
508
|
+
*/
|
|
509
|
+
function mergePresetMcpServers(configMcpServers, presetMcpServers) {
|
|
510
|
+
const newMcpServers = { ...configMcpServers };
|
|
511
|
+
for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) {
|
|
512
|
+
// Cancel if set to false in config
|
|
513
|
+
if (Object.prototype.hasOwnProperty.call(newMcpServers, serverName) &&
|
|
514
|
+
newMcpServers[serverName] === false) {
|
|
515
|
+
delete newMcpServers[serverName];
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
// Only add if not already defined in config (local config takes precedence)
|
|
519
|
+
if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
|
|
520
|
+
newMcpServers[serverName] = serverConfig;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return newMcpServers;
|
|
524
|
+
}
|
|
525
|
+
async function loadConfigFile(searchFrom) {
|
|
526
|
+
const explorer = (0, cosmiconfig_1.cosmiconfig)("aicm", {
|
|
527
|
+
searchPlaces: ["aicm.json", "package.json"],
|
|
528
|
+
});
|
|
529
|
+
try {
|
|
530
|
+
const result = await explorer.search(searchFrom);
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
539
|
+
* This is useful for commands that only need to know the workspace setting
|
|
540
|
+
*/
|
|
541
|
+
async function checkWorkspacesEnabled(cwd) {
|
|
542
|
+
const workingDir = cwd || process.cwd();
|
|
543
|
+
const configResult = await loadConfigFile(workingDir);
|
|
544
|
+
if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
|
|
545
|
+
return detectWorkspacesFromPackageJson(workingDir);
|
|
546
|
+
}
|
|
547
|
+
return resolveWorkspaces(configResult.config, configResult.filepath, workingDir);
|
|
548
|
+
}
|
|
549
|
+
async function loadConfig(cwd) {
|
|
550
|
+
const workingDir = cwd || process.cwd();
|
|
551
|
+
const configResult = await loadConfigFile(workingDir);
|
|
552
|
+
if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const config = configResult.config;
|
|
556
|
+
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
557
|
+
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
558
|
+
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
559
|
+
const { rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = await loadAllRules(configWithDefaults, workingDir);
|
|
560
|
+
return {
|
|
561
|
+
config: configWithDefaults,
|
|
562
|
+
rules,
|
|
563
|
+
commands,
|
|
564
|
+
assets,
|
|
565
|
+
skills,
|
|
566
|
+
agents,
|
|
567
|
+
mcpServers,
|
|
568
|
+
hooks,
|
|
569
|
+
hookFiles,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function saveConfig(config, cwd) {
|
|
573
|
+
const workingDir = cwd || process.cwd();
|
|
574
|
+
const configPath = node_path_1.default.join(workingDir, "aicm.json");
|
|
575
|
+
try {
|
|
576
|
+
fs_extra_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
catch (_a) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type HookType = "beforeShellExecution" | "beforeMCPExecution" | "afterShellExecution" | "afterMCPExecution" | "beforeReadFile" | "afterFileEdit" | "beforeSubmitPrompt" | "stop";
|
|
2
|
+
export interface HookCommand {
|
|
3
|
+
command: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HooksJson {
|
|
6
|
+
version: number;
|
|
7
|
+
hooks: {
|
|
8
|
+
[K in HookType]?: HookCommand[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface HookFile {
|
|
12
|
+
name: string;
|
|
13
|
+
basename: string;
|
|
14
|
+
content: Buffer;
|
|
15
|
+
sourcePath: string;
|
|
16
|
+
source: "local" | "preset";
|
|
17
|
+
presetName?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load hooks configuration from a root directory
|
|
21
|
+
* The directory should contain hooks.json (sibling to hooks/ directory)
|
|
22
|
+
* All files in the hooks/ subdirectory are copied during installation
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadHooksFromDirectory(rootDir: string, source: "local" | "preset", presetName?: string): Promise<{
|
|
25
|
+
config: HooksJson;
|
|
26
|
+
files: HookFile[];
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Merge multiple hooks configurations into one
|
|
30
|
+
* Later configurations override earlier ones for the same hook type
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeHooksConfigs(configs: HooksJson[]): HooksJson;
|
|
33
|
+
/**
|
|
34
|
+
* Rewrite command paths to point to the managed hooks directory (hooks/aicm/)
|
|
35
|
+
* At this point, paths are already namespaced filenames from loadHooksFromDirectory
|
|
36
|
+
*/
|
|
37
|
+
export declare function rewriteHooksConfigToManagedDir(hooksConfig: HooksJson): HooksJson;
|
|
38
|
+
/**
|
|
39
|
+
* Count the number of hook entries in a hooks configuration
|
|
40
|
+
*/
|
|
41
|
+
export declare function countHooks(hooksConfig: HooksJson): number;
|
|
42
|
+
/**
|
|
43
|
+
* Dedupe hook files by namespaced path, warn on content conflicts
|
|
44
|
+
* Presets are namespaced with directories, so same basename from different presets won't collide
|
|
45
|
+
*/
|
|
46
|
+
export declare function dedupeHookFiles(hookFiles: HookFile[]): HookFile[];
|
|
47
|
+
/**
|
|
48
|
+
* Write hooks configuration and files to Cursor target
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeHooksToCursor(hooksConfig: HooksJson, hookFiles: HookFile[], cwd: string): void;
|