@intellectronica/ruler 0.3.10 → 0.3.12

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.
@@ -40,6 +40,7 @@ exports.processSingleConfiguration = processSingleConfiguration;
40
40
  exports.applyConfigurationsToAgents = applyConfigurationsToAgents;
41
41
  exports.updateGitignore = updateGitignore;
42
42
  const path = __importStar(require("path"));
43
+ const fs_1 = require("fs");
43
44
  const FileSystemUtils = __importStar(require("./FileSystemUtils"));
44
45
  const RuleProcessor_1 = require("./RuleProcessor");
45
46
  const ConfigLoader_1 = require("./ConfigLoader");
@@ -51,16 +52,13 @@ const propagateOpenCodeMcp_1 = require("../mcp/propagateOpenCodeMcp");
51
52
  const agent_utils_1 = require("../agents/agent-utils");
52
53
  const capabilities_1 = require("../mcp/capabilities");
53
54
  const constants_1 = require("../constants");
54
- async function loadNestedConfigurations(projectRoot, configPath, localOnly) {
55
+ async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
55
56
  const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
56
- const rootConfig = await (0, ConfigLoader_1.loadConfig)({
57
- projectRoot,
58
- configPath,
59
- });
60
57
  const results = [];
61
58
  const rulerDirConfigs = await processIndependentRulerDirs(rulerDirs);
62
59
  for (const { rulerDir, files } of rulerDirConfigs) {
63
- results.push(await createHierarchicalConfiguration(rulerDir, files, rootConfig));
60
+ const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
61
+ results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
64
62
  }
65
63
  return results;
66
64
  }
@@ -77,14 +75,78 @@ async function processIndependentRulerDirs(rulerDirs) {
77
75
  }
78
76
  return results;
79
77
  }
80
- async function createHierarchicalConfiguration(rulerDir, files, rootConfig) {
78
+ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
81
79
  await warnAboutLegacyMcpJson(rulerDir);
82
80
  const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(rulerDir));
81
+ const directoryRoot = path.dirname(rulerDir);
82
+ const localConfigPath = path.join(rulerDir, 'ruler.toml');
83
+ let configPathToUse = cliConfigPath;
84
+ try {
85
+ await fs_1.promises.access(localConfigPath);
86
+ configPathToUse = localConfigPath;
87
+ }
88
+ catch {
89
+ // fall back to CLI config or default resolution
90
+ }
91
+ const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
92
+ const unifiedConfig = await loadUnifiedConfig({
93
+ projectRoot: directoryRoot,
94
+ configPath: configPathToUse,
95
+ });
96
+ let rulerMcpJson = null;
97
+ if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
98
+ rulerMcpJson = {
99
+ mcpServers: unifiedConfig.mcp.servers,
100
+ };
101
+ }
83
102
  return {
84
103
  rulerDir,
85
- config: rootConfig,
104
+ config,
86
105
  concatenatedRules,
87
- rulerMcpJson: null, // No nested MCP support - each level uses root config only
106
+ rulerMcpJson,
107
+ };
108
+ }
109
+ async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested) {
110
+ const directoryRoot = path.dirname(rulerDir);
111
+ const localConfigPath = path.join(rulerDir, 'ruler.toml');
112
+ let hasLocalConfig = false;
113
+ try {
114
+ await fs_1.promises.access(localConfigPath);
115
+ hasLocalConfig = true;
116
+ }
117
+ catch {
118
+ hasLocalConfig = false;
119
+ }
120
+ const loaded = await (0, ConfigLoader_1.loadConfig)({
121
+ projectRoot: directoryRoot,
122
+ configPath: hasLocalConfig ? localConfigPath : cliConfigPath,
123
+ });
124
+ const cloned = cloneLoadedConfig(loaded);
125
+ if (resolvedNested) {
126
+ if (hasLocalConfig && loaded.nestedDefined && loaded.nested === false) {
127
+ (0, constants_1.logWarn)(`Nested mode is enabled but ${localConfigPath} sets nested = false. Continuing with nested processing.`);
128
+ }
129
+ cloned.nested = true;
130
+ cloned.nestedDefined = true;
131
+ }
132
+ return cloned;
133
+ }
134
+ function cloneLoadedConfig(config) {
135
+ const clonedAgentConfigs = {};
136
+ for (const [agent, agentConfig] of Object.entries(config.agentConfigs)) {
137
+ clonedAgentConfigs[agent] = {
138
+ ...agentConfig,
139
+ mcp: agentConfig.mcp ? { ...agentConfig.mcp } : undefined,
140
+ };
141
+ }
142
+ return {
143
+ defaultAgents: config.defaultAgents ? [...config.defaultAgents] : undefined,
144
+ agentConfigs: clonedAgentConfigs,
145
+ cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
146
+ mcp: config.mcp ? { ...config.mcp } : undefined,
147
+ gitignore: config.gitignore ? { ...config.gitignore } : undefined,
148
+ nested: config.nested,
149
+ nestedDefined: config.nestedDefined,
88
150
  };
89
151
  }
