aicm 0.20.1 → 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/utils/config.d.ts +1 -0
- package/dist/utils/config.js +107 -54
- package/package.json +6 -2
- package/dist/utils/safe-path.d.ts +0 -10
- package/dist/utils/safe-path.js +0 -28
package/dist/utils/config.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export declare function resolvePresetPath(presetPath: string, cwd: string): stri
|
|
|
96
96
|
export declare function loadPreset(presetPath: string, cwd: string): Promise<{
|
|
97
97
|
config: Config;
|
|
98
98
|
rootDir: string;
|
|
99
|
+
resolvedPath: string;
|
|
99
100
|
}>;
|
|
100
101
|
export declare function loadAllRules(config: Config, cwd: string): Promise<{
|
|
101
102
|
rules: RuleFile[];
|
package/dist/utils/config.js
CHANGED
|
@@ -277,7 +277,8 @@ function extractNamespaceFromPresetPath(presetPath) {
|
|
|
277
277
|
// For scoped packages like @scope/package/subdir, create nested directories
|
|
278
278
|
return presetPath.split("/");
|
|
279
279
|
}
|
|
280
|
-
|
|
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);
|
|
281
282
|
return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
|
|
282
283
|
}
|
|
283
284
|
function resolvePresetPath(presetPath, cwd) {
|
|
@@ -318,26 +319,110 @@ async function loadPreset(presetPath, cwd) {
|
|
|
318
319
|
}
|
|
319
320
|
const presetDir = node_path_1.default.dirname(resolvedPresetPath);
|
|
320
321
|
const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
|
|
321
|
-
// Check
|
|
322
|
+
// Check if preset has content or inherits from other presets
|
|
322
323
|
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
|
|
323
324
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
324
325
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
325
326
|
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
326
327
|
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
|
|
327
328
|
const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "agents"));
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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`);
|
|
335
339
|
}
|
|
336
340
|
return {
|
|
337
341
|
config: presetConfig,
|
|
338
342
|
rootDir: presetRootDir,
|
|
343
|
+
resolvedPath: resolvedPresetPath,
|
|
339
344
|
};
|
|
340
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
|
+
}
|
|
341
426
|
async function loadAllRules(config, cwd) {
|
|
342
427
|
const allRules = [];
|
|
343
428
|
const allCommands = [];
|
|
@@ -388,52 +473,20 @@ async function loadAllRules(config, cwd) {
|
|
|
388
473
|
allAgents.push(...localAgents);
|
|
389
474
|
}
|
|
390
475
|
}
|
|
391
|
-
// Load presets
|
|
392
|
-
if (config.presets) {
|
|
476
|
+
// Load presets recursively
|
|
477
|
+
if (config.presets && config.presets.length > 0) {
|
|
478
|
+
const visited = new Set();
|
|
393
479
|
for (const presetPath of config.presets) {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
if (fs_extra_1.default.existsSync(presetCommandsPath)) {
|
|
405
|
-
const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
|
|
406
|
-
allCommands.push(...presetCommands);
|
|
407
|
-
}
|
|
408
|
-
// Load preset hooks from hooks.json (sibling to hooks/ directory)
|
|
409
|
-
const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
|
|
410
|
-
if (fs_extra_1.default.existsSync(presetHooksFile)) {
|
|
411
|
-
const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
|
|
412
|
-
allHooksConfigs.push(presetHooksConfig);
|
|
413
|
-
allHookFiles.push(...presetHookFiles);
|
|
414
|
-
}
|
|
415
|
-
// Load preset assets from assets/ subdirectory
|
|
416
|
-
const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
|
|
417
|
-
if (fs_extra_1.default.existsSync(presetAssetsPath)) {
|
|
418
|
-
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
419
|
-
allAssets.push(...presetAssets);
|
|
420
|
-
}
|
|
421
|
-
// Load preset skills from skills/ subdirectory
|
|
422
|
-
const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
|
|
423
|
-
if (fs_extra_1.default.existsSync(presetSkillsPath)) {
|
|
424
|
-
const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
|
|
425
|
-
allSkills.push(...presetSkills);
|
|
426
|
-
}
|
|
427
|
-
// Load preset agents from agents/ subdirectory
|
|
428
|
-
const presetAgentsPath = node_path_1.default.join(presetRootDir, "agents");
|
|
429
|
-
if (fs_extra_1.default.existsSync(presetAgentsPath)) {
|
|
430
|
-
const presetAgents = await loadAgentsFromDirectory(presetAgentsPath, "preset", presetPath);
|
|
431
|
-
allAgents.push(...presetAgents);
|
|
432
|
-
}
|
|
433
|
-
// Merge MCP servers from preset
|
|
434
|
-
if (preset.config.mcpServers) {
|
|
435
|
-
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
436
|
-
}
|
|
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);
|
|
437
490
|
}
|
|
438
491
|
}
|
|
439
492
|
// Merge all hooks configurations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicm",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"description": "A TypeScript CLI tool for managing AI IDE rules across different projects and teams",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"types": "dist/api.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lint": "eslint",
|
|
26
26
|
"prepare": "husky install && npx ts-node src/bin/aicm.ts install",
|
|
27
27
|
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
28
|
-
"release": "np --no-tests"
|
|
28
|
+
"release": "np --no-tests --no-publish"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
31
31
|
"ai",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "Ran Yitzhaki <ranyitz@gmail.com>",
|
|
39
39
|
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/ranyitz/aicm"
|
|
43
|
+
},
|
|
40
44
|
"dependencies": {
|
|
41
45
|
"arg": "^5.0.2",
|
|
42
46
|
"chalk": "^4.1.2",
|
|
@@ -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
|
-
}
|