@intellectronica/ruler 0.3.4 → 0.3.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.
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CodexCliAgent = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs_1 = require("fs");
39
- const toml = __importStar(require("toml"));
40
39
  const toml_1 = require("@iarna/toml");
41
40
  const AgentsMdAgent_1 = require("./AgentsMdAgent");
42
41
  const FileSystemUtils_1 = require("../core/FileSystemUtils");
@@ -44,26 +43,26 @@ const constants_1 = require("../constants");
44
43
  /**
45
44
  * OpenAI Codex CLI agent adapter.
46
45
  */
47
- class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
46
+ class CodexCliAgent {
47
+ constructor() {
48
+ this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
49
+ }
48
50
  getIdentifier() {
49
51
  return 'codex';
50
52
  }
51
53
  getName() {
52
54
  return 'OpenAI Codex CLI';
53
55
  }
54
- async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
55
- // First perform idempotent AGENTS.md write via base class (instructions file).
56
- await super.applyRulerConfig(concatenatedRules, projectRoot, null, {
56
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
57
+ // First perform idempotent AGENTS.md write via composed AgentsMdAgent
58
+ await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
57
59
  // Preserve explicit outputPath precedence semantics if provided.
58
60
  outputPath: agentConfig?.outputPath ||
59
61
  agentConfig?.outputPathInstructions ||
60
62
  undefined,
61
- });
62
- // Resolve config path helper (mirrors previous logic)
63
- const defaults = {
64
- instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
65
- config: path.join(projectRoot, '.codex', 'config.toml'),
66
- };
63
+ }, backup);
64
+ // Use proper path resolution from getDefaultOutputPath and agentConfig
65
+ const defaults = this.getDefaultOutputPath(projectRoot);
67
66
  const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
68
67
  if (mcpEnabled && rulerMcpJson) {
69
68
  // Apply MCP server filtering and transformation
@@ -73,7 +72,7 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
73
72
  return; // No compatible servers found
74
73
  }
75
74
  const filteredRulerMcpJson = filteredMcpConfig;
76
- // Determine the config file path
75
+ // Determine the config file path using proper precedence
77
76
  const configPath = agentConfig?.outputPathConfig ?? defaults.config;
78
77
  // Ensure the parent directory exists
79
78
  await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
@@ -85,7 +84,7 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
85
84
  let existingConfig = {};
86
85
  try {
87
86
  const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
88
- existingConfig = toml.parse(existingContent);
87
+ existingConfig = (0, toml_1.parse)(existingContent);
89
88
  }
90
89
  catch {
91
90
  // File doesn't exist or can't be parsed, use empty config
@@ -121,62 +120,19 @@ class CodexCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
121
120
  updatedConfig.mcp_servers[serverName] = mcpServer;
122
121
  }
123
122
  }
124
- // Convert to TOML with special handling for env to ensure it's an inline table
125
- let tomlContent = '';
126
- // Handle non-mcp_servers sections first
127
- const configWithoutMcpServers = { ...updatedConfig };
128
- delete configWithoutMcpServers.mcp_servers;
129
- if (Object.keys(configWithoutMcpServers).length > 0) {
130
- tomlContent += (0, toml_1.stringify)(configWithoutMcpServers);
131
- }
132
- // Now handle mcp_servers with special formatting for env
133
- if (updatedConfig.mcp_servers &&
134
- Object.keys(updatedConfig.mcp_servers).length > 0) {
135
- for (const [serverName, serverConfig] of Object.entries(updatedConfig.mcp_servers)) {
136
- tomlContent += `\n[mcp_servers.${serverName}]\n`;
137
- // Add command
138
- if (serverConfig.command) {
139
- tomlContent += `command = "${serverConfig.command}"\n`;
140
- }
141
- // Add args if present
142
- if (serverConfig.args && Array.isArray(serverConfig.args)) {
143
- const argsStr = JSON.stringify(serverConfig.args)
144
- .replace(/"/g, '"')
145
- .replace(/,/g, ', ');
146
- tomlContent += `args = ${argsStr}\n`;
147
- }
148
- // Add env as inline table if present
149
- if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
150
- tomlContent += `env = { `;
151
- const entries = Object.entries(serverConfig.env);
152
- for (let i = 0; i < entries.length; i++) {
153
- const [key, value] = entries[i];
154
- tomlContent += `${key} = "${value}"`;
155
- if (i < entries.length - 1) {
156
- tomlContent += ', ';
157
- }
158
- }
159
- tomlContent += ` }\n`;
160
- }
161
- // Add headers as inline table if present (from transformed remote servers)
162
- if (serverConfig.headers &&
163
- Object.keys(serverConfig.headers).length > 0) {
164
- tomlContent += `headers = { `;
165
- const entries = Object.entries(serverConfig.headers);
166
- for (let i = 0; i < entries.length; i++) {
167
- const [key, value] = entries[i];
168
- tomlContent += `${JSON.stringify(key)} = "${value}"`;
169
- if (i < entries.length - 1) {
170
- tomlContent += ', ';
171
- }
172
- }
173
- tomlContent += ` }\n`;
174
- }
175
- }
176
- }
123
+ // Convert to TOML using structured objects
124
+ const finalConfig = { ...updatedConfig };
125
+ // @iarna/toml should handle the formatting properly
126
+ const tomlContent = (0, toml_1.stringify)(finalConfig);
177
127
  await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
