@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
@@ -67,10 +67,14 @@ function extractApiKey(headers) {
67
67
  }
68
68
  return null;
69
69
  }
70
- function createRemoteServerEntry(url, headers) {
70
+ function createRemoteServerEntry(name, url, headers) {
71
+ const hasHeaders = headers && Object.keys(headers).length > 0;
71
72
  const apiKey = extractApiKey(headers);
72
- if (apiKey) {
73
- return { url, api_key: apiKey };
73
+ if (hasHeaders) {
74
+ if (apiKey) {
75
+ return { url, api_key: apiKey };
76
+ }
77
+ throw new Error(`OpenHands MCP remote server "${name}" has unsupported headers. OpenHands config.toml can only represent a Bearer Authorization header as api_key.`);
74
78
  }
75
79
  return url;
76
80
  }
@@ -89,8 +93,11 @@ function normalizeRemoteServerArray(entries) {
89
93
  // All entries are strings, keep as is
90
94
  return entries;
91
95
  }
92
- async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true) {
96
+ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup = true, strategy = 'merge', containmentRoot) {
93
97
  const rulerMcp = rulerMcpData || {};
98
+ if (containmentRoot) {
99
+ await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(openHandsConfigPath, containmentRoot, 'Refusing to write generated file outside project');
100
+ }
94
101
  // Always use the legacy Ruler MCP config format as input (top-level "mcpServers" key)
95
102
  const rulerServers = rulerMcp.mcpServers || {};
96
103
  // Return early if no servers to process
@@ -100,12 +107,22 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
100
107
  return;
101
108
  }
102
109
  let config = {};
110
+ let tomlContent;
103
111
  try {
104
- const tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
105
- config = (0, toml_1.parse)(tomlContent);
112
+ tomlContent = await fs.readFile(openHandsConfigPath, 'utf8');
113
+ }
114
+ catch (error) {
115
+ if (error.code !== 'ENOENT') {
116
+ throw new Error(`Could not read OpenHands config at ${openHandsConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
106
118
  }
107
- catch {
108
- // File doesn't exist, we'll create it.
119
+ if (tomlContent !== undefined) {
120
+ try {
121
+ config = (0, toml_1.parse)(tomlContent);
122
+ }
123
+ catch (error) {
124
+ throw new Error(`Invalid OpenHands config at ${openHandsConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
125
+ }
109
126
  }
110
127
  if (!config.mcp) {
111
128
  config.mcp = {};
@@ -119,18 +136,24 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
119
136
  if (!config.mcp.shttp_servers) {
120
137
  config.mcp.shttp_servers = [];
121
138
  }
122
- // Build maps for merging existing servers
123
- const existingStdioServers = new Map(config.mcp.stdio_servers.map((s) => [s.name, s]));
139
+ // Build maps for merging existing servers, or start fresh when overwriting.
140
+ const existingStdioServers = new Map(strategy === 'overwrite'
141
+ ? []
142
+ : config.mcp.stdio_servers.map((s) => [s.name, s]));
124
143
  const existingSseServers = new Map();
125
- config.mcp.sse_servers.forEach((entry) => {
126
- const url = typeof entry === 'string' ? entry : entry.url;
127
- existingSseServers.set(url, entry);
128
- });
144
+ if (strategy !== 'overwrite') {
145
+ config.mcp.sse_servers.forEach((entry) => {
146
+ const url = typeof entry === 'string' ? entry : entry.url;
147
+ existingSseServers.set(url, entry);
148
+ });
149
+ }
129
150
  const existingShttpServers = new Map();
130
- config.mcp.shttp_servers.forEach((entry) => {
131
- const url = typeof entry === 'string' ? entry : entry.url;
132
- existingShttpServers.set(url, entry);
133
- });
151
+ if (strategy !== 'overwrite') {
152
+ config.mcp.shttp_servers.forEach((entry) => {
153
+ const url = typeof entry === 'string' ? entry : entry.url;
154
+ existingShttpServers.set(url, entry);
155
+ });
156
+ }
134
157
  for (const [name, serverDef] of Object.entries(rulerServers)) {
135
158
  if (isRulerMcpServer(serverDef)) {
136
159
  if (serverDef.command) {
@@ -146,7 +169,7 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
146
169
  else if (serverDef.url) {
147
170
  // Remote server
148
171
  const classification = classifyRemoteServer(serverDef.url);
149
- const entry = createRemoteServerEntry(serverDef.url, serverDef.headers);
172
+ const entry = createRemoteServerEntry(name, serverDef.url, serverDef.headers);
150
173
  if (classification === 'sse') {
151
174
  existingSseServers.set(serverDef.url, entry);
152
175
  }
@@ -160,10 +183,14 @@ async function propagateMcpToOpenHands(rulerMcpData, openHandsConfigPath, backup
160
183
  config.mcp.stdio_servers = Array.from(existingStdioServers.values());
161
184
  config.mcp.sse_servers = normalizeRemoteServerArray(Array.from(existingSseServers.values()));
162
185
  config.mcp.shttp_servers = normalizeRemoteServerArray(Array.from(existingShttpServers.values()));
186
+ const finalContent = (0, toml_1.stringify)(config);
187
+ if (tomlContent === finalContent) {
188
+ return;
189
+ }
163
190
  await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(openHandsConfigPath));
164
191
  if (backup) {
165
192
  const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
166
- await backupFile(openHandsConfigPath);
193
+ await backupFile(openHandsConfigPath, containmentRoot);
167
194
  }
168
- await fs.writeFile(openHandsConfigPath, (0, toml_1.stringify)(config));
195
+ await (0, FileSystemUtils_1.writeGeneratedFile)(openHandsConfigPath, finalContent, containmentRoot);
169
196
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate the structure of the Ruler MCP JSON config.
3
+ * Minimal validation: ensure 'mcpServers' property exists and is an object.
4
+ * @param data Parsed JSON object from .ruler/mcp.json.
5
+ * @throws Error if validation fails.
6
+ */
7
+ export declare function validateMcp(data: unknown): void;
@@ -8,10 +8,15 @@ exports.validateMcp = validateMcp;
8
8
  * @throws Error if validation fails.
9
9
  */
10
10
  function validateMcp(data) {
11
+ const mcpServers = data && typeof data === 'object'
12
+ ? data.mcpServers
13
+ : undefined;
11
14
  if (!data ||
12
15
  typeof data !== 'object' ||
13
16
  !('mcpServers' in data) ||
14
- typeof data.mcpServers !== 'object') {
17
+ !mcpServers ||
18
+ typeof mcpServers !== 'object' ||
19
+ Array.isArray(mcpServers)) {
15
20
  throw new Error('[ruler] Invalid MCP config: must contain an object property "mcpServers" (Ruler style)');
16
21
  }
17
22
  }
@@ -0,0 +1,8 @@
1
+ /** Determine the native MCP config path for a given agent. */
2
+ export declare function getNativeMcpPath(adapterName: string, projectRoot: string): Promise<string | null>;
3
+ /** Read native MCP config from disk, or return empty object if missing. */
4
+ export declare function readNativeMcp(filePath: string): Promise<Record<string, unknown>>;
5
+ /** Read native Codex TOML MCP config from disk, or return empty object if missing. */
6
+ export declare function readNativeMcpToml(filePath: string, parseToml: (text: string) => Record<string, unknown>): Promise<Record<string, unknown>>;
7
+ /** Write native MCP config to disk, creating parent directories as needed. */
8
+ export declare function writeNativeMcp(filePath: string, data: unknown, containmentRoot?: string): Promise<void>;
package/dist/paths/mcp.js CHANGED
@@ -35,9 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getNativeMcpPath = getNativeMcpPath;
37
37
  exports.readNativeMcp = readNativeMcp;
38
+ exports.readNativeMcpToml = readNativeMcpToml;
38
39
  exports.writeNativeMcp = writeNativeMcp;
39
40
  const path = __importStar(require("path"));
40
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
+ }
41
46
  /** Determine the native MCP config path for a given agent. */
42
47
  async function getNativeMcpPath(adapterName, projectRoot) {
43
48
  const candidates = [];
@@ -111,19 +116,50 @@ async function getNativeMcpPath(adapterName, projectRoot) {
111
116
  // default to first candidate if none exist
112
117
  return candidates.length > 0 ? candidates[0] : null;
113
118
  }
114
- /** Read native MCP config from disk, or return empty object if missing/invalid. */
119
+ /** Read native MCP config from disk, or return empty object if missing. */
115
120
  async function readNativeMcp(filePath) {
121
+ let text;
122
+ try {
123
+ text = await fs_1.promises.readFile(filePath, 'utf8');
124
+ }
125
+ catch (error) {
126
+ if (error.code === 'ENOENT') {
127
+ return {};
128
+ }
129
+ throw new Error(`Could not read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
130
+ }
131
+ try {
132
+ const parsed = JSON.parse(text);
133
+ if (!isRecord(parsed)) {
134
+ throw new Error('must be a JSON object');
135
+ }
136
+ return parsed;
137
+ }
138
+ catch (error) {
139
+ throw new Error(`Invalid MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
140
+ }
141
+ }
142
+ /** Read native Codex TOML MCP config from disk, or return empty object if missing. */
143
+ async function readNativeMcpToml(filePath, parseToml) {
144
+ let text;
145
+ try {
146
+ text = await fs_1.promises.readFile(filePath, 'utf8');
147
+ }
148
+ catch (error) {
149
+ if (error.code === 'ENOENT') {
150
+ return {};
151
+ }
152
+ throw new Error(`Could not read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
153
+ }
116
154
  try {
117
- const text = await fs_1.promises.readFile(filePath, 'utf8');
118
- return JSON.parse(text);
155
+ return parseToml(text);
119
156
  }
120
- catch {
121
- return {};
157
+ catch (error) {
158
+ throw new Error(`Invalid MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
122
159
  }
123
160
  }
124
161
  /** Write native MCP config to disk, creating parent directories as needed. */
125
- async function writeNativeMcp(filePath, data) {
126
- await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
162
+ async function writeNativeMcp(filePath, data, containmentRoot) {
127
163
  const text = JSON.stringify(data, null, 2) + '\n';
128
- await fs_1.promises.writeFile(filePath, text, 'utf8');
164
+ await (0, FileSystemUtils_1.writeGeneratedFile)(filePath, text, containmentRoot);
129
165
  }
@@ -0,0 +1,6 @@
1
+ import { allAgents } from './agents';
2
+ export { allAgents };
3
+ /**
4
+ * Reverts ruler configurations for selected AI agents.
5
+ */
6
+ export declare function revertAllAgentConfigs(projectRoot: string, includedAgents?: string[], configPath?: string, keepBackups?: boolean, verbose?: boolean, dryRun?: boolean, localOnly?: boolean): Promise<void>;
package/dist/revert.js CHANGED
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.allAgents = void 0;
37
37
  exports.revertAllAgentConfigs = revertAllAgentConfigs;
38
- const path = __importStar(require("path"));
39
38
  const fs_1 = require("fs");
40
39
  const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
41
40
  const ConfigLoader_1 = require("./core/ConfigLoader");
@@ -45,21 +44,25 @@ const constants_1 = require("./constants");
45
44
  const revert_engine_1 = require("./core/revert-engine");
46
45
  const agent_selection_1 = require("./core/agent-selection");
47
46
  const config_utils_1 = require("./core/config-utils");
47
+ const GitignoreUtils_1 = require("./core/GitignoreUtils");
48
48
  const agents = agents_1.allAgents;
49
+ const MANAGED_IGNORE_FILES = ['.gitignore', '.git/info/exclude'];
49
50
  /**
50
51
  * Reverts ruler configurations for selected AI agents.
51
52
  */
52
53
  async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, keepBackups = false, verbose = false, dryRun = false, localOnly = false) {
53
- (0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${projectRoot}`, verbose);
54
- const config = await (0, ConfigLoader_1.loadConfig)({
55
- projectRoot,
56
- cliAgents: includedAgents,
57
- configPath,
58
- });
59
54
  const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
60
55
  if (!rulerDir) {
61
56
  throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
62
57
  }
58
+ const effectiveProjectRoot = FileSystemUtils.resolveProjectRootForRulerDir(projectRoot, rulerDir);
59
+ (0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${effectiveProjectRoot}`, verbose);
60
+ const config = await (0, ConfigLoader_1.loadConfig)({
61
+ projectRoot: effectiveProjectRoot,
62
+ cliAgents: includedAgents,
63
+ configPath,
64
+ checkGlobal: !localOnly,
65
+ });
63
66
  (0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
64
67
  // Normalize per-agent config keys to agent identifiers
65
68
  config.agentConfigs = (0, config_utils_1.mapRawAgentConfigs)(config.agentConfigs, agents);
@@ -77,18 +80,19 @@ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, ke
77
80
  // Fall back to the old logic without validation
78
81
  if (config.cliAgents && config.cliAgents.length > 0) {
79
82
  const filters = config.cliAgents.map((n) => n.toLowerCase());
80
- selected = agents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
81
- agent.getName().toLowerCase().includes(f)));
83
+ const validAgentIdentifiers = new Set(agents.map((agent) => agent.getIdentifier()));
84
+ selected = agents.filter((agent) => filters.some((f) => (0, agent_selection_1.agentMatchesFilter)(agent, f, validAgentIdentifiers)));
82
85
  }
83
86
  else if (config.defaultAgents && config.defaultAgents.length > 0) {
84
87
  const defaults = config.defaultAgents.map((n) => n.toLowerCase());
88
+ const validAgentIdentifiers = new Set(agents.map((agent) => agent.getIdentifier()));
85
89
  selected = agents.filter((agent) => {
86
90
  const identifier = agent.getIdentifier();
87
91
  const override = config.agentConfigs[identifier]?.enabled;
88
92
  if (override !== undefined) {
89
93
  return override;
90
94
  }
91
- return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
95
+ return defaults.some((d) => (0, agent_selection_1.agentMatchesFilter)(agent, d, validAgentIdentifiers));
92
96
  });
93
97
  }
94
98
  else {
@@ -100,28 +104,36 @@ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, ke
100
104
  }
101
105
  }
102
106
  (0, constants_1.logVerbose)(`Selected agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
107
+ const isFullRevert = !config.cliAgents || config.cliAgents.length === 0;
103
108
  // Revert configurations for each agent
104
109
  let totalFilesProcessed = 0;
105
110
  let totalFilesRestored = 0;
106
111
  let totalFilesRemoved = 0;
107
112
  let totalBackupsRemoved = 0;
113
+ let totalDirectoriesRemoved = 0;
108
114
  for (const agent of selected) {
109
115
  const prefix = (0, constants_1.actionPrefix)(dryRun);
110
116
  console.log(`${prefix} Reverting ${agent.getName()}...`);
111
117
  const agentConfig = config.agentConfigs[agent.getIdentifier()];
112
- 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);
113
119
  totalFilesProcessed += result.restored + result.removed;
114
120
  totalFilesRestored += result.restored;
115
121
  totalFilesRemoved += result.removed;
116
122
  totalBackupsRemoved += result.backupsRemoved;
123
+ if (!isFullRevert) {
124
+ totalDirectoriesRemoved += await (0, revert_engine_1.cleanUpAgentDirectories)(agent, effectiveProjectRoot, agentConfig, verbose, dryRun);
125
+ }
117
126
  }
118
- // Clean up auxiliary files and directories
119
- 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 };
120
131
  totalFilesRemoved += cleanupResult.additionalFilesRemoved;
121
- // Clean .gitignore if reverting all agents
122
- const gitignoreCleaned = !config.cliAgents || config.cliAgents.length === 0
123
- ? await cleanGitignore(projectRoot, verbose, dryRun)
124
- : false;
132
+ totalDirectoriesRemoved += cleanupResult.directoriesRemoved;
133
+ // Clean managed ignore blocks if reverting all agents.
134
+ const cleanedIgnoreFiles = isFullRevert
135
+ ? await cleanManagedIgnoreFiles(effectiveProjectRoot, verbose, dryRun)
136
+ : [];
125
137
  // Display summary
126
138
  const prefix = (0, constants_1.actionPrefix)(dryRun);
127
139
  if (dryRun) {
@@ -133,53 +145,53 @@ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, ke
133
145
  console.log(` Files processed: ${totalFilesProcessed}`);
134
146
  console.log(` Files restored from backup: ${totalFilesRestored}`);
135
147
  console.log(` Generated files removed: ${totalFilesRemoved}`);
136
- if (!keepBackups) {
137
- console.log(` Backup files removed: ${totalBackupsRemoved}`);
138
- }
139
- if (cleanupResult.directoriesRemoved > 0) {
140
- 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}`);
141
151
  }
142
- if (gitignoreCleaned) {
143
- console.log(` .gitignore cleaned: yes`);
152
+ for (const ignoreFile of cleanedIgnoreFiles) {
153
+ console.log(` ${ignoreFile} cleaned: yes`);
144
154
  }
145
155
  }
146
156
  /**
147
- * Removes the ruler-managed block from .gitignore file.
157
+ * Removes the ruler-managed block from ignore files Ruler can update.
148
158
  */
149
- async function cleanGitignore(projectRoot, verbose, dryRun) {
150
- const gitignorePath = path.join(projectRoot, '.gitignore');
159
+ async function cleanManagedIgnoreFiles(projectRoot, verbose, dryRun) {
160
+ const cleanedFiles = [];
161
+ for (const ignoreFile of MANAGED_IGNORE_FILES) {
162
+ if (await cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun)) {
163
+ cleanedFiles.push(ignoreFile);
164
+ }
165
+ }
166
+ return cleanedFiles;
167
+ }
168
+ async function cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun) {
169
+ const ignorePath = await (0, GitignoreUtils_1.resolveIgnoreFilePath)(projectRoot, ignoreFile);
151
170
  try {
152
- await fs_1.promises.access(gitignorePath);
171
+ await fs_1.promises.access(ignorePath);
153
172
  }
154
173
  catch {
155
- (0, constants_1.logVerbose)('No .gitignore file found', verbose);
174
+ (0, constants_1.logVerbose)(`No ${ignoreFile} file found`, verbose);
156
175
  return false;
157
176
  }
158
- const content = await fs_1.promises.readFile(gitignorePath, 'utf8');
159
- const startMarker = '# START Ruler Generated Files';
160
- const endMarker = '# END Ruler Generated Files';
161
- const startIndex = content.indexOf(startMarker);
162
- const endIndex = content.indexOf(endMarker);
163
- if (startIndex === -1 || endIndex === -1) {
164
- (0, constants_1.logVerbose)('No ruler-managed block found in .gitignore', verbose);
177
+ const content = await fs_1.promises.readFile(ignorePath, 'utf8');
178
+ const cleaned = (0, GitignoreUtils_1.removeCompleteRulerBlocks)(content);
179
+ if (!cleaned.removed) {
180
+ (0, constants_1.logVerbose)(`No ruler-managed block found in ${ignoreFile}`, verbose);
165
181
  return false;
166
182
  }
167
183
  const prefix = (0, constants_1.actionPrefix)(dryRun);
168
184
  if (dryRun) {
169
- (0, constants_1.logVerbose)(`${prefix} Would remove ruler block from .gitignore`, verbose);
185
+ (0, constants_1.logVerbose)(`${prefix} Would remove ruler block from ${ignoreFile}`, verbose);
170
186
  }
171
187
  else {
172
- const beforeBlock = content.substring(0, startIndex);
173
- const afterBlock = content.substring(endIndex + endMarker.length);
174
- let newContent = beforeBlock + afterBlock;
175
- newContent = newContent.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
176
- if (newContent.trim() === '') {
177
- await fs_1.promises.unlink(gitignorePath);
178
- (0, constants_1.logVerbose)(`${prefix} Removed empty .gitignore file`, verbose);
188
+ if (cleaned.content.trim() === '') {
189
+ await fs_1.promises.unlink(ignorePath);
190
+ (0, constants_1.logVerbose)(`${prefix} Removed empty ${ignoreFile} file`, verbose);
179
191
  }
180
192
  else {
181
- await fs_1.promises.writeFile(gitignorePath, newContent);
182
- (0, constants_1.logVerbose)(`${prefix} Removed ruler block from .gitignore`, verbose);
193
+ await fs_1.promises.writeFile(ignorePath, cleaned.content);
194
+ (0, constants_1.logVerbose)(`${prefix} Removed ruler block from ${ignoreFile}`, verbose);
183
195
  }
184
196
  }
185
197
  return true;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Types for Model Context Protocol (MCP) server configuration.
3
+ */
4
+ export type McpStrategy = 'merge' | 'overwrite';
5
+ /** MCP configuration for an agent or global. */
6
+ export interface McpConfig {
7
+ /** Enable or disable MCP propagation (merge or overwrite). */
8
+ enabled?: boolean;
9
+ /** Merge strategy: 'merge' to merge servers, 'overwrite' to replace config. */
10
+ strategy?: McpStrategy;
11
+ }
12
+ /** Global MCP configuration section (same as agent-specific config). */
13
+ export type GlobalMcpConfig = McpConfig;
14
+ /** Gitignore configuration for automatic .gitignore file updates. */
15
+ export interface GitignoreConfig {
16
+ /** Enable or disable automatic .gitignore updates. */
17
+ enabled?: boolean;
18
+ /** Write managed ignore entries to .git/info/exclude instead of .gitignore. */
19
+ local?: boolean;
20
+ }
21
+ /** Backup configuration for .bak file generation. */
22
+ export interface BackupConfig {
23
+ /** Enable or disable creation of .bak backup files. */
24
+ enabled?: boolean;
25
+ }
26
+ /** Skills configuration for automatic skills distribution. */
27
+ export interface SkillsConfig {
28
+ /** Enable or disable skills support. */
29
+ enabled?: boolean;
30
+ }
31
+ /** Information about a discovered skill. */
32
+ export interface SkillInfo {
33
+ /** Name of the skill (directory name). */
34
+ name: string;
35
+ /** Absolute path to the skill directory. */
36
+ path: string;
37
+ /** Whether the directory contains a SKILL.md file. */
38
+ hasSkillMd: boolean;
39
+ /** Whether this is a valid skill. */
40
+ valid: boolean;
41
+ /** Error message if invalid. */
42
+ error?: string;
43
+ }
44
+ /** Subagents configuration for automatic subagent distribution. */
45
+ export interface SubagentsConfig {
46
+ /** Enable or disable subagents support. */
47
+ enabled?: boolean;
48
+ /**
49
+ * When true, Ruler may delete previously generated native subagent
50
+ * directories that are stale (disabled, no source definitions, or
51
+ * deselected targets). Defaults to false (non-destructive).
52
+ */
53
+ cleanup_orphaned?: boolean;
54
+ /**
55
+ * When true, `.ruler/agents/*.md` are also concatenated into the
56
+ * generated top-level rule files (CLAUDE.md, AGENTS.md, Copilot
57
+ * instructions, etc.). When false (default), `.ruler/agents/` is
58
+ * skipped during rule concatenation, mirroring `.ruler/skills/`.
59
+ */
60
+ include_in_rules?: boolean;
61
+ }
62
+ /** Frontmatter fields recognised on a source subagent definition. */
63
+ export interface SubagentFrontmatter {
64
+ name: string;
65
+ description: string;
66
+ tools?: string[];
67
+ model?: string;
68
+ readonly?: boolean;
69
+ is_background?: boolean;
70
+ }
71
+ /** Information about a discovered subagent. */
72
+ export interface SubagentInfo {
73
+ /** Name of the subagent (filename stem and frontmatter `name`). */
74
+ name: string;
75
+ /** Absolute path to the source `.md` file. */
76
+ path: string;
77
+ /** Relative `.md` path under `.ruler/agents/` (preserves nested layout). */
78
+ sourceRelativePath?: string;
79
+ /** Parsed frontmatter (only present when valid). */
80
+ frontmatter?: SubagentFrontmatter;
81
+ /** Body content after the frontmatter delimiter. */
82
+ body?: string;
83
+ /** Whether this subagent passed validation. */
84
+ valid: boolean;
85
+ /** Error message if invalid. */
86
+ error?: string;
87
+ }
@@ -0,0 +1,40 @@
1
+ import { McpStrategy } from '../types';
2
+ /**
3
+ * VSCode settings.json structure for Augment MCP configuration
4
+ */
5
+ export interface VSCodeSettings {
6
+ 'augment.advanced'?: {
7
+ mcpServers?: AugmentMcpServer[];
8
+ [key: string]: unknown;
9
+ };
10
+ [key: string]: unknown;
11
+ }
12
+ /**
13
+ * Augment MCP server configuration format
14
+ */
15
+ export interface AugmentMcpServer {
16
+ name: string;
17
+ command: string;
18
+ args?: string[];
19
+ env?: Record<string, string>;
20
+ }
21
+ /**
22
+ * Read VSCode settings.json file
23
+ */
24
+ export declare function readVSCodeSettings(settingsPath: string): Promise<VSCodeSettings>;
25
+ /**
26
+ * Write VSCode settings.json file
27
+ */
28
+ export declare function writeVSCodeSettings(settingsPath: string, settings: VSCodeSettings, containmentRoot?: string): Promise<void>;
29
+ /**
30
+ * Transform ruler MCP config to Augment MCP server array format
31
+ */
32
+ export declare function transformRulerToAugmentMcp(rulerMcpJson: Record<string, unknown>): AugmentMcpServer[];
33
+ /**
34
+ * Merge MCP servers into VSCode settings using the specified strategy
35
+ */
36
+ export declare function mergeAugmentMcpServers(existingSettings: VSCodeSettings, newServers: AugmentMcpServer[], strategy: McpStrategy): VSCodeSettings;
37
+ /**
38
+ * Get the VSCode settings.json path for a project (local)
39
+ */
40
+ export declare function getVSCodeSettingsPath(projectRoot: string): string;
@@ -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,17 +1,20 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.3.41",
3
+ "version": "0.3.43",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
+ "types": "dist/lib.d.ts",
6
7
  "scripts": {
7
8
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
8
- "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
9
- "test": "jest",
9
+ "format": "prettier --write package.json package-lock.json tsconfig.json README.md \".github/**/*.yml\" \"src/**/*.{ts,tsx,json,md}\" \"tests/**/*.{ts,tsx,json,md}\"",
10
+ "format:check": "prettier --check package.json package-lock.json tsconfig.json README.md \".github/**/*.yml\" \"src/**/*.{ts,tsx,json,md}\" \"tests/**/*.{ts,tsx,json,md}\"",
11
+ "test": "jest --coverage",
10
12
  "test:watch": "jest --watch",
11
13
  "test:coverage": "jest --coverage",
12
14
  "test:integration": "jest tests/e2e/ruler.integration.test.ts --verbose",
13
15
  "build": "tsc",
14
- "prepare": "npm run build"
16
+ "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); }\"",
17
+ "prepublishOnly": "npm run build"
15
18
  },
16
19
  "repository": {
17
20
  "type": "git",
@@ -66,7 +69,7 @@
66
69
  },
67
70
  "dependencies": {
68
71
  "@iarna/toml": "^2.2.5",
69
- "js-yaml": "^4.1.0",
72
+ "js-yaml": "^4.2.0",
70
73
  "yargs": "^18.0.0",
71
74
  "zod": "^4.1.12"
72
75
  }