@intellectronica/ruler 0.3.42 → 0.3.44

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 (53) hide show
  1. package/README.md +98 -11
  2. package/dist/agents/AbstractAgent.js +3 -2
  3. package/dist/agents/AgentsMdAgent.js +3 -2
  4. package/dist/agents/AiderAgent.js +4 -3
  5. package/dist/agents/AmazonQCliAgent.js +6 -4
  6. package/dist/agents/AugmentCodeAgent.js +3 -2
  7. package/dist/agents/CodexCliAgent.js +1 -1
  8. package/dist/agents/CopilotAgent.js +1 -1
  9. package/dist/agents/CrushAgent.d.ts +1 -1
  10. package/dist/agents/CrushAgent.js +15 -6
  11. package/dist/agents/FirebenderAgent.js +5 -4
  12. package/dist/agents/GeminiCliAgent.d.ts +1 -0
  13. package/dist/agents/GeminiCliAgent.js +11 -5
  14. package/dist/agents/IAgent.d.ts +2 -0
  15. package/dist/agents/MistralVibeAgent.js +14 -3
  16. package/dist/agents/OpenCodeAgent.d.ts +1 -1
  17. package/dist/agents/OpenCodeAgent.js +10 -3
  18. package/dist/agents/QwenCodeAgent.d.ts +1 -0
  19. package/dist/agents/QwenCodeAgent.js +9 -3
  20. package/dist/agents/RooCodeAgent.js +3 -2
  21. package/dist/agents/ZedAgent.js +3 -3
  22. package/dist/constants.d.ts +1 -1
  23. package/dist/constants.js +1 -1
  24. package/dist/core/ConfigLoader.d.ts +2 -0
  25. package/dist/core/ConfigLoader.js +87 -6
  26. package/dist/core/FileSystemUtils.d.ts +5 -2
  27. package/dist/core/FileSystemUtils.js +121 -3
  28. package/dist/core/GitignoreUtils.d.ts +10 -0
  29. package/dist/core/GitignoreUtils.js +62 -31
  30. package/dist/core/SkillsProcessor.d.ts +2 -2
  31. package/dist/core/SkillsProcessor.js +46 -37
  32. package/dist/core/SkillsUtils.js +4 -1
  33. package/dist/core/SubagentsProcessor.js +8 -5
  34. package/dist/core/UnifiedConfigLoader.js +96 -11
  35. package/dist/core/UnifiedConfigTypes.d.ts +3 -1
  36. package/dist/core/agent-selection.js +6 -4
  37. package/dist/core/apply-engine.d.ts +1 -0
  38. package/dist/core/apply-engine.js +38 -15
  39. package/dist/core/revert-engine.d.ts +2 -1
  40. package/dist/core/revert-engine.js +79 -27
  41. package/dist/lib.js +9 -6
  42. package/dist/mcp/capabilities.js +2 -2
  43. package/dist/mcp/merge.js +28 -26
  44. package/dist/mcp/propagateOpenCodeMcp.d.ts +1 -1
  45. package/dist/mcp/propagateOpenCodeMcp.js +10 -3
  46. package/dist/mcp/propagateOpenHandsMcp.d.ts +1 -1
  47. package/dist/mcp/propagateOpenHandsMcp.js +18 -7
  48. package/dist/paths/mcp.d.ts +1 -1
  49. package/dist/paths/mcp.js +12 -5
  50. package/dist/revert.js +29 -27
  51. package/dist/vscode/settings.d.ts +1 -1
  52. package/dist/vscode/settings.js +3 -3
  53. package/package.json +6 -4
package/dist/paths/mcp.js CHANGED
@@ -39,12 +39,16 @@ exports.readNativeMcpToml = readNativeMcpToml;
39
39
  exports.writeNativeMcp = writeNativeMcp;
40
40
  const path = __importStar(require("path"));
41
41
  const fs_1 = require("fs");
42
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
43
+ function isRecord(value) {
44
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ }
42
46
  /** Determine the native MCP config path for a given agent. */
