@intellectronica/ruler 0.3.41 → 0.3.43

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.
Files changed (105) hide show
  1. package/README.md +135 -36
  2. package/dist/agents/AbstractAgent.d.ts +53 -0
  3. package/dist/agents/AbstractAgent.js +3 -2
  4. package/dist/agents/AgentsMdAgent.d.ts +14 -0
  5. package/dist/agents/AgentsMdAgent.js +3 -2
  6. package/dist/agents/AiderAgent.d.ts +14 -0
  7. package/dist/agents/AiderAgent.js +7 -4
  8. package/dist/agents/AmazonQCliAgent.d.ts +13 -0
  9. package/dist/agents/AmazonQCliAgent.js +6 -4
  10. package/dist/agents/AmpAgent.d.ts +6 -0
  11. package/dist/agents/AntigravityAgent.d.ts +10 -0
  12. package/dist/agents/AugmentCodeAgent.d.ts +13 -0
  13. package/dist/agents/AugmentCodeAgent.js +3 -2
  14. package/dist/agents/ClaudeAgent.d.ts +13 -0
  15. package/dist/agents/ClineAgent.d.ts +9 -0
  16. package/dist/agents/CodexCliAgent.d.ts +31 -0
  17. package/dist/agents/CodexCliAgent.js +1 -1
  18. package/dist/agents/CopilotAgent.d.ts +20 -0
  19. package/dist/agents/CrushAgent.d.ts +14 -0
  20. package/dist/agents/CrushAgent.js +18 -6
  21. package/dist/agents/CursorAgent.d.ts +17 -0
  22. package/dist/agents/FactoryDroidAgent.d.ts +13 -0
  23. package/dist/agents/FirebaseAgent.d.ts +11 -0
  24. package/dist/agents/FirebenderAgent.d.ts +36 -0
  25. package/dist/agents/FirebenderAgent.js +5 -4
  26. package/dist/agents/GeminiCliAgent.d.ts +12 -0
  27. package/dist/agents/GeminiCliAgent.js +13 -7
  28. package/dist/agents/GooseAgent.d.ts +12 -0
  29. package/dist/agents/IAgent.d.ts +74 -0
  30. package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
  31. package/dist/agents/JulesAgent.d.ts +5 -0
  32. package/dist/agents/JunieAgent.d.ts +12 -0
  33. package/dist/agents/KiloCodeAgent.d.ts +14 -0
  34. package/dist/agents/KiroAgent.d.ts +8 -0
  35. package/dist/agents/MistralVibeAgent.d.ts +31 -0
  36. package/dist/agents/MistralVibeAgent.js +14 -3
  37. package/dist/agents/OpenCodeAgent.d.ts +11 -0
  38. package/dist/agents/OpenCodeAgent.js +24 -12
  39. package/dist/agents/OpenHandsAgent.d.ts +8 -0
  40. package/dist/agents/PiAgent.d.ts +9 -0
  41. package/dist/agents/QwenCodeAgent.d.ts +11 -0
  42. package/dist/agents/QwenCodeAgent.js +11 -5
  43. package/dist/agents/RooCodeAgent.d.ts +16 -0
  44. package/dist/agents/RooCodeAgent.js +3 -2
  45. package/dist/agents/TraeAgent.d.ts +10 -0
  46. package/dist/agents/WarpAgent.d.ts +12 -0
  47. package/dist/agents/WindsurfAgent.d.ts +13 -0
  48. package/dist/agents/ZedAgent.d.ts +21 -0
  49. package/dist/agents/ZedAgent.js +8 -5
  50. package/dist/agents/agent-utils.d.ts +5 -0
  51. package/dist/agents/agent-utils.js +8 -5
  52. package/dist/agents/index.d.ts +9 -0
  53. package/dist/cli/commands.d.ts +4 -0
  54. package/dist/cli/commands.js +1 -2
  55. package/dist/cli/handlers.d.ts +41 -0
  56. package/dist/cli/handlers.js +75 -59
  57. package/dist/cli/index.d.ts +2 -0
  58. package/dist/constants.d.ts +35 -0
  59. package/dist/constants.js +1 -1
  60. package/dist/core/ConfigLoader.d.ts +59 -0
  61. package/dist/core/ConfigLoader.js +178 -44
  62. package/dist/core/FileSystemUtils.d.ts +53 -0
  63. package/dist/core/FileSystemUtils.js +157 -20
  64. package/dist/core/GitignoreUtils.d.ts +25 -0
  65. package/dist/core/GitignoreUtils.js +94 -32
  66. package/dist/core/RuleProcessor.d.ts +8 -0
  67. package/dist/core/SkillsProcessor.d.ts +127 -0
  68. package/dist/core/SkillsProcessor.js +118 -223
  69. package/dist/core/SkillsUtils.d.ts +26 -0
  70. package/dist/core/SubagentsProcessor.d.ts +38 -0
  71. package/dist/core/SubagentsProcessor.js +8 -5
  72. package/dist/core/SubagentsUtils.d.ts +34 -0
  73. package/dist/core/UnifiedConfigLoader.d.ts +10 -0
  74. package/dist/core/UnifiedConfigLoader.js +115 -33
  75. package/dist/core/UnifiedConfigTypes.d.ts +97 -0
  76. package/dist/core/agent-selection.d.ts +12 -0
  77. package/dist/core/agent-selection.js +17 -7
  78. package/dist/core/apply-engine.d.ts +70 -0
  79. package/dist/core/apply-engine.js +88 -58
  80. package/dist/core/config-utils.d.ts +14 -0
  81. package/dist/core/config-utils.js +9 -3
  82. package/dist/core/hash.d.ts +2 -0
  83. package/dist/core/path-utils.d.ts +1 -0
  84. package/dist/core/path-utils.js +42 -0
  85. package/dist/core/revert-engine.d.ts +37 -0
  86. package/dist/core/revert-engine.js +142 -34
  87. package/dist/lib.d.ts +13 -0
  88. package/dist/lib.js +24 -8
  89. package/dist/mcp/capabilities.d.ts +20 -0
  90. package/dist/mcp/merge.d.ts +10 -0
  91. package/dist/mcp/merge.js +36 -16
  92. package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
  93. package/dist/mcp/propagateOpenCodeMcp.js +30 -11
  94. package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
  95. package/dist/mcp/propagateOpenHandsMcp.js +48 -21
  96. package/dist/mcp/validate.d.ts +7 -0
  97. package/dist/mcp/validate.js +6 -1
  98. package/dist/paths/mcp.d.ts +8 -0
  99. package/dist/paths/mcp.js +44 -8
  100. package/dist/revert.d.ts +6 -0
  101. package/dist/revert.js +58 -46
  102. package/dist/types.d.ts +87 -0
  103. package/dist/vscode/settings.d.ts +40 -0
  104. package/dist/vscode/settings.js +3 -3
  105. package/package.json +8 -5