178
128
  }
179
129
  }
130
+ getDefaultOutputPath(projectRoot) {
131
+ return {
132
+ instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
133
+ config: path.join(projectRoot, '.codex', 'config.toml'),
134
+ };
135
+ }
180
136
  supportsMcpStdio() {
181
137
  return true;
182
138
  }
@@ -1,17 +1,95 @@
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.CopilotAgent = void 0;
37
+ const path = __importStar(require("path"));
4
38
  const AgentsMdAgent_1 = require("./AgentsMdAgent");
39
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
+ const fs_1 = require("fs");
5
41
  /**
6
42
  * GitHub Copilot agent adapter.
43
+ * Writes to both AGENTS.md (for web-based GitHub Copilot) and
44
+ * .github/copilot-instructions.md (for VS Code extension compatibility).
7
45
  */
8
- class CopilotAgent extends AgentsMdAgent_1.AgentsMdAgent {
46
+ class CopilotAgent {
47
+ constructor() {
48
+ this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
49
+ }
9
50
  getIdentifier() {
10
51
  return 'copilot';
11
52
  }
12
53
  getName() {
13
54
  return 'GitHub Copilot';
14
55
  }
56
+ /**
57
+ * Returns multiple output paths to ensure both files are added to .gitignore.
58
+ */
59
+ getDefaultOutputPath(projectRoot) {
60
+ return {
61
+ instructions: path.join(projectRoot, 'AGENTS.md'),
62
+ legacy: path.join(projectRoot, '.github', 'copilot-instructions.md'),
63
+ };
64
+ }
65
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
66
+ // First, write to AGENTS.md using the existing AgentsMdAgent infrastructure
67
+ await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, // No MCP config needed for the instructions file
68
+ {
69
+ // Preserve explicit outputPath precedence semantics if provided
70
+ outputPath: agentConfig?.outputPath || agentConfig?.outputPathInstructions,
71
+ }, backup);
72
+ // Additionally write to .github/copilot-instructions.md for VS Code extension compatibility
73
+ const outputPaths = this.getDefaultOutputPath(projectRoot);
74
+ const legacyPath = path.resolve(projectRoot, outputPaths.legacy);
75
+ // Add marker comment to the content to identify it as generated
76
+ const contentWithMarker = `<!-- Generated by Ruler -->\n${concatenatedRules}`;
77
+ await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(legacyPath));
78
+ // Check if content has changed (idempotency)
79
+ let existingLegacy = null;
80
+ try {
81
+ existingLegacy = await fs_1.promises.readFile(legacyPath, 'utf8');
82
+ }
83
+ catch {
84
+ existingLegacy = null;
85
+ }
86
+ if (existingLegacy === null || existingLegacy !== contentWithMarker) {
87
+ if (backup) {
88
+ await (0, FileSystemUtils_1.backupFile)(legacyPath);
89
+ }
90
+ await (0, FileSystemUtils_1.writeGeneratedFile)(legacyPath, contentWithMarker);
91
+ }
92
+ }
15
93
  getMcpServerKey() {
16
94
  return 'servers';
17
95
  }