90
152
  /**
@@ -120,7 +182,7 @@ async function findRulerDirectories(projectRoot, localOnly, hierarchical) {
120
182
  async function warnAboutLegacyMcpJson(rulerDir) {
121
183
  try {
122
184
  const legacyMcpPath = path.join(rulerDir, 'mcp.json');
123
- await (await Promise.resolve().then(() => __importStar(require('fs/promises')))).access(legacyMcpPath);
185
+ await fs_1.promises.access(legacyMcpPath);
124
186
  (0, constants_1.logWarn)('Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
125
187
  }
126
188
  catch {
@@ -171,13 +233,14 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
171
233
  * @param cliMcpStrategy MCP strategy from CLI
172
234
  * @returns Promise resolving to array of generated file paths
173
235
  */
174
- async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true) {
236
+ async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true, skillsEnabled = true) {
175
237
  const allGeneratedPaths = [];
176
238
  for (const config of configurations) {
177
239
  (0, constants_1.logVerboseInfo)(`Processing .ruler directory: ${config.rulerDir}`, verbose, dryRun);
178
240
  const rulerRoot = path.dirname(config.rulerDir);
179
- const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
180
- allGeneratedPaths.push(...paths);
241
+ const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
242
+ const normalizedPaths = paths.map((p) => path.isAbsolute(p) ? p : path.join(rulerRoot, p));
243
+ allGeneratedPaths.push(...normalizedPaths);
181
244
  }
182
245
  return allGeneratedPaths;
183
246
  }