@@ -35,12 +35,29 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.revertAgentConfiguration = revertAgentConfiguration;
37
37
  exports.cleanUpAuxiliaryFiles = cleanUpAuxiliaryFiles;
38
+ exports.cleanUpAgentDirectories = cleanUpAgentDirectories;
38
39
  const path = __importStar(require("path"));
39
40
  const fs_1 = require("fs");
40
41
  const agent_utils_1 = require("../agents/agent-utils");
41
42
  const mcp_1 = require("../paths/mcp");
42
43
  const constants_1 = require("../constants");
44
+ const GitignoreUtils_1 = require("./GitignoreUtils");
43
45
  const settings_1 = require("../vscode/settings");
46
+ const path_utils_1 = require("./path-utils");
47
+ const FileSystemUtils_1 = require("./FileSystemUtils");
48
+ const RULER_START_MARKER = '# START Ruler Generated Files';
49
+ const RULER_END_MARKER = '# END Ruler Generated Files';
50
+ const RULER_GENERATED_MARKER = '<!-- Generated by Ruler -->';
51
+ const RULER_SOURCE_MARKER_PREFIXES = [
52
+ '<!-- Source: .ruler/',
53
+ '<!-- Source: ruler/',
54
+ ];
55
+ async function resolveMcpPathForRevert(agent, projectRoot, agentConfig) {
56
+ if (agentConfig?.outputPathConfig) {
57
+ return path.resolve(projectRoot, agentConfig.outputPathConfig);
58
+ }
59
+ return await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
60
+ }
44
61
  /**
45
62
  * Checks if a file exists.
46
63
  */
@@ -53,10 +70,62 @@ async function fileExists(filePath) {
53
70
  return false;
54
71
  }
55
72
  }
