aicm 0.20.1 → 0.20.5
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/package.json +6 -2
- package/dist/api.d.ts +0 -16
- package/dist/api.js +0 -22
- package/dist/bin/aicm.d.ts +0 -2
- package/dist/bin/aicm.js +0 -5
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -102
- package/dist/commands/clean.d.ts +0 -19
- package/dist/commands/clean.js +0 -385
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +0 -49
- package/dist/commands/install-workspaces.d.ts +0 -5
- package/dist/commands/install-workspaces.js +0 -421
- package/dist/commands/install.d.ts +0 -110
- package/dist/commands/install.js +0 -736
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +0 -40
- package/dist/utils/config.d.ts +0 -117
- package/dist/utils/config.js +0 -529
- package/dist/utils/hooks.d.ts +0 -50
- package/dist/utils/hooks.js +0 -346
- package/dist/utils/is-ci.d.ts +0 -1
- package/dist/utils/is-ci.js +0 -8
- package/dist/utils/rules-file-writer.d.ts +0 -24
- package/dist/utils/rules-file-writer.js +0 -197
- package/dist/utils/safe-path.d.ts +0 -10
- package/dist/utils/safe-path.js +0 -28
- package/dist/utils/working-directory.d.ts +0 -5
- package/dist/utils/working-directory.js +0 -21
- package/dist/utils/workspace-discovery.d.ts +0 -13
- package/dist/utils/workspace-discovery.js +0 -53
package/dist/utils/hooks.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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;
|
package/dist/utils/hooks.js
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
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.loadHooksFromDirectory = loadHooksFromDirectory;
|
|
7
|
-
exports.mergeHooksConfigs = mergeHooksConfigs;
|
|
8
|
-
exports.rewriteHooksConfigToManagedDir = rewriteHooksConfigToManagedDir;
|
|
9
|
-
exports.countHooks = countHooks;
|
|
10
|
-
exports.dedupeHookFiles = dedupeHookFiles;
|
|
11
|
-
exports.writeHooksToCursor = writeHooksToCursor;
|
|
12
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
14
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
-
const config_1 = require("./config");
|
|
16
|
-
/**
|
|
17
|
-
* Validate that a command path points to a file within the hooks directory
|
|
18
|
-
* Commands should be relative paths starting with ./hooks/
|
|
19
|
-
*/
|
|
20
|
-
function validateHookPath(commandPath, rootDir, hooksDir) {
|
|
21
|
-
if (!commandPath.startsWith("./") && !commandPath.startsWith("../")) {
|
|
22
|
-
return { valid: false };
|
|
23
|
-
}
|
|
24
|
-
// Resolve path relative to rootDir (where hooks.json is located)
|
|
25
|
-
const resolvedPath = node_path_1.default.resolve(rootDir, commandPath);
|
|
26
|
-
const relativePath = node_path_1.default.relative(hooksDir, resolvedPath);
|
|
27
|
-
// Check if the file is within hooks directory (not using .. to escape)
|
|
28
|
-
if (relativePath.startsWith("..") || node_path_1.default.isAbsolute(relativePath)) {
|
|
29
|
-
return { valid: false };
|
|
30
|
-
}
|
|
31
|
-
return { valid: true, relativePath };
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Load all files from the hooks directory
|
|
35
|
-
*/
|
|
36
|
-
async function loadAllFilesFromDirectory(dir, baseDir = dir) {
|
|
37
|
-
const files = [];
|
|
38
|
-
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
// Recursively load files from subdirectories
|
|
43
|
-
const subFiles = await loadAllFilesFromDirectory(fullPath, baseDir);
|
|
44
|
-
files.push(...subFiles);
|
|
45
|
-
}
|
|
46
|
-
else if (entry.isFile() && entry.name !== "hooks.json") {
|
|
47
|
-
// Skip hooks.json, collect all other files
|
|
48
|
-
const relativePath = node_path_1.default.relative(baseDir, fullPath);
|
|
49
|
-
files.push({ relativePath, absolutePath: fullPath });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return files;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Load hooks configuration from a root directory
|
|
56
|
-
* The directory should contain hooks.json (sibling to hooks/ directory)
|
|
57
|
-
* All files in the hooks/ subdirectory are copied during installation
|
|
58
|
-
*/
|
|
59
|
-
async function loadHooksFromDirectory(rootDir, source, presetName) {
|
|
60
|
-
const hooksFilePath = node_path_1.default.join(rootDir, "hooks.json");
|
|
61
|
-
const hooksDir = node_path_1.default.join(rootDir, "hooks");
|
|
62
|
-
if (!fs_extra_1.default.existsSync(hooksFilePath)) {
|
|
63
|
-
return {
|
|
64
|
-
config: { version: 1, hooks: {} },
|
|
65
|
-
files: [],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const content = await fs_extra_1.default.readFile(hooksFilePath, "utf8");
|
|
69
|
-
const hooksConfig = JSON.parse(content);
|
|
70
|
-
// Load all files from hooks/ subdirectory
|
|
71
|
-
const hookFiles = [];
|
|
72
|
-
if (fs_extra_1.default.existsSync(hooksDir)) {
|
|
73
|
-
const allFiles = await loadAllFilesFromDirectory(hooksDir);
|
|
74
|
-
// Create a map of all files for validation
|
|
75
|
-
const filePathMap = new Map();
|
|
76
|
-
for (const file of allFiles) {
|
|
77
|
-
filePathMap.set(file.relativePath, file.absolutePath);
|
|
78
|
-
}
|
|
79
|
-
// Validate that all referenced commands point to files within hooks directory
|
|
80
|
-
if (hooksConfig.hooks) {
|
|
81
|
-
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
82
|
-
const hookCommands = hooksConfig.hooks[hookType];
|
|
83
|
-
if (hookCommands && Array.isArray(hookCommands)) {
|
|
84
|
-
for (const hookCommand of hookCommands) {
|
|
85
|
-
const commandPath = hookCommand.command;
|
|
86
|
-
if (commandPath && typeof commandPath === "string") {
|
|
87
|
-
const validation = validateHookPath(commandPath, rootDir, hooksDir);
|
|
88
|
-
if (!validation.valid || !validation.relativePath) {
|
|
89
|
-
console.warn(`Warning: Hook command "${commandPath}" in hooks.json must reference a file within the hooks/ directory. Skipping.`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Copy all files from the hooks/ directory
|
|
97
|
-
for (const file of allFiles) {
|
|
98
|
-
const fileContent = await fs_extra_1.default.readFile(file.absolutePath);
|
|
99
|
-
const basename = node_path_1.default.basename(file.absolutePath);
|
|
100
|
-
// Namespace preset files
|
|
101
|
-
let namespacedPath;
|
|
102
|
-
if (source === "preset" && presetName) {
|
|
103
|
-
const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
|
|
104
|
-
// Use posix paths for consistent cross-platform behavior
|
|
105
|
-
namespacedPath = node_path_1.default.posix.join(...namespace, file.relativePath.split(node_path_1.default.sep).join(node_path_1.default.posix.sep));
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
// For local files, use the relative path as-is
|
|
109
|
-
namespacedPath = file.relativePath.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
|
|
110
|
-
}
|
|
111
|
-
hookFiles.push({
|
|
112
|
-
name: namespacedPath,
|
|
113
|
-
basename,
|
|
114
|
-
content: fileContent,
|
|
115
|
-
sourcePath: file.absolutePath,
|
|
116
|
-
source,
|
|
117
|
-
presetName,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// Rewrite the config to use namespaced file names
|
|
122
|
-
const rewrittenConfig = rewriteHooksConfigForNamespace(hooksConfig, hookFiles, rootDir);
|
|
123
|
-
return { config: rewrittenConfig, files: hookFiles };
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Rewrite hooks config to use the namespaced names from the hook files
|
|
127
|
-
*/
|
|
128
|
-
function rewriteHooksConfigForNamespace(hooksConfig, hookFiles, rootDir) {
|
|
129
|
-
// Create a map from sourcePath to the hookFile
|
|
130
|
-
const sourcePathToFile = new Map();
|
|
131
|
-
for (const hookFile of hookFiles) {
|
|
132
|
-
sourcePathToFile.set(hookFile.sourcePath, hookFile);
|
|
133
|
-
}
|
|
134
|
-
const rewritten = {
|
|
135
|
-
version: hooksConfig.version,
|
|
136
|
-
hooks: {},
|
|
137
|
-
};
|
|
138
|
-
if (hooksConfig.hooks) {
|
|
139
|
-
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
140
|
-
const hookCommands = hooksConfig.hooks[hookType];
|
|
141
|
-
if (hookCommands && Array.isArray(hookCommands)) {
|
|
142
|
-
rewritten.hooks[hookType] = hookCommands
|
|
143
|
-
.map((hookCommand) => {
|
|
144
|
-
const commandPath = hookCommand.command;
|
|
145
|
-
if (commandPath &&
|
|
146
|
-
typeof commandPath === "string" &&
|
|
147
|
-
(commandPath.startsWith("./") || commandPath.startsWith("../"))) {
|
|
148
|
-
// Resolve path relative to rootDir (where hooks.json is)
|
|
149
|
-
const resolvedPath = node_path_1.default.resolve(rootDir, commandPath);
|
|
150
|
-
const hookFile = sourcePathToFile.get(resolvedPath);
|
|
151
|
-
if (hookFile) {
|
|
152
|
-
// Use the namespaced name
|
|
153
|
-
return { command: hookFile.name };
|
|
154
|
-
}
|
|
155
|
-
// File was invalid or not found, filter it out
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
return hookCommand;
|
|
159
|
-
})
|
|
160
|
-
.filter((cmd) => cmd !== null);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return rewritten;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Merge multiple hooks configurations into one
|
|
168
|
-
* Later configurations override earlier ones for the same hook type
|
|
169
|
-
*/
|
|
170
|
-
function mergeHooksConfigs(configs) {
|
|
171
|
-
const merged = {
|
|
172
|
-
version: 1,
|
|
173
|
-
hooks: {},
|
|
174
|
-
};
|
|
175
|
-
for (const config of configs) {
|
|
176
|
-
// Use the latest version
|
|
177
|
-
if (config.version) {
|
|
178
|
-
merged.version = config.version;
|
|
179
|
-
}
|
|
180
|
-
// Merge hooks - concatenate arrays for each hook type
|
|
181
|
-
if (config.hooks) {
|
|
182
|
-
for (const hookType of Object.keys(config.hooks)) {
|
|
183
|
-
const hookCommands = config.hooks[hookType];
|
|
184
|
-
if (hookCommands && Array.isArray(hookCommands)) {
|
|
185
|
-
if (!merged.hooks[hookType]) {
|
|
186
|
-
merged.hooks[hookType] = [];
|
|
187
|
-
}
|
|
188
|
-
// Concatenate commands (later configs add to the list)
|
|
189
|
-
merged.hooks[hookType] = [
|
|
190
|
-
...(merged.hooks[hookType] || []),
|
|
191
|
-
...hookCommands,
|
|
192
|
-
];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return merged;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Rewrite command paths to point to the managed hooks directory (hooks/aicm/)
|
|
201
|
-
* At this point, paths are already namespaced filenames from loadHooksFromDirectory
|
|
202
|
-
*/
|
|
203
|
-
function rewriteHooksConfigToManagedDir(hooksConfig) {
|
|
204
|
-
const rewritten = {
|
|
205
|
-
version: hooksConfig.version,
|
|
206
|
-
hooks: {},
|
|
207
|
-
};
|
|
208
|
-
if (hooksConfig.hooks) {
|
|
209
|
-
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
210
|
-
const hookCommands = hooksConfig.hooks[hookType];
|
|
211
|
-
if (hookCommands && Array.isArray(hookCommands)) {
|
|
212
|
-
rewritten.hooks[hookType] = hookCommands.map((hookCommand) => {
|
|
213
|
-
const commandPath = hookCommand.command;
|
|
214
|
-
if (commandPath && typeof commandPath === "string") {
|
|
215
|
-
return { command: `./hooks/aicm/${commandPath}` };
|
|
216
|
-
}
|
|
217
|
-
return hookCommand;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return rewritten;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Count the number of hook entries in a hooks configuration
|
|
226
|
-
*/
|
|
227
|
-
function countHooks(hooksConfig) {
|
|
228
|
-
let count = 0;
|
|
229
|
-
if (hooksConfig.hooks) {
|
|
230
|
-
for (const hookType of Object.keys(hooksConfig.hooks)) {
|
|
231
|
-
const hookCommands = hooksConfig.hooks[hookType];
|
|
232
|
-
if (hookCommands && Array.isArray(hookCommands)) {
|
|
233
|
-
count += hookCommands.length;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return count;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Dedupe hook files by namespaced path, warn on content conflicts
|
|
241
|
-
* Presets are namespaced with directories, so same basename from different presets won't collide
|
|
242
|
-
*/
|
|
243
|
-
function dedupeHookFiles(hookFiles) {
|
|
244
|
-
const fileMap = new Map();
|
|
245
|
-
for (const hookFile of hookFiles) {
|
|
246
|
-
const namespacedPath = hookFile.name;
|
|
247
|
-
if (fileMap.has(namespacedPath)) {
|
|
248
|
-
const existing = fileMap.get(namespacedPath);
|
|
249
|
-
const existingHash = node_crypto_1.default
|
|
250
|
-
.createHash("md5")
|
|
251
|
-
.update(existing.content)
|
|
252
|
-
.digest("hex");
|
|
253
|
-
const currentHash = node_crypto_1.default
|
|
254
|
-
.createHash("md5")
|
|
255
|
-
.update(hookFile.content)
|
|
256
|
-
.digest("hex");
|
|
257
|
-
if (existingHash !== currentHash) {
|
|
258
|
-
const sourceInfo = hookFile.presetName
|
|
259
|
-
? `preset "${hookFile.presetName}"`
|
|
260
|
-
: hookFile.source;
|
|
261
|
-
const existingSourceInfo = existing.presetName
|
|
262
|
-
? `preset "${existing.presetName}"`
|
|
263
|
-
: existing.source;
|
|
264
|
-
console.warn(`Warning: Hook file "${namespacedPath}" has different content from ${existingSourceInfo} and ${sourceInfo}. Using last occurrence.`);
|
|
265
|
-
}
|
|
266
|
-
// Last writer wins
|
|
267
|
-
fileMap.set(namespacedPath, hookFile);
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
fileMap.set(namespacedPath, hookFile);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return Array.from(fileMap.values());
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Write hooks configuration and files to Cursor target
|
|
277
|
-
*/
|
|
278
|
-
function writeHooksToCursor(hooksConfig, hookFiles, cwd) {
|
|
279
|
-
const cursorRoot = node_path_1.default.join(cwd, ".cursor");
|
|
280
|
-
const hooksJsonPath = node_path_1.default.join(cursorRoot, "hooks.json");
|
|
281
|
-
const hooksDir = node_path_1.default.join(cursorRoot, "hooks", "aicm");
|
|
282
|
-
// Dedupe hook files
|
|
283
|
-
const dedupedHookFiles = dedupeHookFiles(hookFiles);
|
|
284
|
-
// Create hooks directory and clean it
|
|
285
|
-
fs_extra_1.default.emptyDirSync(hooksDir);
|
|
286
|
-
// Copy hook files to managed directory
|
|
287
|
-
for (const hookFile of dedupedHookFiles) {
|
|
288
|
-
const targetPath = node_path_1.default.join(hooksDir, hookFile.name);
|
|
289
|
-
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(targetPath));
|
|
290
|
-
fs_extra_1.default.writeFileSync(targetPath, hookFile.content);
|
|
291
|
-
}
|
|
292
|
-
// Rewrite paths to point to managed directory
|
|
293
|
-
const finalConfig = rewriteHooksConfigToManagedDir(hooksConfig);
|
|
294
|
-
// Read existing hooks.json and preserve user hooks
|
|
295
|
-
let existingConfig = null;
|
|
296
|
-
if (fs_extra_1.default.existsSync(hooksJsonPath)) {
|
|
297
|
-
try {
|
|
298
|
-
existingConfig = fs_extra_1.default.readJsonSync(hooksJsonPath);
|
|
299
|
-
}
|
|
300
|
-
catch (_a) {
|
|
301
|
-
existingConfig = null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Extract user hooks (non-aicm managed)
|
|
305
|
-
const userHooks = { version: 1, hooks: {} };
|
|
306
|
-
if (existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.hooks) {
|
|
307
|
-
for (const hookType of Object.keys(existingConfig.hooks)) {
|
|
308
|
-
const commands = existingConfig.hooks[hookType];
|
|
309
|
-
if (commands && Array.isArray(commands)) {
|
|
310
|
-
const userCommands = commands.filter((cmd) => { var _a; return !((_a = cmd.command) === null || _a === void 0 ? void 0 : _a.includes("hooks/aicm/")); });
|
|
311
|
-
if (userCommands.length > 0) {
|
|
312
|
-
userHooks.hooks[hookType] = userCommands;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Merge user hooks with aicm hooks
|
|
318
|
-
const mergedConfig = {
|
|
319
|
-
version: finalConfig.version,
|
|
320
|
-
hooks: {},
|
|
321
|
-
};
|
|
322
|
-
// Add user hooks first
|
|
323
|
-
if (userHooks.hooks) {
|
|
324
|
-
for (const hookType of Object.keys(userHooks.hooks)) {
|
|
325
|
-
const commands = userHooks.hooks[hookType];
|
|
326
|
-
if (commands) {
|
|
327
|
-
mergedConfig.hooks[hookType] = [...commands];
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
// Then add aicm hooks
|
|
332
|
-
if (finalConfig.hooks) {
|
|
333
|
-
for (const hookType of Object.keys(finalConfig.hooks)) {
|
|
334
|
-
const commands = finalConfig.hooks[hookType];
|
|
335
|
-
if (commands) {
|
|
336
|
-
if (!mergedConfig.hooks[hookType]) {
|
|
337
|
-
mergedConfig.hooks[hookType] = [];
|
|
338
|
-
}
|
|
339
|
-
mergedConfig.hooks[hookType].push(...commands);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// Write hooks.json
|
|
344
|
-
fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(hooksJsonPath));
|
|
345
|
-
fs_extra_1.default.writeJsonSync(hooksJsonPath, mergedConfig, { spaces: 2 });
|
|
346
|
-
}
|
package/dist/utils/is-ci.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const isCIEnvironment: () => boolean;
|
package/dist/utils/is-ci.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isCIEnvironment = void 0;
|
|
4
|
-
const node_process_1 = require("node:process");
|
|
5
|
-
const isCIEnvironment = () => (node_process_1.env.CI !== "0" && node_process_1.env.CI !== "false" && "CI" in node_process_1.env) ||
|
|
6
|
-
"CONTINUOUS_INTEGRATION" in node_process_1.env ||
|
|
7
|
-
Object.keys(node_process_1.env).some((key) => key.startsWith("CI_"));
|
|
8
|
-
exports.isCIEnvironment = isCIEnvironment;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export type RuleMetadata = Record<string, string | boolean | string[]>;
|
|
2
|
-
/**
|
|
3
|
-
* Parse YAML frontmatter blocks from a rule file and return a flat metadata object
|
|
4
|
-
*/
|
|
5
|
-
export declare function parseRuleFrontmatter(content: string): RuleMetadata;
|
|
6
|
-
/**
|
|
7
|
-
* Remove the rules block from the content
|
|
8
|
-
*/
|
|
9
|
-
export declare function removeRulesBlock(content: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Write rules to the .windsurfrules file
|
|
12
|
-
* This will update the content between the RULES_BEGIN and RULES_END markers
|
|
13
|
-
* If the file doesn't exist, it will create it
|
|
14
|
-
* If the markers don't exist, it will append them to the existing content
|
|
15
|
-
*/
|
|
16
|
-
export declare function writeRulesFile(rulesContent: string, rulesFilePath?: string): void;
|
|
17
|
-
/**
|
|
18
|
-
* Generate the rules file content based on rule files
|
|
19
|
-
*/
|
|
20
|
-
export declare function generateRulesFileContent(ruleFiles: {
|
|
21
|
-
name: string;
|
|
22
|
-
path: string;
|
|
23
|
-
metadata: Record<string, string | boolean | string[]>;
|
|
24
|
-
}[]): string;
|
|
@@ -1,197 +0,0 @@
|
|
|
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.parseRuleFrontmatter = parseRuleFrontmatter;
|
|
7
|
-
exports.removeRulesBlock = removeRulesBlock;
|
|
8
|
-
exports.writeRulesFile = writeRulesFile;
|
|
9
|
-
exports.generateRulesFileContent = generateRulesFileContent;
|
|
10
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
/**
|
|
13
|
-
* Parse YAML frontmatter blocks from a rule file and return a flat metadata object
|
|
14
|
-
*/
|
|
15
|
-
function parseRuleFrontmatter(content) {
|
|
16
|
-
const metadata = {};
|
|
17
|
-
// Support both LF and CRLF line endings
|
|
18
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/gm;
|
|
19
|
-
let match;
|
|
20
|
-
while ((match = frontmatterRegex.exec(content)) !== null) {
|
|
21
|
-
const lines = match[1].split("\n");
|
|
22
|
-
for (const line of lines) {
|
|
23
|
-
const trimmed = line.trim();
|
|
24
|
-
if (!trimmed)
|
|
25
|
-
continue;
|
|
26
|
-
const [key, ...rest] = trimmed.split(":");
|
|
27
|
-
if (!key)
|
|
28
|
-
continue;
|
|
29
|
-
const raw = rest.join(":").trim();
|
|
30
|
-
if (raw === "") {
|
|
31
|
-
metadata[key] = "";
|
|
32
|
-
}
|
|
33
|
-
else if (raw === "true" || raw === "false") {
|
|
34
|
-
metadata[key] = raw === "true";
|
|
35
|
-
}
|
|
36
|
-
else if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
37
|
-
try {
|
|
38
|
-
const parsed = JSON.parse(raw.replace(/'/g, '"'));
|
|
39
|
-
metadata[key] = parsed;
|
|
40
|
-
}
|
|
41
|
-
catch (_a) {
|
|
42
|
-
metadata[key] = raw;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else if ((raw.startsWith('"') && raw.endsWith('"')) ||
|
|
46
|
-
(raw.startsWith("'") && raw.endsWith("'"))) {
|
|
47
|
-
metadata[key] = raw.slice(1, -1);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
metadata[key] = raw;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return metadata;
|
|
55
|
-
}
|
|
56
|
-
const RULES_BEGIN = "<!-- AICM:BEGIN -->";
|
|
57
|
-
const RULES_END = "<!-- AICM:END -->";
|
|
58
|
-
const WARNING = "<!-- WARNING: Everything between these markers will be overwritten during installation -->";
|
|
59
|
-
/**
|
|
60
|
-
* Remove the rules block from the content
|
|
61
|
-
*/
|
|
62
|
-
function removeRulesBlock(content) {
|
|
63
|
-
// Check if our markers exist
|
|
64
|
-
if (content.includes(RULES_BEGIN) && content.includes(RULES_END)) {
|
|
65
|
-
const parts = content.split(RULES_BEGIN);
|
|
66
|
-
const beforeMarker = parts[0];
|
|
67
|
-
const afterParts = parts[1].split(RULES_END);
|
|
68
|
-
const afterMarker = afterParts.slice(1).join(RULES_END); // In case RULES_END appears multiple times (unlikely but safe)
|
|
69
|
-
return (beforeMarker + afterMarker).trim();
|
|
70
|
-
}
|
|
71
|
-
return content;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Create a formatted block of content with rules markers
|
|
75
|
-
*/
|
|
76
|
-
function createRulesBlock(rulesContent) {
|
|
77
|
-
return `${RULES_BEGIN}
|
|
78
|
-
${WARNING}
|
|
79
|
-
|
|
80
|
-
${rulesContent}
|
|
81
|
-
|
|
82
|
-
${RULES_END}`;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Write rules to the .windsurfrules file
|
|
86
|
-
* This will update the content between the RULES_BEGIN and RULES_END markers
|
|
87
|
-
* If the file doesn't exist, it will create it
|
|
88
|
-
* If the markers don't exist, it will append them to the existing content
|
|
89
|
-
*/
|
|
90
|
-
function writeRulesFile(rulesContent, rulesFilePath = path_1.default.join(process.cwd(), ".windsurfrules")) {
|
|
91
|
-
let fileContent;
|
|
92
|
-
const formattedRulesBlock = createRulesBlock(rulesContent);
|
|
93
|
-
// Check if file exists
|
|
94
|
-
if (fs_extra_1.default.existsSync(rulesFilePath)) {
|
|
95
|
-
const existingContent = fs_extra_1.default.readFileSync(rulesFilePath, "utf8");
|
|
96
|
-
// Check if our markers exist
|
|
97
|
-
if (existingContent.includes(RULES_BEGIN) &&
|
|
98
|
-
existingContent.includes(RULES_END)) {
|
|
99
|
-
// Replace content between markers
|
|
100
|
-
const beforeMarker = existingContent.split(RULES_BEGIN)[0];
|
|
101
|
-
const afterMarker = existingContent.split(RULES_END)[1];
|
|
102
|
-
fileContent = beforeMarker + formattedRulesBlock + afterMarker;
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
// Preserve the existing content and append markers
|
|
106
|
-
// Ensure there's proper spacing between existing content and markers
|
|
107
|
-
let separator = "";
|
|
108
|
-
if (!existingContent.endsWith("\n")) {
|
|
109
|
-
separator += "\n";
|
|
110
|
-
}
|
|
111
|
-
// Add an extra line if the file doesn't already end with multiple newlines
|
|
112
|
-
if (!existingContent.endsWith("\n\n")) {
|
|
113
|
-
separator += "\n";
|
|
114
|
-
}
|
|
115
|
-
// Create the new file content with preserved original content
|
|
116
|
-
fileContent = existingContent + separator + formattedRulesBlock;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// Create new file with markers and content
|
|
121
|
-
fileContent = formattedRulesBlock;
|
|
122
|
-
}
|
|
123
|
-
fs_extra_1.default.writeFileSync(rulesFilePath, fileContent);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Generate the rules file content based on rule files
|
|
127
|
-
*/
|
|
128
|
-
function generateRulesFileContent(ruleFiles) {
|
|
129
|
-
const alwaysRules = [];
|
|
130
|
-
const autoAttachedRules = [];
|
|
131
|
-
const agentRequestedRules = [];
|
|
132
|
-
const manualRules = [];
|
|
133
|
-
ruleFiles.forEach(({ path, metadata }) => {
|
|
134
|
-
// Determine rule type based on metadata
|
|
135
|
-
if (metadata.type === "always" ||
|
|
136
|
-
metadata.alwaysApply === true ||
|
|
137
|
-
metadata.alwaysApply === "true") {
|
|
138
|
-
alwaysRules.push(path);
|
|
139
|
-
}
|
|
140
|
-
else if (metadata.type === "auto-attached" || metadata.globs) {
|
|
141
|
-
const globPattern = typeof metadata.globs === "string" || Array.isArray(metadata.globs)
|
|
142
|
-
? Array.isArray(metadata.globs)
|
|
143
|
-
? metadata.globs.join(", ")
|
|
144
|
-
: metadata.globs
|
|
145
|
-
: undefined;
|
|
146
|
-
if (globPattern !== undefined) {
|
|
147
|
-
autoAttachedRules.push({ path, glob: globPattern });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
else if (metadata.type === "agent-requested" || metadata.description) {
|
|
151
|
-
agentRequestedRules.push(path);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
// Default to manual inclusion
|
|
155
|
-
manualRules.push(path);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
// Generate the content
|
|
159
|
-
let content = "";
|
|
160
|
-
// Always rules
|
|
161
|
-
if (alwaysRules.length > 0) {
|
|
162
|
-
content +=
|
|
163
|
-
"The following rules always apply to all files in the project:\n";
|
|
164
|
-
alwaysRules.forEach((rule) => {
|
|
165
|
-
content += `- ${rule}\n`;
|
|
166
|
-
});
|
|
167
|
-
content += "\n";
|
|
168
|
-
}
|
|
169
|
-
// Auto Attached rules
|
|
170
|
-
if (autoAttachedRules.length > 0) {
|
|
171
|
-
content +=
|
|
172
|
-
"The following rules are automatically attached to matching glob patterns:\n";
|
|
173
|
-
autoAttachedRules.forEach((rule) => {
|
|
174
|
-
content += `- [${rule.glob}] ${rule.path}\n`;
|
|
175
|
-
});
|
|
176
|
-
content += "\n";
|
|
177
|
-
}
|
|
178
|
-
// Agent Requested rules
|
|
179
|
-
if (agentRequestedRules.length > 0) {
|
|
180
|
-
content +=
|
|
181
|
-
"The following rules can be loaded when relevant. Check each file's description:\n";
|
|
182
|
-
agentRequestedRules.forEach((rule) => {
|
|
183
|
-
content += `- ${rule}\n`;
|
|
184
|
-
});
|
|
185
|
-
content += "\n";
|
|
186
|
-
}
|
|
187
|
-
// Manual rules
|
|
188
|
-
if (manualRules.length > 0) {
|
|
189
|
-
content +=
|
|
190
|
-
"The following rules are only included when explicitly referenced:\n";
|
|
191
|
-
manualRules.forEach((rule) => {
|
|
192
|
-
content += `- ${rule}\n`;
|
|
193
|
-
});
|
|
194
|
-
content += "\n";
|
|
195
|
-
}
|
|
196
|
-
return content.trim();
|
|
197
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Check if a resolved path is safely within the specified base directory.
|
|
3
|
-
* This prevents path traversal attacks where malicious paths like "../../../etc"
|
|
4
|
-
* or absolute paths could escape the intended directory.
|
|
5
|
-
*
|
|
6
|
-
* @param baseDir - The directory that should contain the path
|
|
7
|
-
* @param relativePath - The potentially untrusted relative path
|
|
8
|
-
* @returns The safely resolved full path, or null if the path would escape baseDir
|
|
9
|
-
*/
|
|
10
|
-
export declare function resolveSafePath(baseDir: string, relativePath: string): string | null;
|
package/dist/utils/safe-path.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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.resolveSafePath = resolveSafePath;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
-
/**
|
|
9
|
-
* Check if a resolved path is safely within the specified base directory.
|
|
10
|
-
* This prevents path traversal attacks where malicious paths like "../../../etc"
|
|
11
|
-
* or absolute paths could escape the intended directory.
|
|
12
|
-
*
|
|
13
|
-
* @param baseDir - The directory that should contain the path
|
|
14
|
-
* @param relativePath - The potentially untrusted relative path
|
|
15
|
-
* @returns The safely resolved full path, or null if the path would escape baseDir
|
|
16
|
-
*/
|
|
17
|
-
function resolveSafePath(baseDir, relativePath) {
|
|
18
|
-
// Resolve both to absolute paths
|
|
19
|
-
const resolvedBase = node_path_1.default.resolve(baseDir);
|
|
20
|
-
const resolvedTarget = node_path_1.default.resolve(baseDir, relativePath);
|
|
21
|
-
// The resolved path must start with the base directory + separator
|
|
22
|
-
// This ensures it's truly inside the directory, not a sibling with similar prefix
|
|
23
|
-
// e.g., /foo/bar should not match /foo/bar-other
|
|
24
|
-
if (!resolvedTarget.startsWith(resolvedBase + node_path_1.default.sep)) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return resolvedTarget;
|
|
28
|
-
}
|