@@ -193,8 +256,43 @@ async function processHierarchicalConfigurations(agents, configurations, verbose
193
256
  * @param cliMcpStrategy MCP strategy from CLI
194
257
  * @returns Promise resolving to array of generated file paths
195
258
  */
196
- async function processSingleConfiguration(agents, configuration, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true) {
197
- return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
259
+ async function processSingleConfiguration(agents, configuration, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true, skillsEnabled = true) {
260
+ return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
261
+ }
262
+ /**
263
+ * Adds Skillz MCP server to rulerMcpJson if skills exist and any agent needs it.
264
+ * Returns augmented MCP config or original if no changes needed.
265
+ */
266
+ async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose) {
267
+ // Check if any agent supports MCP stdio but not native skills
268
+ const hasAgentNeedingSkillz = agents.some((agent) => agent.supportsMcpStdio?.() && !agent.supportsNativeSkills?.());
269
+ if (!hasAgentNeedingSkillz) {
270
+ return rulerMcpJson;
271
+ }
272
+ // Check if .skillz directory exists
273
+ try {
274
+ const { SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
275
+ const skillzPath = path.join(projectRoot, SKILLZ_DIR);
276
+ await fs_1.promises.access(skillzPath);
277
+ // Skills exist, add Skillz MCP server
278
+ const { buildSkillzMcpConfig } = await Promise.resolve().then(() => __importStar(require('./SkillsProcessor')));
279
+ const skillzMcp = buildSkillzMcpConfig(projectRoot);
280
+ // Initialize empty config if null
281
+ const baseConfig = rulerMcpJson || { mcpServers: {} };
282
+ const mcpServers = baseConfig.mcpServers || {};
283
+ (0, constants_1.logVerbose)('Adding Skillz MCP server to configuration for agents that need it', verbose);
284
+ return {
285
+ ...baseConfig,
286
+ mcpServers: {
287
+ ...mcpServers,
288
+ ...skillzMcp,
289
+ },
290
+ };
291
+ }
292
+ catch {
293
+ // No .skillz directory, return original config
294
+ return rulerMcpJson;
295
+ }
198
296
  }
199
297
  /**
200
298
  * Applies configurations to the selected agents (internal function).
@@ -207,23 +305,22 @@ async function processSingleConfiguration(agents, configuration, projectRoot, ve
207
305
  * @param dryRun Whether to perform a dry run
208
306
  * @returns Promise resolving to array of generated file paths
209
307
  */
210
- async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true) {
308
+ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true, skillsEnabled = true) {
211
309
  const generatedPaths = [];
212
310
  let agentsMdWritten = false;
311
+ // Add Skillz MCP server to rulerMcpJson if skills are enabled
312
+ // This must happen before calling agent.applyRulerConfig() so that agents
313
+ // that handle MCP internally (e.g. Codex, Gemini) receive the Skillz server
314
+ let augmentedRulerMcpJson = rulerMcpJson;
315
+ if (skillsEnabled && !dryRun) {
316
+ augmentedRulerMcpJson = await addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose);
317
+ }
213
318
  for (const agent of agents) {
214
319
  (0, constants_1.logInfo)(`Applying rules for ${agent.getName()}...`, dryRun);
215
320
  (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
216
321
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
217
322
  // Collect output paths for .gitignore
218
- let outputPaths;
219
- // Special handling for Windsurf agent to account for file splitting
220
- if (agent.getIdentifier() === 'windsurf' &&
221
- 'getActualOutputPaths' in agent) {
222
- outputPaths = agent.getActualOutputPaths(concatenatedRules, projectRoot, agentConfig);
223
- }
224
- else {
225
- outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
226
- }
323
+ const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
227
324
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
228
325
  generatedPaths.push(...outputPaths);
229
326
  // Only add the backup file paths to the gitignore list if backups are enabled
@@ -247,7 +344,7 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
247
344
  }
248
345
  }
249
346
  let finalAgentConfig = agentConfig;
250
- if (agent.getIdentifier() === 'augmentcode' && rulerMcpJson) {
347
+ if (agent.getIdentifier() === 'augmentcode' && augmentedRulerMcpJson) {
251
348
  const resolvedStrategy = cliMcpStrategy ??
252
349
  agentConfig?.mcp?.strategy ??
253
350
  config.mcp?.strategy ??
@@ -261,25 +358,59 @@ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJs
261
358
  };
262
359
  }
263
360
  if (!skipApplyForThisAgent) {
264
- await agent.applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, finalAgentConfig, backup);
361
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, augmentedRulerMcpJson, finalAgentConfig, backup);
265
362
  }
266
363
  }
267
364
  // Handle MCP configuration
268
- await handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
365
+ await handleMcpConfiguration(agent, agentConfig, config, augmentedRulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
269
366
  }
270
367
  return generatedPaths;
271
368
  }