@@ -44,13 +44,12 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
44
44
  getName() {
45
45
  return 'Gemini CLI';
46
46
  }
47
- async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
48
- agentConfig) {
47
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
49
48
  // First, perform idempotent write of AGENTS.md via base class
50
49
  await super.applyRulerConfig(concatenatedRules, projectRoot, null, {
51
50
  outputPath: agentConfig?.outputPath,
52
51
  });
53
- // Ensure .gemini/settings.json has contextFileName set to AGENTS.md
52
+ // Prepare .gemini/settings.json with contextFileName and MCP configuration
54
53
  const settingsPath = path.join(projectRoot, '.gemini', 'settings.json');
55
54
  let existingSettings = {};
56
55
  try {
@@ -66,6 +65,23 @@ class GeminiCliAgent extends AgentsMdAgent_1.AgentsMdAgent {
66
65
  ...existingSettings,
67
66
  contextFileName: 'AGENTS.md',
68
67
  };
68
+ // Handle MCP server configuration if provided
69
+ const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
70
+ if (mcpEnabled && rulerMcpJson) {
71
+ const strategy = agentConfig?.mcp?.strategy ?? 'merge';
72
+ if (strategy === 'overwrite') {
73
+ // For overwrite, preserve existing settings except MCP servers
74
+ const incomingServers = rulerMcpJson.mcpServers || {};
75
+ updated[this.getMcpServerKey()] = incomingServers;
76
+ }
77
+ else {
78
+ // For merge strategy, merge with existing MCP servers
79
+ const baseServers = existingSettings[this.getMcpServerKey()] || {};
80
+ const incomingServers = rulerMcpJson.mcpServers || {};
81
+ const mergedServers = { ...baseServers, ...incomingServers };
82
+ updated[this.getMcpServerKey()] = mergedServers;
83
+ }
84
+ }
69
85
  await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
70
86
  await fs_1.promises.writeFile(settingsPath, JSON.stringify(updated, null, 2));
71
87
  }
@@ -37,7 +37,7 @@ exports.loadConfig = loadConfig;
37
37
  const fs_1 = require("fs");
38
38
  const path = __importStar(require("path"));
39
39
  const os = __importStar(require("os"));
40
- const TOML = __importStar(require("toml"));
40
+ const toml_1 = require("@iarna/toml");
41
41
  const zod_1 = require("zod");
42
42
  const constants_1 = require("../constants");
43
43
  const mcpConfigSchema = zod_1.z
@@ -97,7 +97,7 @@ async function loadConfig(options) {
97
97
  let raw = {};
98
98
  try {
99
99
  const text = await fs_1.promises.readFile(configFile, 'utf8');
100
- raw = text.trim() ? TOML.parse(text) : {};
100
+ raw = text.trim() ? (0, toml_1.parse)(text) : {};
101
101
  // Validate the configuration with zod
102
102
  const validationResult = rulerConfigSchema.safeParse(raw);
103
103
  if (!validationResult.success) {
@@ -57,8 +57,9 @@ async function updateGitignore(projectRoot, paths) {
57
57
  throw err;
58
58
  }
59
59
  }
60
- // Convert paths to relative POSIX format
61
- const relativePaths = paths.map((p) => {
60
+ // Convert paths to repo-relative POSIX format with leading /
61
+ const relativePaths = paths
62
+ .map((p) => {
62
63
  let relative;
63
64
  if (path.isAbsolute(p)) {
64
65
  relative = path.relative(projectRoot, p);
@@ -78,6 +79,14 @@ async function updateGitignore(projectRoot, paths) {
78
79
  }
79
80
  }
80
81
  return relative.replace(/\\/g, '/'); // Convert to POSIX format
82
+ })
83
+ .filter((p) => {
84
+ // Never include any path that resides inside a .ruler directory (inputs, not outputs)
85
+ return !p.includes('/.ruler/') && !p.startsWith('.ruler/');
86
+ })
87
+ .map((p) => {
88
+ // Always write full repository-relative paths (prefix with leading /)
89
+ return p.startsWith('/') ? p : `/${p}`;
81
90
  });
82
91
  // Get all existing paths from .gitignore (excluding Ruler block)
83
92
  const existingPaths = getExistingPathsExcludingRulerBlock(existingContent);
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadUnifiedConfig = loadUnifiedConfig;
37
37
  const fs_1 = require("fs");
38
38
  const path = __importStar(require("path"));
39
- const TOML = __importStar(require("toml"));
39
+ const toml_1 = require("@iarna/toml");
40
40
  const hash_1 = require("./hash");
41
41
  const RuleProcessor_1 = require("./RuleProcessor");
42
42
  const FileSystemUtils = __importStar(require("./FileSystemUtils"));
@@ -58,7 +58,7 @@ async function loadUnifiedConfig(options) {
58
58
  : path.join(meta.rulerDir, 'ruler.toml');
59
59
  try {
60
60
  const text = await fs_1.promises.readFile(tomlFile, 'utf8');
61
- tomlRaw = text.trim() ? TOML.parse(text) : {};
61
+ tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
62
62
  meta.configFile = tomlFile;
63
63
  }
64
64
  catch (err) {
@@ -228,11 +228,21 @@ async function loadUnifiedConfig(options) {
228
228
  try {
229
229
  await fs_1.promises.access(mcpFile);
230
230
  mcpJsonExists = true;
231
- console.warn('[ruler] Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
231
+ // Warning is handled by apply-engine to avoid duplication
232
232
  }
233
233
  catch {
234
234
  // file not present
235
235
  }
236
+ // Add deprecation warning if mcp.json exists (regardless of validity)
237
+ if (mcpJsonExists) {
238
+ meta.mcpFile = mcpFile;
239
+ diagnostics.push({
240
+ severity: 'warning',
241
+ code: 'MCP_JSON_DEPRECATED',
242
+ message: 'mcp.json detected: please migrate MCP servers to ruler.toml [mcp_servers.*] sections',
243
+ file: mcpFile,
244
+ });
245
+ }
236
246
  try {
237
247
  if (mcpJsonExists) {
238
248
  const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
@@ -256,14 +266,6 @@ async function loadUnifiedConfig(options) {
256
266
  throw e; // rethrow original error for diagnostics
257
267
  }
258
268
  }
259
- meta.mcpFile = mcpFile;
260
- // Add deprecation warning if mcp.json exists (structured diagnostic)
261
- diagnostics.push({
262
- severity: 'warning',
263
- code: 'MCP_JSON_DEPRECATED',
264
- message: 'mcp.json detected: please migrate MCP servers to ruler.toml [mcp_servers.*] sections',
265
- file: mcpFile,
266
- });
267
269
  const parsedObj = parsed;
268
270
  const serversRaw = parsedObj.mcpServers ||
269
271
  parsedObj.servers ||
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadNestedConfigurations = loadNestedConfigurations;
37
37
  exports.loadSingleConfiguration = loadSingleConfiguration;
38
- exports.selectAgentsToRun = selectAgentsToRun;
39
38
  exports.processHierarchicalConfigurations = processHierarchicalConfigurations;
40
39
  exports.processSingleConfiguration = processSingleConfiguration;
41
40
  exports.applyConfigurationsToAgents = applyConfigurationsToAgents;
@@ -161,52 +160,6 @@ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
161
160
  rulerMcpJson,
162
161
  };
163
162
  }
164
- /**
165
- * Selects the agents to process based on configuration.
166
- * @param allAgents Array of all available agents
167
- * @param config Loaded configuration
168
- * @returns Array of agents to be processed
169
- */
170
- function selectAgentsToRun(allAgents, config) {
171
- // CLI --agents > config.default_agents > per-agent.enabled flags > default all
172
- let selected = allAgents;
173
- if (config.cliAgents && config.cliAgents.length > 0) {
174
- const filters = config.cliAgents.map((n) => n.toLowerCase());
175
- // Check if any of the specified agents don't exist
176
- const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
177
- const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
178
- const invalidAgents = filters.filter((filter) => !validAgentIdentifiers.has(filter) &&
179
- ![...validAgentNames].some((name) => name.includes(filter)));
180
- if (invalidAgents.length > 0) {
181
- throw (0, constants_1.createRulerError)(`Invalid agent specified: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
182
- }
183
- selected = allAgents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
184
- agent.getName().toLowerCase().includes(f)));
185
- }
186
- else if (config.defaultAgents && config.defaultAgents.length > 0) {
187
- const defaults = config.defaultAgents.map((n) => n.toLowerCase());
188
- // Check if any of the default agents don't exist
189
- const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
190
- const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
191
- const invalidAgents = defaults.filter((filter) => !validAgentIdentifiers.has(filter) &&
192
- ![...validAgentNames].some((name) => name.includes(filter)));
193
- if (invalidAgents.length > 0) {
194
- throw (0, constants_1.createRulerError)(`Invalid agent specified in default_agents: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
195
- }
196
- selected = allAgents.filter((agent) => {
197
- const identifier = agent.getIdentifier();
198
- const override = config.agentConfigs[identifier]?.enabled;
199
- if (override !== undefined) {
200
- return override;
201
- }
202
- return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
203
- });
204
- }
205
- else {
206
- selected = allAgents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
207
- }
208
- return selected;
209
- }
210
163
  /**
211
164
  * Processes hierarchical configurations by applying rules to each .ruler directory independently.
212
165
  * Each directory gets its own set of rules and generates its own agent files.
@@ -385,13 +338,21 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
385
338
  (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
386
339
  }
387
340
  else {
388
- if (backup) {
389
- const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
390
- await backupFile(dest);
391
- }
392
341
  const existing = await (0, mcp_1.readNativeMcp)(dest);
393
342
  const merged = (0, merge_1.mergeMcp)(existing, filteredMcpJson, strategy, serverKey);
394
- await (0, mcp_1.writeNativeMcp)(dest, merged);
343
+ // Only backup and write if content would actually change (idempotent)
344
+ const currentContent = JSON.stringify(existing, null, 2);
345
+ const newContent = JSON.stringify(merged, null, 2);
346
+ if (currentContent !== newContent) {
347
+ if (backup) {
348
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
349
+ await backupFile(dest);
350
+ }
351
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
352
+ }
353
+ else {
354
+ (0, constants_1.logVerbose)(`MCP config for ${agent.getName()} is already up to date - skipping backup and write`, verbose);
355
+ }
395
356
  }
396
357
  }
397
358
  /**
@@ -402,7 +363,7 @@ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agent
402
363
  * @param cliGitignoreEnabled CLI gitignore setting
403
364
  * @param dryRun Whether to perform a dry run
404
365
  */
405
- async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun, backup = true) {
366
+ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun) {
406
367
  // Configuration precedence: CLI > TOML > Default (enabled)
407
368
  let gitignoreEnabled;
408
369
  if (cliGitignoreEnabled !== undefined) {
@@ -416,10 +377,8 @@ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignore
416
377
  }
417
378
  if (gitignoreEnabled && generatedPaths.length > 0) {
418
379
  const uniquePaths = [...new Set(generatedPaths)];
419
- // Add wildcard pattern for backup files only if backup is enabled
420
- if (backup) {
421
- uniquePaths.push('*.bak');
422
- }
380
+ // Note: Individual backup patterns are added per-file in the collection phase
381
+ // No need to add a broad *.bak pattern here
423
382
  if (uniquePaths.length > 0) {
424
383
  const prefix = (0, constants_1.actionPrefix)(dryRun);
425
384
  if (dryRun) {
package/dist/lib.js CHANGED
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "allAgents", { enumerable: true, get: function ()
7
7
  const constants_1 = require("./constants");
8
8
  const apply_engine_1 = require("./core/apply-engine");
9
9
  const config_utils_1 = require("./core/config-utils");
10
+ const agent_selection_1 = require("./core/agent-selection");
10
11
  const agents = agents_1.allAgents;
11
12
  /**
12
13
  * Applies ruler configurations for all supported AI agents.
@@ -38,7 +39,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
38
39
  (0, constants_1.logVerbose)(`Loaded ${hierarchicalConfigs.length} .ruler directory configurations`, verbose);
39
40
  (0, constants_1.logVerbose)(`Root configuration has ${Object.keys(rootConfig.agentConfigs).length} agent configs`, verbose);
40
41
  normalizeAgentConfigs(rootConfig, agents);
41
- selectedAgents = (0, apply_engine_1.selectAgentsToRun)(agents, rootConfig);
42
+ selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(rootConfig, agents);
42
43
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
43
44
  generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
44
45
  }
@@ -49,11 +50,11 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
49
50
  (0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(singleConfig.config.agentConfigs).length} agent configs`, verbose);
50
51
  (0, constants_1.logVerbose)(`Found .ruler directory with ${singleConfig.concatenatedRules.length} characters of rules`, verbose);
51
52
  normalizeAgentConfigs(singleConfig.config, agents);
52
- selectedAgents = (0, apply_engine_1.selectAgentsToRun)(agents, singleConfig.config);
53
+ selectedAgents = (0, agent_selection_1.resolveSelectedAgents)(singleConfig.config, agents);
53
54
  (0, constants_1.logVerbose)(`Selected ${selectedAgents.length} agents: ${selectedAgents.map((a) => a.getName()).join(', ')}`, verbose);
54
55
  generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
55
56
  }
56
- await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun, backup);
57
+ await (0, apply_engine_1.updateGitignore)(projectRoot, generatedPaths, loadedConfig, cliGitignoreEnabled, dryRun);
57
58
  }
58
59
  /**
59
60
  * Normalizes per-agent config keys to agent identifiers for consistent lookup.
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.propagateMcpToOpenHands = propagateMcpToOpenHands;
37
37
  const fs = __importStar(require("fs/promises"));
38
- const TOML = __importStar(require("toml"));
39
38
  const toml_1 = require("@iarna/toml");
40
39
  const FileSystemUtils_1 = require("../core/FileSystemUtils");
41
40
  const path = __importStar(require("path"));
@@ -103,7 +102,7 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
103
102
  let config = {};
104
103
  try {
105
104
  const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
106
- config = TOML.parse(tomlContent);
105
+ config = (0, toml_1.parse)(tomlContent);
107
106
  }
108
107
  catch {
109
108
  // File doesn't exist, we'll create it.
package/dist/paths/mcp.js CHANGED
@@ -36,12 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getNativeMcpPath = getNativeMcpPath;
37
37
  exports.readNativeMcp = readNativeMcp;
38
38
  exports.writeNativeMcp = writeNativeMcp;
39
- const os = __importStar(require("os"));
40
39
  const path = __importStar(require("path"));
41
40
  const fs_1 = require("fs");
42
41
  /** Determine the native MCP config path for a given agent. */
43
42
  async function getNativeMcpPath(adapterName, projectRoot) {
44
- const home = os.homedir();
45
43
  const candidates = [];
46
44
  switch (adapterName) {
47
45
  case 'GitHub Copilot':
@@ -53,16 +51,15 @@ async function getNativeMcpPath(adapterName, projectRoot) {
53
51
  break;
54
52
  case 'Cursor':
55
53
  candidates.push(path.join(projectRoot, '.cursor', 'mcp.json'));
56
- candidates.push(path.join(home, '.cursor', 'mcp.json'));
57
54
  break;
58
55
  case 'Windsurf':
59
- candidates.push(path.join(home, '.codeium', 'windsurf', 'mcp_config.json'));
56
+ candidates.push(path.join(projectRoot, '.windsurf', 'mcp_config.json'));
60
57
  break;
61
58
  case 'Claude Code':
62
59
  candidates.push(path.join(projectRoot, '.mcp.json'));
63
60
  break;
64
61
  case 'OpenAI Codex CLI':
65
- candidates.push(path.join(home, '.codex', 'config.json'));
62
+ candidates.push(path.join(projectRoot, '.codex', 'config.json'));
66
63
  break;
67
64
  case 'Aider':
68
65
  candidates.push(path.join(projectRoot, '.mcp.json'));
@@ -82,7 +79,6 @@ async function getNativeMcpPath(adapterName, projectRoot) {
82
79
  break;
83
80
  case 'OpenCode':
84
81
  candidates.push(path.join(projectRoot, 'opencode.json'));
85
- candidates.push(path.join(home, '.config', 'opencode', 'opencode.json'));
86
82
  break;
87
83
  case 'Zed':
88
84
  // Only consider project-local Zed settings (avoid writing to user home directory)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -62,7 +62,6 @@
62
62
  "dependencies": {
63
63
  "@iarna/toml": "^2.2.5",
64
64
  "js-yaml": "^4.1.0",
65
- "toml": "^3.0.0",
66
65
  "yargs": "^17.7.2",
67
66
  "zod": "^3.25.28"
68
67
  }