73
+ async function ignoreFileHasRulerGeneratedPath(ignoreFilePath, generatedPath) {
74
+ try {
75
+ const content = await fs_1.promises.readFile(ignoreFilePath, 'utf8');
76
+ const lines = content.split('\n');
77
+ let inRulerBlock = false;
78
+ for (const line of lines) {
79
+ const trimmed = line.trim();
80
+ if (trimmed === RULER_START_MARKER) {
81
+ inRulerBlock = true;
82
+ continue;
83
+ }
84
+ if (trimmed === RULER_END_MARKER) {
85
+ inRulerBlock = false;
86
+ continue;
87
+ }
88
+ if (inRulerBlock &&
89
+ (trimmed === generatedPath || trimmed === generatedPath.slice(1))) {
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ return false;
98
+ }
99
+ async function hasRulerGeneratedProvenance(filePath, projectRoot) {
100
+ try {
101
+ const content = await fs_1.promises.readFile(filePath, 'utf8');
102
+ if (content.startsWith(RULER_GENERATED_MARKER)) {
103
+ return true;
104
+ }
105
+ const trimmedContent = content.trimStart();
106
+ if (RULER_SOURCE_MARKER_PREFIXES.some((prefix) => trimmedContent.startsWith(prefix))) {
107
+ return true;
108
+ }
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ const relativePath = `/${path.relative(projectRoot, filePath).replace(/\\/g, '/')}`;
114
+ const ignoreFiles = [
115
+ await (0, GitignoreUtils_1.resolveIgnoreFilePath)(projectRoot, '.gitignore'),
116
+ await (0, GitignoreUtils_1.resolveIgnoreFilePath)(projectRoot, '.git/info/exclude'),
117
+ ];
118
+ for (const ignoreFile of ignoreFiles) {
119
+ if (await ignoreFileHasRulerGeneratedPath(ignoreFile, relativePath)) {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
56
125
  /**
57
126
  * Restores a file from its backup if the backup exists.
58
127
  */
59
- async function restoreFromBackup(filePath, verbose, dryRun) {
128
+ async function restoreFromBackup(filePath, verbose, dryRun, projectRoot) {
60
129
  const backupPath = `${filePath}.bak`;
61
130
  const backupExists = await fileExists(backupPath);
62
131
  if (!backupExists) {
@@ -68,6 +137,10 @@ async function restoreFromBackup(filePath, verbose, dryRun) {
68
137
  (0, constants_1.logVerbose)(`${prefix} Would restore: ${filePath} from backup`, verbose);
69
138
  }
70
139
  else {
140
+ if (projectRoot) {
141
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to restore backup through symlinked output path');
142
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to restore from backup through symlinked backup path');
143
+ }
71
144
  await fs_1.promises.copyFile(backupPath, filePath);
72
145
  (0, constants_1.logVerbose)(`${prefix} Restored: ${filePath} from backup`, verbose);
73
146
  }
@@ -76,7 +149,7 @@ async function restoreFromBackup(filePath, verbose, dryRun) {
76
149
  /**
77
150
  * Removes a file if it exists and has no backup (meaning it was generated by ruler).
78
151
  */
79
- async function removeGeneratedFile(filePath, verbose, dryRun) {
152
+ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
80
153
  const fileExistsFlag = await fileExists(filePath);
81
154
  const backupExists = await fileExists(`${filePath}.bak`);
82
155
  if (!fileExistsFlag) {
@@ -87,11 +160,19 @@ async function removeGeneratedFile(filePath, verbose, dryRun) {
87
160
  (0, constants_1.logVerbose)(`File has backup, skipping removal: ${filePath}`, verbose);
88
161
  return false;
89
162
  }
163
+ if (projectRoot &&
164
+ !(await hasRulerGeneratedProvenance(filePath, projectRoot))) {
165
+ (0, constants_1.logVerbose)(`Preserving file without backup or Ruler provenance: ${filePath}`, verbose);
166
+ return false;
167
+ }
90
168
  const prefix = (0, constants_1.actionPrefix)(dryRun);
91
169
  if (dryRun) {
92
170
  (0, constants_1.logVerbose)(`${prefix} Would remove generated file: ${filePath}`, verbose);
93
171
  }
94
172
  else {
173
+ if (projectRoot) {
174
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to remove generated file through symlinked path');
175
+ }
95
176
  await fs_1.promises.unlink(filePath);
96
177
  (0, constants_1.logVerbose)(`${prefix} Removed generated file: ${filePath}`, verbose);
97
178
  }
@@ -100,7 +181,7 @@ async function removeGeneratedFile(filePath, verbose, dryRun) {
100
181
  /**
101
182
  * Removes backup files.
102
183
  */
103
- async function removeBackupFile(filePath, verbose, dryRun) {
184
+ async function removeBackupFile(filePath, verbose, dryRun, projectRoot) {
104
185
  const backupPath = `${filePath}.bak`;
105
186
  const backupExists = await fileExists(backupPath);
106
187
  if (!backupExists) {
@@ -111,6 +192,9 @@ async function removeBackupFile(filePath, verbose, dryRun) {
111
192
  (0, constants_1.logVerbose)(`${prefix} Would remove backup file: ${backupPath}`, verbose);
112
193
  }
113
194
  else {
195
+ if (projectRoot) {
196
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to remove backup file through symlinked path');
197
+ }
114
198
  await fs_1.promises.unlink(backupPath);
115
199
  (0, constants_1.logVerbose)(`${prefix} Removed backup file: ${backupPath}`, verbose);
116
200
  }
@@ -262,11 +346,14 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
262
346
  }
263
347
  const backupExists = await fileExists(`${fullPath}.bak`);
264
348
  if (backupExists) {
265
- const restored = await restoreFromBackup(fullPath, verbose, dryRun);
349
+ const restored = await restoreFromBackup(fullPath, verbose, dryRun, projectRoot);
266
350
  if (restored) {
267
351
  filesRemoved++;
268
352
  }
269
353
  }
354
+ else if (!(await hasRulerGeneratedProvenance(fullPath, projectRoot))) {
355
+ (0, constants_1.logVerbose)(`Preserving additional file without backup or Ruler provenance: ${fullPath}`, verbose);
356
+ }
270
357
  else {
271
358
  if (dryRun) {
272
359
  (0, constants_1.logVerbose)(`${prefix} Would remove additional file: ${fullPath}`, verbose);
@@ -285,10 +372,10 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
285
372
  const settingsPath = (0, settings_1.getVSCodeSettingsPath)(projectRoot);
286
373
  const backupPath = `${settingsPath}.bak`;
287
374
  if (await fileExists(backupPath)) {
288
- const restored = await restoreFromBackup(settingsPath, verbose, dryRun);
375
+ const restored = await restoreFromBackup(settingsPath, verbose, dryRun, projectRoot);
289
376
  if (restored) {
290
377
  filesRemoved++;
291
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Restored VSCode settings from backup`, verbose);
378
+ (0, constants_1.logVerbose)(`${prefix} Restored VSCode settings from backup`, verbose);
292
379
  }
293
380
  }
294
381
  else if (await fileExists(settingsPath)) {
@@ -299,10 +386,10 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
299
386
  delete settings['augment.advanced'];
300
387
  const remainingKeys = Object.keys(settings);
301
388
  if (remainingKeys.length === 0) {
302
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Would remove empty VSCode settings file`, verbose);
389
+ (0, constants_1.logVerbose)(`${prefix} Would remove empty VSCode settings file`, verbose);
303
390
  }
304
391
  else {
305
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
392
+ (0, constants_1.logVerbose)(`${prefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
306
393
  }
307
394
  filesRemoved++;
308
395
  }
@@ -313,12 +400,13 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
313
400
  delete settings['augment.advanced'];
314
401
  const remainingKeys = Object.keys(settings);
315
402
  if (remainingKeys.length === 0) {
403
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(settingsPath, projectRoot, 'Refusing to remove VSCode settings through symlinked path');
316
404
  await fs_1.promises.unlink(settingsPath);
317
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Removed empty VSCode settings file`, verbose);
405
+ (0, constants_1.logVerbose)(`${prefix} Removed empty VSCode settings file`, verbose);
318
406
  }
319
407
  else {
320
- await (0, settings_1.writeVSCodeSettings)(settingsPath, settings);
321
- (0, constants_1.logVerbose)(`${constants_1.actionPrefix} Removed augment.advanced section from VSCode settings`, verbose);
408
+ await (0, settings_1.writeVSCodeSettings)(settingsPath, settings, projectRoot);
409
+ (0, constants_1.logVerbose)(`${prefix} Removed augment.advanced section from VSCode settings`, verbose);
322
410
  }
323
411
  filesRemoved++;
324
412
  }
@@ -338,7 +426,7 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
338
426
  * @param agent The agent to revert
339
427
  * @param projectRoot Root directory of the project
340
428
  * @param agentConfig Agent-specific configuration
341
- * @param keepBackups Whether to keep backup files
429
+ * @param keepBackups Whether restored backup files should be preserved
342
430
  * @param verbose Whether to enable verbose logging
343
431
  * @param dryRun Whether to perform a dry run
344
432
  * @returns Promise resolving to revert statistics
@@ -350,49 +438,44 @@ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBac
350
438
  backupsRemoved: 0,
351
439
  };
352
440
  const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
441
+ const processedPaths = new Set();
353
442
  (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
354
- for (const outputPath of outputPaths) {
355
- const restored = await restoreFromBackup(outputPath, verbose, dryRun);
443
+ const processPath = async (outputPath) => {
444
+ const resolvedPath = path.resolve(projectRoot, outputPath);
445
+ if (processedPaths.has(resolvedPath)) {
446
+ (0, constants_1.logVerbose)(`Skipping already processed path: ${outputPath}`, verbose);
447
+ return;
448
+ }
449
+ processedPaths.add(resolvedPath);
450
+ const restored = await restoreFromBackup(outputPath, verbose, dryRun, projectRoot);
356
451
  if (restored) {
357
452
  result.restored++;
358
453
  if (!keepBackups) {
359
- const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun);
454
+ const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun, projectRoot);
360
455
  if (backupRemoved) {
361
456
  result.backupsRemoved++;
362
457
  }
363
458
  }
364
459
  }
365
460
  else {
366
- const removed = await removeGeneratedFile(outputPath, verbose, dryRun);
461
+ const removed = await removeGeneratedFile(outputPath, verbose, dryRun, projectRoot);
367
462
  if (removed) {
368
463
  result.removed++;
369
464
  }
370
465
  }
466
+ };
467
+ for (const outputPath of outputPaths) {
468
+ await processPath(outputPath);
371
469
  }
372
470
  // Handle MCP files
373
- const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
374
- if (mcpPath && mcpPath.startsWith(projectRoot)) {
471
+ const mcpPath = await resolveMcpPathForRevert(agent, projectRoot, agentConfig);
472
+ if (mcpPath && (0, path_utils_1.isPathInsideOrEqual)(projectRoot, mcpPath)) {
375
473
  if (agent.getName() === 'AugmentCode' &&
376
474
  mcpPath.endsWith('.vscode/settings.json')) {
377
475
  (0, constants_1.logVerbose)(`Skipping MCP handling for AugmentCode settings.json - handled separately`, verbose);
378
476
  }
379
477
  else {
380
- const mcpRestored = await restoreFromBackup(mcpPath, verbose, dryRun);
381
- if (mcpRestored) {
382
- result.restored++;
383
- if (!keepBackups) {
384
- const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun);
385
- if (mcpBackupRemoved) {
386
- result.backupsRemoved++;
387
- }
388
- }
389
- }
390
- else {
391
- const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun);
392
- if (mcpRemoved) {
393
- result.removed++;
394
- }
395
- }
478
+ await processPath(mcpPath);
396
479
  }
397
480
  }
398
481
  return result;
@@ -412,3 +495,28 @@ async function cleanUpAuxiliaryFiles(projectRoot, verbose, dryRun) {
412
495
  directoriesRemoved,
413
496
  };
414
497
  }
498
+ async function cleanUpAgentDirectories(agent, projectRoot, agentConfig, verbose, dryRun) {
499
+ const candidateFiles = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
500
+ const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
501
+ if (mcpPath && (0, path_utils_1.isPathInsideOrEqual)(projectRoot, mcpPath)) {
502
+ candidateFiles.push(mcpPath);
503
+ }
504
+ const candidateDirs = [
505
+ ...new Set(candidateFiles.map((filePath) => path.dirname(filePath))),
506
+ ];
507
+ let directoriesRemoved = 0;
508
+ const resolvedProjectRoot = path.resolve(projectRoot);
509
+ for (const candidateDir of candidateDirs) {
510
+ let currentDir = path.resolve(candidateDir);
511
+ while (currentDir !== resolvedProjectRoot &&
512
+ (0, path_utils_1.isPathInsideOrEqual)(resolvedProjectRoot, currentDir)) {
513
+ const removed = await removeEmptyDirectory(currentDir, verbose, dryRun);
514
+ if (!removed) {
515
+ break;
516
+ }
517
+ directoriesRemoved++;
518
+ currentDir = path.dirname(currentDir);
519
+ }
520
+ }
521
+ return directoriesRemoved;
522
+ }
package/dist/lib.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { allAgents } from './agents';
2
+ import { McpStrategy } from './types';
3
+ export { allAgents };
4
+ /**
5
+ * Applies ruler configurations for all supported AI agents.
6
+ * @param projectRoot Root directory of the project
7
+ */
8
+ /**
9
+ * Applies ruler configurations for selected AI agents.
10
+ * @param projectRoot Root directory of the project
11
+ * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
12
+ */
13
+ export declare function applyAllAgentConfigs(projectRoot: string, includedAgents?: string[], configPath?: string, cliMcpEnabled?: boolean, cliMcpStrategy?: McpStrategy, cliGitignoreEnabled?: boolean, verbose?: boolean, dryRun?: boolean, localOnly?: boolean, nested?: boolean, backup?: boolean, skillsEnabled?: boolean, cliGitignoreLocal?: boolean, subagentsEnabled?: boolean): Promise<void>;
package/dist/lib.js CHANGED
@@ -53,6 +53,17 @@ function resolveSkillsEnabled(cliFlag, configSetting) {
53
53
  ? configSetting
54
54
  : true; // default to enabled
55
55
  }
56
+ /**
57
+ * Resolves backup enabled state based on precedence:
58
+ * CLI flag > ruler.toml > default (enabled).
59
+ */
60
+ function resolveBackupEnabled(cliFlag, configSetting) {
61
+ return cliFlag !== undefined
62
+ ? cliFlag
63
+ : configSetting !== undefined
64
+ ? configSetting
65
+ : true; // default to enabled
66
+ }
56
67
  /**
57
68
  * Resolves subagents enabled state based on precedence:
58
69
  * CLI flag > ruler.toml > default (disabled).
@@ -82,7 +93,7 @@ function resolveSubagentsCleanupOrphaned(configSetting) {
82
93
  * @param projectRoot Root directory of the project
83
94
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
84
95
  */
85
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup = true, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
96
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false, nested = false, backup, skillsEnabled, cliGitignoreLocal, subagentsEnabled) {
86
97
  // Load configuration and rules
87
98
  (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
88
99
  if (configPath) {
@@ -91,6 +102,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
91
102
  let selectedAgents;
92
103
  let generatedPaths;
93
104
  let loadedConfig;
105
+ let outputProjectRoot = projectRoot;
94
106
  if (nested) {
95
107
  const hierarchicalConfigs = await (0, apply_engine_1.loadNestedConfigurations)(projectRoot, configPath, localOnly, nested);
96
108
  if (hierarchicalConfigs.length === 0) {
@@ -123,6 +135,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
123
135
  // Propagate subagents (mirrors skills handling for nested mode).
124
136
  const subagentsEnabledResolved = resolveSubagentsEnabled(subagentsEnabled, rootConfig.subagents?.enabled);
125
137
  const subagentsCleanupOrphaned = resolveSubagentsCleanupOrphaned(rootConfig.subagents?.cleanup_orphaned);
138
+ const backupEnabledResolved = resolveBackupEnabled(backup, rootConfig.backup?.enabled);
126
139
  {
127
140
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
128
141
  for (const configEntry of hierarchicalConfigs) {
@@ -131,10 +144,12 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
131
144
  await propagateSubagents(nestedRoot, selectedAgents, subagentsEnabledResolved, subagentsCleanupOrphaned, verbose, dryRun);
132
145
  }
133
146
  }
134
- generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
147
+ generatedPaths = await (0, apply_engine_1.processHierarchicalConfigurations)(selectedAgents, hierarchicalConfigs, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backupEnabledResolved);
135
148
  }
136
149
  else {
137
150
  const singleConfig = await (0, apply_engine_1.loadSingleConfiguration)(projectRoot, configPath, localOnly);
151
+ const singleProjectRoot = singleConfig.projectRoot;
152
+ outputProjectRoot = singleProjectRoot;
138
153
  loadedConfig = singleConfig.config;
139
154
  singleConfig.config.cliAgents = includedAgents;
140
155
  (0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(singleConfig.config.agentConfigs).length} agent configs`, verbose);
@@ -146,16 +161,17 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
146
161
  const skillsEnabledResolved = resolveSkillsEnabled(skillsEnabled, singleConfig.config.skills?.enabled);
147
162
  if (skillsEnabledResolved) {
148
163
  const { propagateSkills } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
149
- await propagateSkills(projectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
164
+ await propagateSkills(singleProjectRoot, selectedAgents, skillsEnabledResolved, verbose, dryRun);
150
165
  }
151
166
  // Propagate subagents (mirrors skills handling).
152
167
  const subagentsEnabledResolvedSingle = resolveSubagentsEnabled(subagentsEnabled, singleConfig.config.subagents?.enabled);
153
168
  const subagentsCleanupOrphanedSingle = resolveSubagentsCleanupOrphaned(singleConfig.config.subagents?.cleanup_orphaned);
169
+ const backupEnabledResolvedSingle = resolveBackupEnabled(backup, singleConfig.config.backup?.enabled);
154
170
  {
155
171
  const { propagateSubagents } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
156
- await propagateSubagents(projectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
172
+ await propagateSubagents(singleProjectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
157
173
  }
158
- generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup);
174
+ generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig, singleProjectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backupEnabledResolvedSingle);
159
175
  }
160
176
  // Add skills-generated paths to gitignore if skills are enabled
161
177
  let allGeneratedPaths = generatedPaths;
@@ -163,17 +179,17 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
163
179
  if (skillsEnabledForGitignore) {
164
180
  // Skills enabled by default or explicitly
165
181
  const { getSkillsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SkillsProcessor')));
166
- const skillsPaths = await getSkillsGitignorePaths(projectRoot, selectedAgents);
182
+ const skillsPaths = await getSkillsGitignorePaths(outputProjectRoot, selectedAgents);
167
183
  allGeneratedPaths = [...allGeneratedPaths, ...skillsPaths];
168
184
  }
169
185
  // Add subagents-generated paths to gitignore if subagents are enabled.
170
186
  const subagentsEnabledForGitignore = resolveSubagentsEnabled(subagentsEnabled, loadedConfig.subagents?.enabled);
171
187
  if (subagentsEnabledForGitignore) {
172
188
  const { getSubagentsGitignorePaths } = await Promise.resolve().then(() => __importStar(require('./core/SubagentsProcessor')));
173
- const subagentPaths = await getSubagentsGitignorePaths(projectRoot, selectedAgents);
189
+ const subagentPaths = await getSubagentsGitignorePaths(outputProjectRoot, selectedAgents);
174
190
  allGeneratedPaths = [...allGeneratedPaths, ...subagentPaths];
175
191
  }
176
- await (0, apply_engine_1.updateGitignore)(projectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun, cliGitignoreLocal);
192
+ await (0, apply_engine_1.updateGitignore)(outputProjectRoot, allGeneratedPaths, loadedConfig, cliGitignoreEnabled, dryRun, cliGitignoreLocal);
177
193
  }
178
194
  /**
179
195
  * Normalizes per-agent config keys to agent identifiers for consistent lookup.
@@ -0,0 +1,20 @@
1
+ import { IAgent } from '../agents/IAgent';
2
+ /**
3
+ * MCP capability types for agents
4
+ */
5
+ export interface McpCapabilities {
6
+ supportsStdio: boolean;
7
+ supportsRemote: boolean;
8
+ }
9
+ /**
10
+ * Derives MCP capabilities for an agent
11
+ */
12
+ export declare function getAgentMcpCapabilities(agent: IAgent): McpCapabilities;
13
+ /**
14
+ * Checks if an agent supports any MCP functionality
15
+ */
16
+ export declare function agentSupportsMcp(agent: IAgent): boolean;
17
+ /**
18
+ * Filters MCP configuration based on agent capabilities
19
+ */
20
+ export declare function filterMcpConfigForAgent(mcpConfig: Record<string, unknown>, agent: IAgent): Record<string, unknown> | null;
@@ -0,0 +1,10 @@
1
+ import { McpStrategy } from '../types';
2
+ /**
3
+ * Merge native and incoming MCP server configurations according to strategy.
4
+ * @param base Existing native MCP config object.
5
+ * @param incoming Ruler MCP config object.
6
+ * @param strategy Merge strategy: 'merge' to union servers, 'overwrite' to replace.
7
+ * @param serverKey The key to use for servers in the output (e.g., 'servers' for Copilot, 'mcpServers' for others).
8
+ * @returns Merged MCP config object.
9
+ */
10
+ export declare function mergeMcp(base: Record<string, unknown>, incoming: Record<string, unknown>, strategy: McpStrategy, serverKey: string): Record<string, unknown>;
package/dist/mcp/merge.js CHANGED
@@ -1,6 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeMcp = mergeMcp;
4
+ const MCP_SERVER_KEYS = [
5
+ 'mcp',
6
+ 'mcpServers',
7
+ 'servers',
8
+ 'mcp_servers',
9
+ 'context_servers',
10
+ ];
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function collectMcpServers(config, serverKey) {
15
+ const servers = {};
16
+ const aliases = MCP_SERVER_KEYS.filter((key) => key !== serverKey);
17
+ for (const key of [...aliases, serverKey]) {
18
+ const value = config[key];
19
+ if (isRecord(value)) {
20
+ Object.assign(servers, value);
21
+ }
22
+ }
23
+ return servers;
24
+ }
25
+ function removeServerAliases(config, serverKey) {
26
+ const result = { ...config };
27
+ for (const key of MCP_SERVER_KEYS) {
28
+ if (key !== serverKey) {
29
+ delete result[key];
30
+ }
31
+ }
32
+ return result;
33
+ }
4
34
  /**
5
35
  * Merge native and incoming MCP server configurations according to strategy.
6
36
  * @param base Existing native MCP config object.
@@ -11,27 +41,17 @@ exports.mergeMcp = mergeMcp;
11
41
  */
12
42
  function mergeMcp(base, incoming, strategy, serverKey) {
13
43
  if (strategy === 'overwrite') {
14
- // Ensure the incoming object uses the correct server key.
15
- // Transform from the standard (Crush) MCP config format
16
- const incomingServers = incoming[serverKey] ||
17
- incoming.mcpServers ||
18
- incoming.mcp ||
19
- {};
44
+ const incomingServers = collectMcpServers(incoming, serverKey);
45
+ const preservedBase = removeServerAliases(base, serverKey);
20
46
  return {
47
+ ...preservedBase,
21
48
  [serverKey]: incomingServers,
22
49
  };
23
50
  }
24
- const baseServers = base[serverKey] ||
25
- base.mcpServers ||
26
- base.mcp ||
27
- {};
28
- const incomingServers = incoming[serverKey] ||
29
- incoming.mcpServers ||
30
- incoming.mcp ||
31
- {};
51
+ const baseServers = collectMcpServers(base, serverKey);
52
+ const incomingServers = collectMcpServers(incoming, serverKey);
32
53
  const mergedServers = { ...baseServers, ...incomingServers };
33
- const newBase = { ...base };
34
- delete newBase.mcpServers; // Remove old key if present
54
+ const newBase = removeServerAliases(base, serverKey);
35
55
  return {
36
56
  ...newBase,
37
57
  [serverKey]: mergedServers,
@@ -0,0 +1,2 @@
1
+ import { McpStrategy } from '../types';
2
+ export declare function propagateMcpToOpenCode(rulerMcpData: Record<string, unknown> | null, openCodeConfigPath: string, backup?: boolean, strategy?: McpStrategy, containmentRoot?: string): Promise<void>;
@@ -91,16 +91,29 @@ function transformToOpenCodeFormat(rulerMcp) {
91
91
  mcp: openCodeServers,
92
92
  };
93
93
  }
94
- async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true) {
94
+ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup = true, strategy = 'merge', containmentRoot) {
95
95
  const rulerMcp = rulerMcpData || {};
96
+ if (containmentRoot) {
97
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(openCodeConfigPath, containmentRoot, 'Refusing to write generated file outside project');
98
+ }
96
99
  // Read existing OpenCode config if it exists
97
100
  let existingConfig = {};
101
+ let existingContent;
98
102
  try {
99
- const existingContent = await fs.readFile(openCodeConfigPath, 'utf8');
100
- existingConfig = JSON.parse(existingContent);
103
+ existingContent = await fs.readFile(openCodeConfigPath, 'utf8');
104
+ }
105
+ catch (error) {
106
+ if (error.code !== 'ENOENT') {
107
+ throw new Error(`Could not read OpenCode config at ${openCodeConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
101
109
  }
102
- catch {
103
- // File doesn't exist, we'll create it
110
+ if (existingContent !== undefined) {
111
+ try {
112
+ existingConfig = JSON.parse(existingContent);
113
+ }
114
+ catch (error) {
115
+ throw new Error(`Invalid OpenCode config at ${openCodeConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
116
+ }
104
117
  }
105
118
  // Transform ruler MCP to OpenCode format
106
119
  const transformedConfig = transformToOpenCodeFormat(rulerMcp);
@@ -108,15 +121,21 @@ async function propagateMcpToOpenCode(rulerMcpData, openCodeConfigPath, backup =
108
121
  const finalConfig = {
109
122
  ...existingConfig,
110
123
  $schema: transformedConfig.$schema,
111
- mcp: {
112
- ...existingConfig.mcp,
113
- ...transformedConfig.mcp,
114
- },
124
+ mcp: strategy === 'overwrite'
125
+ ? transformedConfig.mcp
126
+ : {
127
+ ...existingConfig.mcp,
128
+ ...transformedConfig.mcp,
129
+ },
115
130
  };
131
+ const finalContent = JSON.stringify(finalConfig, null, 2) + '\n';
132
+ if (existingContent === finalContent) {
133
+ return;
134
+ }
116
135
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openCodeConfigPath));
117
136
  if (backup) {
118
137
  const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
119
- await backupFile(openCodeConfigPath);
138
+ await backupFile(openCodeConfigPath, containmentRoot);
120
139
  }
121
- await fs.writeFile(openCodeConfigPath, JSON.stringify(finalConfig, null, 2) + '\n');
140
+ await (0, FileSystemUtils_1.writeGeneratedFile)(openCodeConfigPath, finalContent, containmentRoot);
122
141
  }
@@ -0,0 +1,2 @@
1
+ import { McpStrategy } from '../types';
2
+ export declare function propagateMcpToOpenHands(rulerMcpData: Record<string, unknown> | null, openHandsConfigPath: string, backup?: boolean, strategy?: McpStrategy, containmentRoot?: string): Promise<void>;