@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.
- package/README.md +135 -36
- package/dist/agents/AbstractAgent.d.ts +53 -0
- package/dist/agents/AbstractAgent.js +3 -2
- package/dist/agents/AgentsMdAgent.d.ts +14 -0
- package/dist/agents/AgentsMdAgent.js +3 -2
- package/dist/agents/AiderAgent.d.ts +14 -0
- package/dist/agents/AiderAgent.js +7 -4
- package/dist/agents/AmazonQCliAgent.d.ts +13 -0
- package/dist/agents/AmazonQCliAgent.js +6 -4
- package/dist/agents/AmpAgent.d.ts +6 -0
- package/dist/agents/AntigravityAgent.d.ts +10 -0
- package/dist/agents/AugmentCodeAgent.d.ts +13 -0
- package/dist/agents/AugmentCodeAgent.js +3 -2
- package/dist/agents/ClaudeAgent.d.ts +13 -0
- package/dist/agents/ClineAgent.d.ts +9 -0
- package/dist/agents/CodexCliAgent.d.ts +31 -0
- package/dist/agents/CodexCliAgent.js +1 -1
- package/dist/agents/CopilotAgent.d.ts +20 -0
- package/dist/agents/CrushAgent.d.ts +14 -0
- package/dist/agents/CrushAgent.js +18 -6
- package/dist/agents/CursorAgent.d.ts +17 -0
- package/dist/agents/FactoryDroidAgent.d.ts +13 -0
- package/dist/agents/FirebaseAgent.d.ts +11 -0
- package/dist/agents/FirebenderAgent.d.ts +36 -0
- package/dist/agents/FirebenderAgent.js +5 -4
- package/dist/agents/GeminiCliAgent.d.ts +12 -0
- package/dist/agents/GeminiCliAgent.js +13 -7
- package/dist/agents/GooseAgent.d.ts +12 -0
- package/dist/agents/IAgent.d.ts +74 -0
- package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
- package/dist/agents/JulesAgent.d.ts +5 -0
- package/dist/agents/JunieAgent.d.ts +12 -0
- package/dist/agents/KiloCodeAgent.d.ts +14 -0
- package/dist/agents/KiroAgent.d.ts +8 -0
- package/dist/agents/MistralVibeAgent.d.ts +31 -0
- package/dist/agents/MistralVibeAgent.js +14 -3
- package/dist/agents/OpenCodeAgent.d.ts +11 -0
- package/dist/agents/OpenCodeAgent.js +24 -12
- package/dist/agents/OpenHandsAgent.d.ts +8 -0
- package/dist/agents/PiAgent.d.ts +9 -0
- package/dist/agents/QwenCodeAgent.d.ts +11 -0
- package/dist/agents/QwenCodeAgent.js +11 -5
- package/dist/agents/RooCodeAgent.d.ts +16 -0
- package/dist/agents/RooCodeAgent.js +3 -2
- package/dist/agents/TraeAgent.d.ts +10 -0
- package/dist/agents/WarpAgent.d.ts +12 -0
- package/dist/agents/WindsurfAgent.d.ts +13 -0
- package/dist/agents/ZedAgent.d.ts +21 -0
- package/dist/agents/ZedAgent.js +8 -5
- package/dist/agents/agent-utils.d.ts +5 -0
- package/dist/agents/agent-utils.js +8 -5
- package/dist/agents/index.d.ts +9 -0
- package/dist/cli/commands.d.ts +4 -0
- package/dist/cli/commands.js +1 -2
- package/dist/cli/handlers.d.ts +41 -0
- package/dist/cli/handlers.js +75 -59
- package/dist/cli/index.d.ts +2 -0
- package/dist/constants.d.ts +35 -0
- package/dist/constants.js +1 -1
- package/dist/core/ConfigLoader.d.ts +59 -0
- package/dist/core/ConfigLoader.js +178 -44
- package/dist/core/FileSystemUtils.d.ts +53 -0
- package/dist/core/FileSystemUtils.js +157 -20
- package/dist/core/GitignoreUtils.d.ts +25 -0
- package/dist/core/GitignoreUtils.js +94 -32
- package/dist/core/RuleProcessor.d.ts +8 -0
- package/dist/core/SkillsProcessor.d.ts +127 -0
- package/dist/core/SkillsProcessor.js +118 -223
- package/dist/core/SkillsUtils.d.ts +26 -0
- package/dist/core/SubagentsProcessor.d.ts +38 -0
- package/dist/core/SubagentsProcessor.js +8 -5
- package/dist/core/SubagentsUtils.d.ts +34 -0
- package/dist/core/UnifiedConfigLoader.d.ts +10 -0
- package/dist/core/UnifiedConfigLoader.js +115 -33
- package/dist/core/UnifiedConfigTypes.d.ts +97 -0
- package/dist/core/agent-selection.d.ts +12 -0
- package/dist/core/agent-selection.js +17 -7
- package/dist/core/apply-engine.d.ts +70 -0
- package/dist/core/apply-engine.js +88 -58
- package/dist/core/config-utils.d.ts +14 -0
- package/dist/core/config-utils.js +9 -3
- package/dist/core/hash.d.ts +2 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +42 -0
- package/dist/core/revert-engine.d.ts +37 -0
- package/dist/core/revert-engine.js +142 -34
- package/dist/lib.d.ts +13 -0
- package/dist/lib.js +24 -8
- package/dist/mcp/capabilities.d.ts +20 -0
- package/dist/mcp/merge.d.ts +10 -0
- package/dist/mcp/merge.js +36 -16
- package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
- package/dist/mcp/propagateOpenCodeMcp.js +30 -11
- package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
- package/dist/mcp/propagateOpenHandsMcp.js +48 -21
- package/dist/mcp/validate.d.ts +7 -0
- package/dist/mcp/validate.js +6 -1
- package/dist/paths/mcp.d.ts +8 -0
- package/dist/paths/mcp.js +44 -8
- package/dist/revert.d.ts +6 -0
- package/dist/revert.js +58 -46
- package/dist/types.d.ts +87 -0
- package/dist/vscode/settings.d.ts +40 -0
- package/dist/vscode/settings.js +3 -3
- 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)(`${
|
|
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)(`${
|
|
389
|
+
(0, constants_1.logVerbose)(`${prefix} Would remove empty VSCode settings file`, verbose);
|
|
303
390
|
}
|
|
304
391
|
else {
|
|
305
|
-
(0, constants_1.logVerbose)(`${
|
|
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)(`${
|
|
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)(`${
|
|
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
|
|
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
|
-
|
|
355
|
-
const
|
|
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 (
|
|
374
|
-
if (mcpPath &&
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
172
|
+
await propagateSubagents(singleProjectRoot, selectedAgents, subagentsEnabledResolvedSingle, subagentsCleanupOrphanedSingle, verbose, dryRun);
|
|
157
173
|
}
|
|
158
|
-
generatedPaths = await (0, apply_engine_1.processSingleConfiguration)(selectedAgents, singleConfig,
|
|
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(
|
|
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(
|
|
189
|
+
const subagentPaths = await getSubagentsGitignorePaths(outputProjectRoot, selectedAgents);
|
|
174
190
|
allGeneratedPaths = [...allGeneratedPaths, ...subagentPaths];
|
|
175
191
|
}
|
|
176
|
-
await (0, apply_engine_1.updateGitignore)(
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
25
|
-
|
|
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 =
|
|
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,
|
|
@@ -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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
140
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(openCodeConfigPath, finalContent, containmentRoot);
|
|
122
141
|
}
|