43
47
  async function getNativeMcpPath(adapterName, projectRoot) {
44
48
  const candidates = [];
45
49
  switch (adapterName) {
46
50
  case 'GitHub Copilot':
47
- candidates.push(path.join(projectRoot, '.vscode', 'mcp.json'));
51
+ candidates.push(path.join(projectRoot, '.mcp.json'));
48
52
  break;
49
53
  case 'Visual Studio':
50
54
  candidates.push(path.join(projectRoot, '.mcp.json'));
@@ -125,7 +129,11 @@ async function readNativeMcp(filePath) {
125
129
  throw new Error(`Could not read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
126
130
  }
127
131
  try {
128
- return JSON.parse(text);
132
+ const parsed = JSON.parse(text);
133
+ if (!isRecord(parsed)) {
134
+ throw new Error('must be a JSON object');
135
+ }
136
+ return parsed;
129
137
  }
130
138
  catch (error) {
131
139
  throw new Error(`Invalid MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -151,8 +159,7 @@ async function readNativeMcpToml(filePath, parseToml) {
151
159
  }
152
160
  }
153
161
  /** Write native MCP config to disk, creating parent directories as needed. */
154
- async function writeNativeMcp(filePath, data) {
155
- await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
162
+ async function writeNativeMcp(filePath, data, containmentRoot) {
156
163
  const text = JSON.stringify(data, null, 2) + '\n';
157
- await fs_1.promises.writeFile(filePath, text, 'utf8');
164
+ await (0, FileSystemUtils_1.writeGeneratedFile)(filePath, text, containmentRoot);
158
165
  }
package/dist/revert.js CHANGED
@@ -46,24 +46,23 @@ const agent_selection_1 = require("./core/agent-selection");
46
46
  const config_utils_1 = require("./core/config-utils");
47
47
  const GitignoreUtils_1 = require("./core/GitignoreUtils");
48
48
  const agents = agents_1.allAgents;
49
- const RULER_IGNORE_START_MARKER = '# START Ruler Generated Files';
50
- const RULER_IGNORE_END_MARKER = '# END Ruler Generated Files';
51
49
  const MANAGED_IGNORE_FILES = ['.gitignore', '.git/info/exclude'];
52
50
  /**
53
51
  * Reverts ruler configurations for selected AI agents.
54
52
  */
55
53
  async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, keepBackups = false, verbose = false, dryRun = false, localOnly = false) {
56
- (0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${projectRoot}`, verbose);
54
+ const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
55
+ if (!rulerDir) {
56
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
57
+ }
58
+ const effectiveProjectRoot = FileSystemUtils.resolveProjectRootForRulerDir(projectRoot, rulerDir);
59
+ (0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${effectiveProjectRoot}`, verbose);
57
60
  const config = await (0, ConfigLoader_1.loadConfig)({
58
- projectRoot,
61
+ projectRoot: effectiveProjectRoot,
59
62
  cliAgents: includedAgents,
60
63
  configPath,
61
64
  checkGlobal: !localOnly,
62
65
  });
63
- const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
64
- if (!rulerDir) {
65
- throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
66
- }
67
66
  (0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
68
67
  // Normalize per-agent config keys to agent identifiers
69
68
  config.agentConfigs = (0, config_utils_1.mapRawAgentConfigs)(config.agentConfigs, agents);
@@ -105,27 +104,35 @@ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, ke
105
104
  }
106
105
  }
107
106
  (0, constants_1.logVerbose)(`Selected agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
107
+ const isFullRevert = !config.cliAgents || config.cliAgents.length === 0;
108
108
  // Revert configurations for each agent
109
109
  let totalFilesProcessed = 0;
110
110
  let totalFilesRestored = 0;
111
111
  let totalFilesRemoved = 0;
112
112
  let totalBackupsRemoved = 0;
113
+ let totalDirectoriesRemoved = 0;
113
114
  for (const agent of selected) {
114
115
  const prefix = (0, constants_1.actionPrefix)(dryRun);
115
116
  console.log(`${prefix} Reverting ${agent.getName()}...`);
116
117
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
117
- const result = await (0, revert_engine_1.revertAgentConfiguration)(agent, projectRoot, agentConfig, keepBackups, verbose, dryRun);
118
+ const result = await (0, revert_engine_1.revertAgentConfiguration)(agent, effectiveProjectRoot, agentConfig, keepBackups, verbose, dryRun);
118
119
  totalFilesProcessed += result.restored + result.removed;
119
120
  totalFilesRestored += result.restored;
120
121
  totalFilesRemoved += result.removed;
121
122
  totalBackupsRemoved += result.backupsRemoved;
123
+ if (!isFullRevert) {
124
+ totalDirectoriesRemoved += await (0, revert_engine_1.cleanUpAgentDirectories)(agent, effectiveProjectRoot, agentConfig, verbose, dryRun);
125
+ }
122
126
  }
123
- // Clean up auxiliary files and directories
124
- const cleanupResult = await (0, revert_engine_1.cleanUpAuxiliaryFiles)(projectRoot, verbose, dryRun);
127
+ // Clean up auxiliary files and directories only when reverting all agents.
128
+ const cleanupResult = isFullRevert
129
+ ? await (0, revert_engine_1.cleanUpAuxiliaryFiles)(effectiveProjectRoot, verbose, dryRun)
130
+ : { additionalFilesRemoved: 0, directoriesRemoved: 0 };
125
131
  totalFilesRemoved += cleanupResult.additionalFilesRemoved;
132
+ totalDirectoriesRemoved += cleanupResult.directoriesRemoved;
126
133
  // Clean managed ignore blocks if reverting all agents.
127
- const cleanedIgnoreFiles = !config.cliAgents || config.cliAgents.length === 0
128
- ? await cleanManagedIgnoreFiles(projectRoot, verbose, dryRun)
134
+ const cleanedIgnoreFiles = isFullRevert
135
+ ? await cleanManagedIgnoreFiles(effectiveProjectRoot, verbose, dryRun)
129
136
  : [];
130
137
  // Display summary
131
138
  const prefix = (0, constants_1.actionPrefix)(dryRun);
@@ -138,11 +145,9 @@ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, ke
138
145
  console.log(` Files processed: ${totalFilesProcessed}`);
139
146
  console.log(` Files restored from backup: ${totalFilesRestored}`);
140
147
  console.log(` Generated files removed: ${totalFilesRemoved}`);
141
- if (!keepBackups) {
142
- console.log(` Backup files removed: ${totalBackupsRemoved}`);
143
- }
144
- if (cleanupResult.directoriesRemoved > 0) {
145
- console.log(` Empty directories removed: ${cleanupResult.directoriesRemoved}`);
148
+ console.log(` Backup files removed: ${totalBackupsRemoved}`);
149
+ if (totalDirectoriesRemoved > 0) {
150
+ console.log(` Empty directories removed: ${totalDirectoriesRemoved}`);
146
151
  }
147
152
  for (const ignoreFile of cleanedIgnoreFiles) {
148
153
  console.log(` ${ignoreFile} cleaned: yes`);
@@ -169,10 +174,11 @@ async function cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun) {
169
174
  (0, constants_1.logVerbose)(`No ${ignoreFile} file found`, verbose);
170
175
  return false;
171
176
  }
177
+ await FileSystemUtils.assertManagedPathInsideRoot(ignorePath, projectRoot, `Refusing to clean ${ignoreFile} through symlinked path`);
178
+ await FileSystemUtils.assertNotSymbolicLink(ignorePath, `Refusing to clean symlinked ${ignoreFile}`);
172
179
  const content = await fs_1.promises.readFile(ignorePath, 'utf8');
173
- const startIndex = content.indexOf(RULER_IGNORE_START_MARKER);
174
- const endIndex = content.indexOf(RULER_IGNORE_END_MARKER);
175
- if (startIndex === -1 || endIndex === -1) {
180
+ const cleaned = (0, GitignoreUtils_1.removeCompleteRulerBlocks)(content);
181
+ if (!cleaned.removed) {
176
182
  (0, constants_1.logVerbose)(`No ruler-managed block found in ${ignoreFile}`, verbose);
177
183
  return false;
178
184
  }
@@ -181,16 +187,12 @@ async function cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun) {
181
187
  (0, constants_1.logVerbose)(`${prefix} Would remove ruler block from ${ignoreFile}`, verbose);
182
188
  }
183
189
  else {
184
- const beforeBlock = content.substring(0, startIndex);
185
- const afterBlock = content.substring(endIndex + RULER_IGNORE_END_MARKER.length);
186
- let newContent = beforeBlock + afterBlock;
187
- newContent = newContent.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
188
- if (newContent.trim() === '') {
190
+ if (cleaned.content.trim() === '') {
189
191
  await fs_1.promises.unlink(ignorePath);
190
192
  (0, constants_1.logVerbose)(`${prefix} Removed empty ${ignoreFile} file`, verbose);
191
193
  }
192
194
  else {
193
- await fs_1.promises.writeFile(ignorePath, newContent);
195
+ await fs_1.promises.writeFile(ignorePath, cleaned.content);
194
196
  (0, constants_1.logVerbose)(`${prefix} Removed ruler block from ${ignoreFile}`, verbose);
195
197
  }
196
198
  }
@@ -25,7 +25,7 @@ export declare function readVSCodeSettings(settingsPath: string): Promise<VSCode
25
25
  /**
26
26
  * Write VSCode settings.json file
27
27
  */
28
- export declare function writeVSCodeSettings(settingsPath: string, settings: VSCodeSettings): Promise<void>;
28
+ export declare function writeVSCodeSettings(settingsPath: string, settings: VSCodeSettings, containmentRoot?: string): Promise<void>;
29
29
  /**
30
30
  * Transform ruler MCP config to Augment MCP server array format
31
31
  */
@@ -40,6 +40,7 @@ exports.mergeAugmentMcpServers = mergeAugmentMcpServers;
40
40
  exports.getVSCodeSettingsPath = getVSCodeSettingsPath;
41
41
  const fs_1 = require("fs");
42
42
  const path = __importStar(require("path"));
43
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
43
44
  /**
44
45
  * Read VSCode settings.json file
45
46
  */
@@ -58,9 +59,8 @@ async function readVSCodeSettings(settingsPath) {
58
59
  /**
59
60
  * Write VSCode settings.json file
60
61
  */
61
- async function writeVSCodeSettings(settingsPath, settings) {
62
- await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
63
- await fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 4));
62
+ async function writeVSCodeSettings(settingsPath, settings, containmentRoot) {
63
+ await (0, FileSystemUtils_1.writeGeneratedFile)(settingsPath, JSON.stringify(settings, null, 4), containmentRoot);
64
64
  }
65
65
  /**
66
66
  * Transform ruler MCP config to Augment MCP server array format
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "types": "dist/lib.d.ts",
@@ -12,8 +12,10 @@
12
12
  "test:watch": "jest --watch",
13
13
  "test:coverage": "jest --coverage",
14
14
  "test:integration": "jest tests/e2e/ruler.integration.test.ts --verbose",
15
- "build": "tsc",
16
- "prepare": "npm run build"
15
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
16
+ "build": "npm run clean && tsc",
17
+ "check:package-lock": "node -e \"const pkg=require('./package.json'); const lock=require('./package-lock.json'); const root=lock.packages?.['']; if (lock.version !== pkg.version || root?.version !== pkg.version) { console.error('package-lock.json version metadata must match package.json version'); process.exit(1); }\"",
18
+ "prepublishOnly": "npm run build"
17
19
  },
18
20
  "repository": {
19
21
  "type": "git",
@@ -68,7 +70,7 @@
68
70
  },
69
71
  "dependencies": {
70
72
  "@iarna/toml": "^2.2.5",
71
- "js-yaml": "^4.1.1",
73
+ "js-yaml": "^4.2.0",
72
74
  "yargs": "^18.0.0",
73
75
  "zod": "^4.1.12"
74
76
  }