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.
@@ -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[];
@@ -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
- const parts = presetPath.split(node_path_1.default.sep);
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 for at least one valid subdirectory
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
- if (!hasRules &&
329
- !hasCommands &&
330
- !hasHooks &&
331
- !hasAssets &&
332
- !hasSkills &&
333
- !hasAgents) {
334
- throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, agents/, hooks.json, or assets/`);
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 preset = await loadPreset(presetPath, cwd);
395
- const presetRootDir = preset.rootDir;
396
- // Load preset rules from rules/ subdirectory
397
- const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
398
- if (fs_extra_1.default.existsSync(presetRulesPath)) {
399
- const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
400
- allRules.push(...presetRules);
401
- }
402
- // Load preset commands from commands/ subdirectory
403
- const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
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.1",
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;
@@ -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
- }