aicm 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -65
- package/dist/api.d.ts +7 -0
- package/dist/api.js +10 -0
- package/dist/commands/clean.js +62 -3
- package/dist/commands/init.js +23 -3
- package/dist/commands/install-workspaces.js +45 -136
- package/dist/commands/install.d.ts +5 -2
- package/dist/commands/install.js +98 -53
- package/dist/utils/config.d.ts +22 -13
- package/dist/utils/config.js +140 -132
- package/dist/utils/hooks.d.ts +50 -0
- package/dist/utils/hooks.js +346 -0
- package/package.json +1 -1
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { CosmiconfigResult } from "cosmiconfig";
|
|
2
|
+
import { HooksJson, HookFile } from "./hooks";
|
|
2
3
|
export interface RawConfig {
|
|
3
|
-
|
|
4
|
-
commandsDir?: string;
|
|
4
|
+
rootDir?: string;
|
|
5
5
|
targets?: string[];
|
|
6
6
|
presets?: string[];
|
|
7
|
-
overrides?: Record<string, string | false>;
|
|
8
7
|
mcpServers?: MCPServers;
|
|
9
8
|
workspaces?: boolean;
|
|
10
9
|
skipInstall?: boolean;
|
|
11
10
|
}
|
|
12
11
|
export interface Config {
|
|
13
|
-
|
|
14
|
-
commandsDir?: string;
|
|
12
|
+
rootDir?: string;
|
|
15
13
|
targets: string[];
|
|
16
14
|
presets?: string[];
|
|
17
|
-
overrides?: Record<string, string | false>;
|
|
18
15
|
mcpServers?: MCPServers;
|
|
19
16
|
workspaces?: boolean;
|
|
20
17
|
skipInstall?: boolean;
|
|
@@ -58,30 +55,42 @@ export interface ResolvedConfig {
|
|
|
58
55
|
commands: CommandFile[];
|
|
59
56
|
assets: AssetFile[];
|
|
60
57
|
mcpServers: MCPServers;
|
|
58
|
+
hooks: HooksJson;
|
|
59
|
+
hookFiles: HookFile[];
|
|
61
60
|
}
|
|
62
|
-
export declare const ALLOWED_CONFIG_KEYS: readonly ["
|
|
61
|
+
export declare const ALLOWED_CONFIG_KEYS: readonly ["rootDir", "targets", "presets", "mcpServers", "workspaces", "skipInstall"];
|
|
63
62
|
export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex", "claude"];
|
|
64
63
|
export type SupportedTarget = (typeof SUPPORTED_TARGETS)[number];
|
|
65
64
|
export declare function detectWorkspacesFromPackageJson(cwd: string): boolean;
|
|
66
65
|
export declare function resolveWorkspaces(config: unknown, configFilePath: string, cwd: string): boolean;
|
|
67
66
|
export declare function applyDefaults(config: RawConfig, workspaces: boolean): Config;
|
|
68
67
|
export declare function validateConfig(config: unknown, configFilePath: string, cwd: string, isWorkspaceMode?: boolean): asserts config is Config;
|
|
69
|
-
export declare function loadRulesFromDirectory(
|
|
70
|
-
export declare function loadCommandsFromDirectory(
|
|
71
|
-
export declare function loadAssetsFromDirectory(
|
|
68
|
+
export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
|
|
69
|
+
export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
|
|
70
|
+
export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Extract namespace from preset path for directory structure
|
|
73
|
+
* Handles both npm packages and local paths consistently
|
|
74
|
+
*/
|
|
75
|
+
export declare function extractNamespaceFromPresetPath(presetPath: string): string[];
|
|
72
76
|
export declare function resolvePresetPath(presetPath: string, cwd: string): string | null;
|
|
73
77
|
export declare function loadPreset(presetPath: string, cwd: string): Promise<{
|
|
74
78
|
config: Config;
|
|
75
|
-
|
|
76
|
-
commandsDir?: string;
|
|
79
|
+
rootDir: string;
|
|
77
80
|
}>;
|
|
78
81
|
export declare function loadAllRules(config: Config, cwd: string): Promise<{
|
|
79
82
|
rules: RuleFile[];
|
|
80
83
|
commands: CommandFile[];
|
|
81
84
|
assets: AssetFile[];
|
|
82
85
|
mcpServers: MCPServers;
|
|
86
|
+
hooks: HooksJson;
|
|
87
|
+
hookFiles: HookFile[];
|
|
83
88
|
}>;
|
|
84
|
-
export declare function applyOverrides<T extends ManagedFile>(files: T[], overrides: Record<string, string | false>, cwd: string): T[];
|
|
85
89
|
export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
92
|
+
* This is useful for commands that only need to know the workspace setting
|
|
93
|
+
*/
|
|
94
|
+
export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
|
|
86
95
|
export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
|
|
87
96
|
export declare function saveConfig(config: Config, cwd?: string): boolean;
|
package/dist/utils/config.js
CHANGED
|
@@ -11,23 +11,23 @@ exports.validateConfig = validateConfig;
|
|
|
11
11
|
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
12
|
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
13
13
|
exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
|
|
14
|
+
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
14
15
|
exports.resolvePresetPath = resolvePresetPath;
|
|
15
16
|
exports.loadPreset = loadPreset;
|
|
16
17
|
exports.loadAllRules = loadAllRules;
|
|
17
|
-
exports.applyOverrides = applyOverrides;
|
|
18
18
|
exports.loadConfigFile = loadConfigFile;
|
|
19
|
+
exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
|
|
19
20
|
exports.loadConfig = loadConfig;
|
|
20
21
|
exports.saveConfig = saveConfig;
|
|
21
22
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
22
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
24
|
const cosmiconfig_1 = require("cosmiconfig");
|
|
24
25
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
26
|
+
const hooks_1 = require("./hooks");
|
|
25
27
|
exports.ALLOWED_CONFIG_KEYS = [
|
|
26
|
-
"
|
|
27
|
-
"commandsDir",
|
|
28
|
+
"rootDir",
|
|
28
29
|
"targets",
|
|
29
30
|
"presets",
|
|
30
|
-
"overrides",
|
|
31
31
|
"mcpServers",
|
|
32
32
|
"workspaces",
|
|
33
33
|
"skipInstall",
|
|
@@ -63,11 +63,9 @@ function resolveWorkspaces(config, configFilePath, cwd) {
|
|
|
63
63
|
}
|
|
64
64
|
function applyDefaults(config, workspaces) {
|
|
65
65
|
return {
|
|
66
|
-
|
|
67
|
-
commandsDir: config.commandsDir,
|
|
66
|
+
rootDir: config.rootDir,
|
|
68
67
|
targets: config.targets || ["cursor"],
|
|
69
68
|
presets: config.presets || [],
|
|
70
|
-
overrides: config.overrides || {},
|
|
71
69
|
mcpServers: config.mcpServers || {},
|
|
72
70
|
workspaces,
|
|
73
71
|
skipInstall: config.skipInstall || false,
|
|
@@ -81,36 +79,37 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
81
79
|
if (unknownKeys.length > 0) {
|
|
82
80
|
throw new Error(`Invalid configuration at ${configFilePath}: unknown keys: ${unknownKeys.join(", ")}`);
|
|
83
81
|
}
|
|
84
|
-
// Validate
|
|
85
|
-
const
|
|
86
|
-
const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
|
|
82
|
+
// Validate rootDir
|
|
83
|
+
const hasRootDir = "rootDir" in config && typeof config.rootDir === "string";
|
|
87
84
|
const hasPresets = "presets" in config &&
|
|
88
85
|
Array.isArray(config.presets) &&
|
|
89
86
|
config.presets.length > 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
// Validate rulesDir if provided
|
|
96
|
-
if (hasRulesDir) {
|
|
97
|
-
const rulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
|
|
98
|
-
if (!fs_extra_1.default.existsSync(rulesPath)) {
|
|
99
|
-
throw new Error(`Rules directory does not exist: ${rulesPath}`);
|
|
87
|
+
if (hasRootDir) {
|
|
88
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
89
|
+
if (!fs_extra_1.default.existsSync(rootPath)) {
|
|
90
|
+
throw new Error(`Root directory does not exist: ${rootPath}`);
|
|
100
91
|
}
|
|
101
|
-
if (!fs_extra_1.default.statSync(
|
|
102
|
-
throw new Error(`
|
|
92
|
+
if (!fs_extra_1.default.statSync(rootPath).isDirectory()) {
|
|
93
|
+
throw new Error(`Root path is not a directory: ${rootPath}`);
|
|
103
94
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!
|
|
111
|
-
|
|
95
|
+
// Check for at least one valid subdirectory or file
|
|
96
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
97
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
98
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
|
|
99
|
+
// In workspace mode, root config doesn't need these directories
|
|
100
|
+
// since packages will have their own configurations
|
|
101
|
+
if (!isWorkspaceMode &&
|
|
102
|
+
!hasRules &&
|
|
103
|
+
!hasCommands &&
|
|
104
|
+
!hasHooks &&
|
|
105
|
+
!hasPresets) {
|
|
106
|
+
throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
|
|
112
107
|
}
|
|
113
108
|
}
|
|
109
|
+
else if (!isWorkspaceMode && !hasPresets) {
|
|
110
|
+
// If no rootDir specified and not in workspace mode, must have presets
|
|
111
|
+
throw new Error(`At least one of rootDir or presets must be specified in config at ${configFilePath}`);
|
|
112
|
+
}
|
|
114
113
|
if ("targets" in config) {
|
|
115
114
|
if (!Array.isArray(config.targets)) {
|
|
116
115
|
throw new Error(`targets must be an array in config at ${configFilePath}`);
|
|
@@ -124,22 +123,21 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
|
-
// Validate override rule names will be checked after rule resolution
|
|
128
126
|
}
|
|
129
|
-
async function loadRulesFromDirectory(
|
|
127
|
+
async function loadRulesFromDirectory(directoryPath, source, presetName) {
|
|
130
128
|
const rules = [];
|
|
131
|
-
if (!fs_extra_1.default.existsSync(
|
|
129
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
132
130
|
return rules;
|
|
133
131
|
}
|
|
134
|
-
const pattern = node_path_1.default.join(
|
|
132
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.mdc").replace(/\\/g, "/");
|
|
135
133
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
136
134
|
onlyFiles: true,
|
|
137
135
|
absolute: true,
|
|
138
136
|
});
|
|
139
137
|
for (const filePath of filePaths) {
|
|
140
138
|
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
141
|
-
// Preserve directory structure by using relative path from
|
|
142
|
-
const relativePath = node_path_1.default.relative(
|
|
139
|
+
// Preserve directory structure by using relative path from source directory
|
|
140
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
143
141
|
const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/");
|
|
144
142
|
rules.push({
|
|
145
143
|
name: ruleName,
|
|
@@ -151,12 +149,12 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
|
|
|
151
149
|
}
|
|
152
150
|
return rules;
|
|
153
151
|
}
|
|
154
|
-
async function loadCommandsFromDirectory(
|
|
152
|
+
async function loadCommandsFromDirectory(directoryPath, source, presetName) {
|
|
155
153
|
const commands = [];
|
|
156
|
-
if (!fs_extra_1.default.existsSync(
|
|
154
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
157
155
|
return commands;
|
|
158
156
|
}
|
|
159
|
-
const pattern = node_path_1.default.join(
|
|
157
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
|
|
160
158
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
161
159
|
onlyFiles: true,
|
|
162
160
|
absolute: true,
|
|
@@ -164,7 +162,7 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
|
|
|
164
162
|
filePaths.sort();
|
|
165
163
|
for (const filePath of filePaths) {
|
|
166
164
|
const content = await fs_extra_1.default.readFile(filePath, "utf8");
|
|
167
|
-
const relativePath = node_path_1.default.relative(
|
|
165
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
168
166
|
const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
|
|
169
167
|
commands.push({
|
|
170
168
|
name: commandName,
|
|
@@ -176,13 +174,13 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
|
|
|
176
174
|
}
|
|
177
175
|
return commands;
|
|
178
176
|
}
|
|
179
|
-
async function loadAssetsFromDirectory(
|
|
177
|
+
async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
180
178
|
const assets = [];
|
|
181
|
-
if (!fs_extra_1.default.existsSync(
|
|
179
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
182
180
|
return assets;
|
|
183
181
|
}
|
|
184
182
|
// Find all files except .mdc files and hidden files
|
|
185
|
-
const pattern = node_path_1.default.join(
|
|
183
|
+
const pattern = node_path_1.default.join(directoryPath, "**/*").replace(/\\/g, "/");
|
|
186
184
|
const filePaths = await (0, fast_glob_1.default)(pattern, {
|
|
187
185
|
onlyFiles: true,
|
|
188
186
|
absolute: true,
|
|
@@ -190,8 +188,8 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
|
|
|
190
188
|
});
|
|
191
189
|
for (const filePath of filePaths) {
|
|
192
190
|
const content = await fs_extra_1.default.readFile(filePath);
|
|
193
|
-
// Preserve directory structure by using relative path from
|
|
194
|
-
const relativePath = node_path_1.default.relative(
|
|
191
|
+
// Preserve directory structure by using relative path from source directory
|
|
192
|
+
const relativePath = node_path_1.default.relative(directoryPath, filePath);
|
|
195
193
|
// Keep extension for assets
|
|
196
194
|
const assetName = relativePath.replace(/\\/g, "/");
|
|
197
195
|
assets.push({
|
|
@@ -204,6 +202,19 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
|
|
|
204
202
|
}
|
|
205
203
|
return assets;
|
|
206
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract namespace from preset path for directory structure
|
|
207
|
+
* Handles both npm packages and local paths consistently
|
|
208
|
+
*/
|
|
209
|
+
function extractNamespaceFromPresetPath(presetPath) {
|
|
210
|
+
// Special case: npm package names always use forward slashes, regardless of platform
|
|
211
|
+
if (presetPath.startsWith("@")) {
|
|
212
|
+
// For scoped packages like @scope/package/subdir, create nested directories
|
|
213
|
+
return presetPath.split("/");
|
|
214
|
+
}
|
|
215
|
+
const parts = presetPath.split(node_path_1.default.sep);
|
|
216
|
+
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
217
|
+
}
|
|
207
218
|
function resolvePresetPath(presetPath, cwd) {
|
|
208
219
|
// Support specifying aicm.json directory and load the config from it
|
|
209
220
|
if (!presetPath.endsWith(".json")) {
|
|
@@ -241,99 +252,103 @@ async function loadPreset(presetPath, cwd) {
|
|
|
241
252
|
throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
242
253
|
}
|
|
243
254
|
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!
|
|
251
|
-
throw new Error(`Preset "${presetPath}" must have
|
|
255
|
+
const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
|
|
256
|
+
// Check for at least one valid subdirectory
|
|
257
|
+
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
|
|
258
|
+
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
259
|
+
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
260
|
+
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
261
|
+
if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
|
|
262
|
+
throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, hooks.json, or assets/`);
|
|
252
263
|
}
|
|
253
264
|
return {
|
|
254
265
|
config: presetConfig,
|
|
255
|
-
|
|
256
|
-
commandsDir: presetCommandsDir,
|
|
266
|
+
rootDir: presetRootDir,
|
|
257
267
|
};
|
|
258
268
|
}
|
|
259
269
|
async function loadAllRules(config, cwd) {
|
|
260
270
|
const allRules = [];
|
|
261
271
|
const allCommands = [];
|
|
262
272
|
const allAssets = [];
|
|
273
|
+
const allHookFiles = [];
|
|
274
|
+
const allHooksConfigs = [];
|
|
263
275
|
let mergedMcpServers = { ...config.mcpServers };
|
|
264
|
-
// Load local
|
|
265
|
-
if (config.
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
+
// Load local files from rootDir only if specified
|
|
277
|
+
if (config.rootDir) {
|
|
278
|
+
const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
|
|
279
|
+
// Load rules from rules/ subdirectory
|
|
280
|
+
const rulesPath = node_path_1.default.join(rootPath, "rules");
|
|
281
|
+
if (fs_extra_1.default.existsSync(rulesPath)) {
|
|
282
|
+
const localRules = await loadRulesFromDirectory(rulesPath, "local");
|
|
283
|
+
allRules.push(...localRules);
|
|
284
|
+
}
|
|
285
|
+
// Load commands from commands/ subdirectory
|
|
286
|
+
const commandsPath = node_path_1.default.join(rootPath, "commands");
|
|
287
|
+
if (fs_extra_1.default.existsSync(commandsPath)) {
|
|
288
|
+
const localCommands = await loadCommandsFromDirectory(commandsPath, "local");
|
|
289
|
+
allCommands.push(...localCommands);
|
|
290
|
+
}
|
|
291
|
+
// Load hooks from hooks.json (sibling to hooks/ directory)
|
|
292
|
+
const hooksFilePath = node_path_1.default.join(rootPath, "hooks.json");
|
|
293
|
+
if (fs_extra_1.default.existsSync(hooksFilePath)) {
|
|
294
|
+
const { config: localHooksConfig, files: localHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(rootPath, "local");
|
|
295
|
+
allHooksConfigs.push(localHooksConfig);
|
|
296
|
+
allHookFiles.push(...localHookFiles);
|
|
297
|
+
}
|
|
298
|
+
// Load assets from assets/ subdirectory
|
|
299
|
+
const assetsPath = node_path_1.default.join(rootPath, "assets");
|
|
300
|
+
if (fs_extra_1.default.existsSync(assetsPath)) {
|
|
301
|
+
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
302
|
+
allAssets.push(...localAssets);
|
|
303
|
+
}
|
|
276
304
|
}
|
|
305
|
+
// Load presets
|
|
277
306
|
if (config.presets) {
|
|
278
307
|
for (const presetPath of config.presets) {
|
|
279
308
|
const preset = await loadPreset(presetPath, cwd);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
309
|
+
const presetRootDir = preset.rootDir;
|
|
310
|
+
// Load preset rules from rules/ subdirectory
|
|
311
|
+
const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
|
|
312
|
+
if (fs_extra_1.default.existsSync(presetRulesPath)) {
|
|
313
|
+
const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
|
|
283
314
|
allRules.push(...presetRules);
|
|
284
|
-
allAssets.push(...presetAssets);
|
|
285
315
|
}
|
|
286
|
-
|
|
287
|
-
|
|
316
|
+
// Load preset commands from commands/ subdirectory
|
|
317
|
+
const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
|
|
318
|
+
if (fs_extra_1.default.existsSync(presetCommandsPath)) {
|
|
319
|
+
const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
|
|
288
320
|
allCommands.push(...presetCommands);
|
|
289
321
|
}
|
|
322
|
+
// Load preset hooks from hooks.json (sibling to hooks/ directory)
|
|
323
|
+
const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
|
|
324
|
+
if (fs_extra_1.default.existsSync(presetHooksFile)) {
|
|
325
|
+
const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
|
|
326
|
+
allHooksConfigs.push(presetHooksConfig);
|
|
327
|
+
allHookFiles.push(...presetHookFiles);
|
|
328
|
+
}
|
|
329
|
+
// Load preset assets from assets/ subdirectory
|
|
330
|
+
const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
|
|
331
|
+
if (fs_extra_1.default.existsSync(presetAssetsPath)) {
|
|
332
|
+
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
333
|
+
allAssets.push(...presetAssets);
|
|
334
|
+
}
|
|
290
335
|
// Merge MCP servers from preset
|
|
291
336
|
if (preset.config.mcpServers) {
|
|
292
337
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
293
338
|
}
|
|
294
339
|
}
|
|
295
340
|
}
|
|
341
|
+
// Merge all hooks configurations
|
|
342
|
+
const mergedHooks = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
|
|
296
343
|
return {
|
|
297
344
|
rules: allRules,
|
|
298
345
|
commands: allCommands,
|
|
299
346
|
assets: allAssets,
|
|
300
347
|
mcpServers: mergedMcpServers,
|
|
348
|
+
hooks: mergedHooks,
|
|
349
|
+
hookFiles: allHookFiles,
|
|
301
350
|
};
|
|
302
351
|
}
|
|
303
|
-
function applyOverrides(files, overrides, cwd) {
|
|
304
|
-
// Validate that all override names exist in the resolved files
|
|
305
|
-
for (const name of Object.keys(overrides)) {
|
|
306
|
-
if (!files.some((file) => file.name === name)) {
|
|
307
|
-
throw new Error(`Override entry "${name}" does not exist in resolved files`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
const fileMap = new Map();
|
|
311
|
-
for (const file of files) {
|
|
312
|
-
fileMap.set(file.name, file);
|
|
313
|
-
}
|
|
314
|
-
for (const [name, override] of Object.entries(overrides)) {
|
|
315
|
-
if (override === false) {
|
|
316
|
-
fileMap.delete(name);
|
|
317
|
-
}
|
|
318
|
-
else if (typeof override === "string") {
|
|
319
|
-
const overridePath = node_path_1.default.resolve(cwd, override);
|
|
320
|
-
if (!fs_extra_1.default.existsSync(overridePath)) {
|
|
321
|
-
throw new Error(`Override file not found: ${override} in ${cwd}`);
|
|
322
|
-
}
|
|
323
|
-
const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
|
|
324
|
-
const existing = fileMap.get(name);
|
|
325
|
-
fileMap.set(name, {
|
|
326
|
-
...(existing !== null && existing !== void 0 ? existing : {}),
|
|
327
|
-
name,
|
|
328
|
-
content,
|
|
329
|
-
sourcePath: overridePath,
|
|
330
|
-
source: "local",
|
|
331
|
-
presetName: undefined,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return Array.from(fileMap.values());
|
|
336
|
-
}
|
|
337
352
|
/**
|
|
338
353
|
* Merge preset MCP servers with local config MCP servers
|
|
339
354
|
* Local config takes precedence over preset config
|
|
@@ -347,7 +362,7 @@ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
|
|
|
347
362
|
delete newMcpServers[serverName];
|
|
348
363
|
continue;
|
|
349
364
|
}
|
|
350
|
-
// Only add if not already defined in config (
|
|
365
|
+
// Only add if not already defined in config (local config takes precedence)
|
|
351
366
|
if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
|
|
352
367
|
newMcpServers[serverName] = serverConfig;
|
|
353
368
|
}
|
|
@@ -366,6 +381,18 @@ async function loadConfigFile(searchFrom) {
|
|
|
366
381
|
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
367
382
|
}
|
|
368
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Check if workspaces mode is enabled without loading all rules/presets
|
|
386
|
+
* This is useful for commands that only need to know the workspace setting
|
|
387
|
+
*/
|
|
388
|
+
async function checkWorkspacesEnabled(cwd) {
|
|
389
|
+
const workingDir = cwd || process.cwd();
|
|
390
|
+
const configResult = await loadConfigFile(workingDir);
|
|
391
|
+
if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
|
|
392
|
+
return detectWorkspacesFromPackageJson(workingDir);
|
|
393
|
+
}
|
|
394
|
+
return resolveWorkspaces(configResult.config, configResult.filepath, workingDir);
|
|
395
|
+
}
|
|
369
396
|
async function loadConfig(cwd) {
|
|
370
397
|
const workingDir = cwd || process.cwd();
|
|
371
398
|
const configResult = await loadConfigFile(workingDir);
|
|
@@ -376,34 +403,15 @@ async function loadConfig(cwd) {
|
|
|
376
403
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
377
404
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
378
405
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
379
|
-
const { rules, commands, assets, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
|
|
380
|
-
let rulesWithOverrides = rules;
|
|
381
|
-
let commandsWithOverrides = commands;
|
|
382
|
-
// Note: Assets are not currently supported in overrides as they are binary/varied files
|
|
383
|
-
if (configWithDefaults.overrides) {
|
|
384
|
-
const overrides = configWithDefaults.overrides;
|
|
385
|
-
const ruleNames = new Set(rules.map((rule) => rule.name));
|
|
386
|
-
const commandNames = new Set(commands.map((command) => command.name));
|
|
387
|
-
for (const overrideName of Object.keys(overrides)) {
|
|
388
|
-
if (!ruleNames.has(overrideName) && !commandNames.has(overrideName)) {
|
|
389
|
-
throw new Error(`Override entry "${overrideName}" does not exist in resolved rules or commands`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const ruleOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => ruleNames.has(name)));
|
|
393
|
-
const commandOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => commandNames.has(name)));
|
|
394
|
-
if (Object.keys(ruleOverrides).length > 0) {
|
|
395
|
-
rulesWithOverrides = applyOverrides(rules, ruleOverrides, workingDir);
|
|
396
|
-
}
|
|
397
|
-
if (Object.keys(commandOverrides).length > 0) {
|
|
398
|
-
commandsWithOverrides = applyOverrides(commands, commandOverrides, workingDir);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
406
|
+
const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
401
407
|
return {
|
|
402
408
|
config: configWithDefaults,
|
|
403
|
-
rules
|
|
404
|
-
commands
|
|
409
|
+
rules,
|
|
410
|
+
commands,
|
|
405
411
|
assets,
|
|
406
412
|
mcpServers,
|
|
413
|
+
hooks,
|
|
414
|
+
hookFiles,
|
|
407
415
|
};
|
|
408
416
|
}
|
|
409
417
|
function saveConfig(config, cwd) {
|
|
@@ -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;
|