272
- async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true) {
369
+ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true, skillsEnabled = true) {
273
370
  if (!(0, capabilities_1.agentSupportsMcp)(agent)) {
274
371
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} does not support MCP - skipping MCP configuration`, verbose);
275
372
  return;
276
373
  }
277
374
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
278
375
  const mcpEnabledForAgent = cliMcpEnabled && (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
279
- if (!dest || !mcpEnabledForAgent || !rulerMcpJson) {
376
+ if (!dest || !mcpEnabledForAgent) {
280
377
  return;
281
378
  }
282
- const filteredMcpJson = (0, capabilities_1.filterMcpConfigForAgent)(rulerMcpJson, agent);
379
+ let filteredMcpJson = rulerMcpJson
380
+ ? (0, capabilities_1.filterMcpConfigForAgent)(rulerMcpJson, agent)
381
+ : null;
382
+ // Add Skillz MCP server for agents that support stdio but not native skills
383
+ // Only add if skills are enabled
384
+ if (skillsEnabled &&
385
+ agent.supportsMcpStdio?.() &&
386
+ !agent.supportsNativeSkills?.()) {
387
+ // Check if .skillz directory exists
388
+ try {
389
+ const { SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
390
+ const skillzPath = path.join(projectRoot, SKILLZ_DIR);
391
+ await fs_1.promises.access(skillzPath);
392
+ // Skills exist, add Skillz MCP server
393
+ const { buildSkillzMcpConfig } = await Promise.resolve().then(() => __importStar(require('./SkillsProcessor')));
394
+ const skillzMcp = buildSkillzMcpConfig(projectRoot);
395
+ // Merge Skillz server into MCP config
396
+ // Initialize empty config if null
397
+ if (!filteredMcpJson) {
398
+ filteredMcpJson = { mcpServers: {} };
399
+ }
400
+ const mcpServers = filteredMcpJson.mcpServers || {};
401
+ filteredMcpJson = {
402
+ ...filteredMcpJson,
403
+ mcpServers: {
404
+ ...mcpServers,
405
+ ...skillzMcp,
406
+ },
407
+ };
408
+ (0, constants_1.logVerboseInfo)(`Added Skillz MCP server for ${agent.getName()}`, verbose, dryRun);
409
+ }
410
+ catch {
411
+ // No .skillz directory, skip adding Skillz server
412
+ }
413
+ }
283
414
  if (!filteredMcpJson) {
284
415
  (0, constants_1.logVerbose)(`No compatible MCP servers found for ${agent.getName()} - skipping MCP configuration`, verbose);
285
416
  return;
@@ -369,6 +500,35 @@ function transformMcpForClaude(mcpJson) {
369
500
  transformedMcp.mcpServers = transformedServers;
370
501
  return transformedMcp;
371
502
  }
503
+ /**
504
+ * Transform MCP server types for Kilo Code compatibility.
505
+ * Kilo Code expects "streamable-http" for remote HTTP servers, not "remote".
506
+ */
507
+ function transformMcpForKiloCode(mcpJson) {
508
+ if (!mcpJson.mcpServers || typeof mcpJson.mcpServers !== 'object') {
509
+ return mcpJson;
510
+ }
511
+ const transformedMcp = { ...mcpJson };
512
+ const transformedServers = {};
513
+ for (const [name, serverDef] of Object.entries(mcpJson.mcpServers)) {
514
+ if (serverDef && typeof serverDef === 'object') {
515
+ const server = serverDef;
516
+ const transformedServer = { ...server };
517
+ // Transform type: "remote" to "streamable-http" for HTTP-based servers
518
+ if (server.type === 'remote' &&
519
+ server.url &&
520
+ typeof server.url === 'string') {
521
+ transformedServer.type = 'streamable-http';
522
+ }
523
+ transformedServers[name] = transformedServer;
524
+ }
525
+ else {
526
+ transformedServers[name] = serverDef;
527
+ }
528
+ }
529
+ transformedMcp.mcpServers = transformedServers;
530
+ return transformedMcp;
531
+ }
372
532
  async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup = true) {
373
533
  const strategy = cliMcpStrategy ??
374
534
  agentConfig?.mcp?.strategy ??
@@ -385,11 +545,14 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
385
545
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
386
546
  }
387
547
  else {
388
- // Transform MCP config for Claude Code compatibility
548
+ // Transform MCP config for agent-specific compatibility
389
549
  let mcpToMerge = filteredMcpJson;
390
550
  if (agent.getIdentifier() === 'claude') {
391
551
  mcpToMerge = transformMcpForClaude(filteredMcpJson);
392
552
  }
553
+ else if (agent.getIdentifier() === 'kilocode') {
554
+ mcpToMerge = transformMcpForKiloCode(filteredMcpJson);
555
+ }
393
556
  const existing = await (0, mcp_1.readNativeMcp)(dest);
394
557
  const merged = (0, merge_1.mergeMcp)(existing, mcpToMerge, strategy, serverKey);
395
558
  // Firebase Studio (IDX) expects no "type" fields in .idx/mcp.json server entries.
package/dist/lib.js CHANGED
@@ -1,7 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.allAgents = void 0;
4
37
  exports.applyAllAgentConfigs = applyAllAgentConfigs;
38
+ const path = __importStar(require("path"));
5
39
  const agents_1 = require("./agents");
6
40
  Object.defineProperty(exports, "allAgents", { enumerable: true, get: function () { return agents_1.allAgents; } });
7
41
  const constants_1 = require("./constants");
@@ -9,6 +43,16 @@ const apply_engine_1 = require("./core/apply-engine");
9
43
  const config_utils_1 = require("./core/config-utils");
10
44
  const agent_selection_1 = require("./core/agent-selection");
11
45
  const agents = agents_1.allAgents;
46
+ /**
47
+ * Resolves skills enabled state based on precedence: CLI flag > ruler.toml > default (enabled)
48
+ */
49
+ function resolveSkillsEnabled(cliFlag, configSetting) {
50
+ return cliFlag !== undefined
51
+ ? cliFlag
52
+ : configSetting !== undefined
53
+ ? configSetting
54
+ : true; // default to enabled
55
+ }
12
56
  /**
13
57
  * Applies ruler configurations for all supported AI agents.
14
58
  * @param projectRoot Root directory of the project
@@ -18,7 +62,7 @@ const agents = agents_1.allAgents;
18
62
  * @param projectRoot Root directory of the project
19
63
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
20
64
  */
21
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true) {
65
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled) {
22
66
  // Load configuration and rules
23
67
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
24
68
  if (configPath) {
@@ -28,20 +72,35 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
28
72
  let generatedPaths;
29
73
  let loadedConfig;
30
74
  if (nested) {
31
- const hierarchicalConfigs = await (0, apply_engine_1.loadNestedConfigurations)(projectRoot, configPath, localOnly);
75
+ const hierarchicalConfigs = await (0, apply_engine_1.loadNestedConfigurations)(projectRoot, configPath, localOnly, nested);
32
76
  if (hierarchicalConfigs.length === 0) {
33
77
  throw new Error('No .ruler directories found');
34
78
  }
79
+ (0, constants_1.logWarn)('Nested mode is experimental and may change in future releases.', dryRun);
35
80
  // Use the root config for agent selection (all levels share the same agent settings)
36
- const rootConfig = hierarchicalConfigs[0].config;
81
+ const rootConfigEntry = selectRootConfiguration(hierarchicalConfigs, projectRoot);
82
+ const rootConfig = rootConfigEntry.config;
37
83
  loadedConfig = rootConfig;
38
84
  rootConfig.cliAgents = includedAgents;
39
85
  (0, constants_1.logVerbose)(`Loaded ${hierarchicalConfigs.length} .ruler directory configurations`, verbose);
40
86
  (0, constants_1.logVerbose)(`Root configuration has ${Object.keys(rootConfig.agentConfigs).length} agent configs`, verbose);
41
- normalizeAgentConfigs(rootConfig, agents);
87
+ for (const configEntry of hierarchicalConfigs) {
88
+ normalizeAgentConfigs(configEntry.config, agents);
89
+ }
42
90
  selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(rootConfig, agents);
43
91
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
44
- generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
92
+ // Propagate skills if enabled - do this for each nested directory
93
+ const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, rootConfig.skills?.enabled);
94
+ if (skillsEnabledResolved) {
95
+ const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
96
+ // Propagate skills for each nested .ruler directory
97
+ for (const configEntry of hierarchicalConfigs) {
98
+ const nestedRoot = path.dirname(configEntry.rulerDir);
99
+ (0, constants_1.logVerbose)(`Propagating skills for nested directory: ${nestedRoot}`, verbose);
100
+ await propagateSkills(nestedRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
101
+ }
102
+ }
103
+ generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabledResolved);
45
104
  }
46
105
  else {
47
106
  const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
@@ -52,9 +111,24 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
52
111
  normalizeAgentConfigs(singleConfig.config, agents);
53
112
  selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(singleConfig.config, agents);
54
113
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
55
- generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
114
+ // Propagate skills if enabled
115
+ const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, singleConfig.config.skills?.enabled);
116
+ if (skillsEnabledResolved) {
117
+ const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
118
+ await propagateSkills(projectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
119
+ }
120
+ generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabledResolved);
56
121
  }
57
- await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun);
122
+ // Add skills-generated paths to gitignore if skills are enabled
123
+ let allGeneratedPaths = generatedPaths;
124
+ const skillsEnabledForGitignore = resolveSkillsEnabled(skillsEnabled, loadedConfig.skills?.enabled);
125
+ if (skillsEnabledForGitignore) {
126
+ // Skills enabled by default or explicitly
127
+ const { getSkillsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
128
+ const skillsPaths = await getSkillsGitignorePaths(projectRoot);
129
+ allGeneratedPaths = [...generatedPaths, ...skillsPaths];
130
+ }
131
+ await (0, apply_engine_1.updateGitignore)(projectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun);
58
132
  }
59
133
  /**
60
134
  * Normalizes per-agent config keys to agent identifiers for consistent lookup.
@@ -66,3 +140,27 @@ function normalizeAgentConfigs(config, agents) {
66
140
  // Normalize per-agent config keys to agent identifiers (exact match or substring match)
67
141
  config.agentConfigs = (0, config_utils_1.mapRawAgentConfigs)(config.agentConfigs, agents);
68
142
  }
143
+ function selectRootConfiguration(configurations, projectRoot) {
144
+ if (configurations.length === 0) {
145
+ throw new Error('No hierarchical configurations available');
146
+ }
147
+ const normalizedProjectRoot = path.resolve(projectRoot);
148
+ let bestIndex = -1;
149
+ let bestDepth = Number.POSITIVE_INFINITY;
150
+ for (let i = 0; i < configurations.length; i++) {
151
+ const entry = configurations[i];
152
+ const normalizedDir = path.resolve(entry.rulerDir);
153
+ if (!normalizedDir.startsWith(normalizedProjectRoot)) {
154
+ continue;
155
+ }
156
+ const depth = normalizedDir.split(path.sep).length;
157
+ if (depth < bestDepth) {
158
+ bestDepth = depth;
159
+ bestIndex = i;
160
+ }
161
+ }
162
+ if (bestIndex === -1) {
163
+ return configurations[0];
164
+ }
165
+ return configurations[bestIndex];
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -35,6 +35,9 @@
35
35
  "url": "https://github.com/intellectronica/ruler/issues"
36
36
  },
37
37
  "homepage": "https://ai.intellectronica.net/ruler",
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
38
41
  "files": [
39
42
  "dist",
40
43
  "README.md",
@@ -47,22 +50,23 @@
47
50
  "@types/iarna__toml": "^2.0.5",
48
51
  "@types/jest": "^29.5.14",
49
52
  "@types/js-yaml": "^4.0.9",
50
- "@types/node": "^22.15.24",
51
- "@types/yargs": "^17.0.33",
52
- "@typescript-eslint/eslint-plugin": "^8.32.1",
53
- "@typescript-eslint/parser": "^8.32.1",
54
- "eslint": "^8.57.1",
55
- "eslint-config-prettier": "^10.1.5",
56
- "eslint-plugin-prettier": "^5.4.0",
53
+ "@types/node": "^24.9.2",
54
+ "@types/yargs": "^17.0.34",
55
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
56
+ "@typescript-eslint/parser": "^8.46.2",
57
+ "eslint": "^9.38.0",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "eslint-plugin-prettier": "^5.5.4",
57
60
  "jest": "^29.7.0",
58
- "prettier": "^3.5.3",
59
- "ts-jest": "^29.3.4",
60
- "typescript": "^5.8.3"
61
+ "prettier": "^3.6.2",
62
+ "ts-jest": "^29.4.5",
63
+ "typescript": "^5.9.3",
64
+ "typescript-eslint": "^8.46.2"
61
65
  },
62
66
  "dependencies": {
63
67
  "@iarna/toml": "^2.2.5",
64
68
  "js-yaml": "^4.1.0",
65
- "yargs": "^17.7.2",
66
- "zod": "^3.25.28"
69
+ "yargs": "^18.0.0",
70
+ "zod": "^4.1.12"
67
71
  }
68
72
  }