@intellectronica/ruler 0.2.8 → 0.2.10

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 CHANGED
@@ -35,20 +35,21 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
35
35
 
36
36
  ## Supported AI Agents
37
37
 
38
- | Agent | File(s) Created/Updated |
39
- | ---------------- | ------------------------------------------------------------- |
40
- | GitHub Copilot | `.github/copilot-instructions.md` |
41
- | Claude Code | `CLAUDE.md` |
42
- | OpenAI Codex CLI | `AGENTS.md` |
43
- | Jules | `AGENTS.md` |
44
- | Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` |
45
- | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
46
- | Cline | `.clinerules` |
47
- | Aider | `ruler_aider_instructions.md` and `.aider.conf.yml` |
48
- | Firebase Studio | `.idx/airules.md` |
49
- | Open Hands | `.openhands/microagents/repo.md` and `.openhands/config.toml` |
50
- | Gemini CLI | `GEMINI.md` and `.gemini/settings.json` |
51
- | Junie | `.junie/guidelines.md` |
38
+ | Agent | Rules File(s) | MCP Configuration |
39
+ | ---------------- | ------------------------------------------------ | ------------------------------------------------ |
40
+ | GitHub Copilot | `.github/copilot-instructions.md` | `.vscode/mcp.json` |
41
+ | Claude Code | `CLAUDE.md` | `claude_desktop_config.json` |
42
+ | OpenAI Codex CLI | `AGENTS.md` | `~/.codex/config.json` |
43
+ | Jules | `AGENTS.md` | - |
44
+ | Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` | `.cursor/mcp.json`, `~/.cursor/mcp.json` |
45
+ | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` | `~/.codeium/windsurf/mcp_config.json` |
46
+ | Cline | `.clinerules` | - |
47
+ | Aider | `ruler_aider_instructions.md`, `.aider.conf.yml` | `.mcp.json` |
48
+ | Firebase Studio | `.idx/airules.md` | - |
49
+ | Open Hands | `.openhands/microagents/repo.md` | `.openhands/config.toml` |
50
+ | Gemini CLI | `GEMINI.md` | `.gemini/settings.json` |
51
+ | Junie | `.junie/guidelines.md` | - |
52
+ | AugmentCode | `.augment/rules/ruler_augment_instructions.md` | `.vscode/settings.json` |
52
53
 
53
54
  ## Getting Started
54
55
 
@@ -144,7 +145,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
144
145
  | Option | Description |
145
146
  | ------------------------------ | --------------------------------------------------------- |
146
147
  | `--project-root <path>` | Path to your project's root (default: current directory) |
147
- | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie) |
148
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, augmentcode) |
148
149
  | `--config <path>` | Path to a custom `ruler.toml` configuration file |
149
150
  | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
150
151
  | `--no-mcp` | Disable applying MCP server configurations |
@@ -192,6 +193,68 @@ ruler apply --verbose
192
193
  ruler apply --no-mcp --no-gitignore
193
194
  ```
194
195
 
196
+ ## Usage: The `revert` Command
197
+
198
+ The `revert` command safely undoes all changes made by `ruler apply`, restoring your project to its pre-ruler state. It intelligently restores files from backups (`.bak` files) when available, or removes generated files that didn't exist before.
199
+
200
+ ### Why Revert is Needed
201
+
202
+ When experimenting with different rule configurations or switching between projects, you may want to:
203
+ - **Clean slate**: Remove all ruler-generated files to start fresh
204
+ - **Restore originals**: Revert modified files back to their original state
205
+ - **Selective cleanup**: Remove configurations for specific agents only
206
+ - **Safe experimentation**: Try ruler without fear of permanent changes
207
+
208
+ ### Primary Command
209
+
210
+ ```bash
211
+ ruler revert [options]
212
+ ```
213
+
214
+ ### Options
215
+
216
+ | Option | Description |
217
+ | ------------------------------ | --------------------------------------------------------- |
218
+ | `--project-root <path>` | Path to your project's root (default: current directory) |
219
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie) |
220
+ | `--config <path>` | Path to a custom `ruler.toml` configuration file |
221
+ | `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
222
+ | `--dry-run` | Preview changes without actually reverting files |
223
+ | `--verbose` / `-v` | Display detailed output during execution |
224
+ | `--local-only` | Only search for local .ruler directories, ignore global config |
225
+
226
+ ### Common Examples
227
+
228
+ **Revert all ruler changes:**
229
+
230
+ ```bash
231
+ ruler revert
232
+ ```
233
+
234
+ **Preview what would be reverted (dry-run):**
235
+
236
+ ```bash
237
+ ruler revert --dry-run
238
+ ```
239
+
240
+ **Revert only specific agents:**
241
+
242
+ ```bash
243
+ ruler revert --agents claude,copilot
244
+ ```
245
+
246
+ **Revert with detailed output:**
247
+
248
+ ```bash
249
+ ruler revert --verbose
250
+ ```
251
+
252
+ **Keep backup files after reverting:**
253
+
254
+ ```bash
255
+ ruler revert --keep-backups
256
+ ```
257
+
195
258
  ## Configuration (`ruler.toml`) in Detail
196
259
 
197
260
  ### Location
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AugmentCodeAgent = void 0;
37
+ const path = __importStar(require("path"));
38
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
39
+ const settings_1 = require("../vscode/settings");
40
+ /**
41
+ * AugmentCode agent adapter.
42
+ * Generates ruler_augment_instructions.md configuration file and updates VSCode settings.json with MCP server configuration.
43
+ */
44
+ class AugmentCodeAgent {
45
+ getIdentifier() {
46
+ return 'augmentcode';
47
+ }
48
+ getName() {
49
+ return 'AugmentCode';
50
+ }
51
+ async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
52
+ const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
53
+ await (0, FileSystemUtils_1.backupFile)(output);
54
+ await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
55
+ if (rulerMcpJson) {
56
+ const settingsPath = (0, settings_1.getVSCodeSettingsPath)(projectRoot);
57
+ await (0, FileSystemUtils_1.backupFile)(settingsPath);
58
+ const existingSettings = await (0, settings_1.readVSCodeSettings)(settingsPath);
59
+ const augmentServers = (0, settings_1.transformRulerToAugmentMcp)(rulerMcpJson);
60
+ const mergedSettings = (0, settings_1.mergeAugmentMcpServers)(existingSettings, augmentServers, agentConfig?.mcp?.strategy ?? 'merge');
61
+ await (0, settings_1.writeVSCodeSettings)(settingsPath, mergedSettings);
62
+ }
63
+ }
64
+ getDefaultOutputPath(projectRoot) {
65
+ return path.join(projectRoot, '.augment', 'rules', 'ruler_augment_instructions.md');
66
+ }
67
+ getMcpServerKey() {
68
+ return 'mcpServers';
69
+ }
70
+ }
71
+ exports.AugmentCodeAgent = AugmentCodeAgent;
@@ -40,6 +40,7 @@ exports.run = run;
40
40
  const yargs_1 = __importDefault(require("yargs"));
41
41
  const helpers_1 = require("yargs/helpers");
42
42
  const lib_1 = require("../lib");
43
+ const revert_1 = require("../revert");
43
44
  const path = __importStar(require("path"));
44
45
  const os = __importStar(require("os"));
45
46
  const fs_1 = require("fs");
@@ -240,6 +241,60 @@ and apply them to your configured AI coding agents.
240
241
  else {
241
242
  console.log(`[ruler] mcp.json already exists, skipping`);
242
243
  }
244
+ })
245
+ .command('revert', 'Revert ruler configurations by restoring backups and removing generated files', (y) => {
246
+ y.option('project-root', {
247
+ type: 'string',
248
+ description: 'Project root directory',
249
+ default: process.cwd(),
250
+ });
251
+ y.option('agents', {
252
+ type: 'string',
253
+ description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie',
254
+ });
255
+ y.option('config', {
256
+ type: 'string',
257
+ description: 'Path to TOML configuration file',
258
+ });
259
+ y.option('keep-backups', {
260
+ type: 'boolean',
261
+ description: 'Keep backup files (.bak) after restoration',
262
+ default: false,
263
+ });
264
+ y.option('verbose', {
265
+ type: 'boolean',
266
+ description: 'Enable verbose logging',
267
+ default: false,
268
+ });
269
+ y.alias('verbose', 'v');
270
+ y.option('dry-run', {
271
+ type: 'boolean',
272
+ description: 'Preview changes without actually reverting files',
273
+ default: false,
274
+ });
275
+ y.option('local-only', {
276
+ type: 'boolean',
277
+ description: 'Only search for local .ruler directories, ignore global config',
278
+ default: false,
279
+ });
280
+ }, async (argv) => {
281
+ const projectRoot = argv['project-root'];
282
+ const agents = argv.agents
283
+ ? argv.agents.split(',').map((a) => a.trim())
284
+ : undefined;
285
+ const configPath = argv.config;
286
+ const keepBackups = argv['keep-backups'];
287
+ const verbose = argv.verbose;
288
+ const dryRun = argv['dry-run'];
289
+ const localOnly = argv['local-only'];
290
+ try {
291
+ await (0, revert_1.revertAllAgentConfigs)(projectRoot, agents, configPath, keepBackups, verbose, dryRun, localOnly);
292
+ }
293
+ catch (err) {
294
+ const message = err instanceof Error ? err.message : String(err);
295
+ console.error(`${constants_1.ERROR_PREFIX} ${message}`);
296
+ process.exit(1);
297
+ }
243
298
  })
244
299
  .demandCommand(1, 'You need to specify a command')
245
300
  .help()
package/dist/lib.js CHANGED
@@ -52,6 +52,7 @@ const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
52
52
  const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
53
53
  const JulesAgent_1 = require("./agents/JulesAgent");
54
54
  const JunieAgent_1 = require("./agents/JunieAgent");
55
+ const AugmentCodeAgent_1 = require("./agents/AugmentCodeAgent");
55
56
  const merge_1 = require("./mcp/merge");
56
57
  const validate_1 = require("./mcp/validate");
57
58
  const mcp_1 = require("./paths/mcp");
@@ -104,6 +105,7 @@ const agents = [
104
105
  new GeminiCliAgent_1.GeminiCliAgent(),
105
106
  new JulesAgent_1.JulesAgent(),
106
107
  new JunieAgent_1.JunieAgent(),
108
+ new AugmentCodeAgent_1.AugmentCodeAgent(),
107
109
  ];
108
110
  /**
109
111
  * Applies ruler configurations for all supported AI agents.
@@ -215,7 +217,21 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
215
217
  }
216
218
  agentsMdWritten = true;
217
219
  }
218
- await agent.applyRulerConfig(concatenated, projectRoot, rulerMcpJson, agentConfig);
220
+ let finalAgentConfig = agentConfig;
221
+ if (agent.getIdentifier() === 'augmentcode' && rulerMcpJson) {
222
+ const resolvedStrategy = cliMcpStrategy ??
223
+ agentConfig?.mcp?.strategy ??
224
+ config.mcp?.strategy ??
225
+ 'merge';
226
+ finalAgentConfig = {
227
+ ...agentConfig,
228
+ mcp: {
229
+ ...agentConfig?.mcp,
230
+ strategy: resolvedStrategy,
231
+ },
232
+ };
233
+ }
234
+ await agent.applyRulerConfig(concatenated, projectRoot, rulerMcpJson, finalAgentConfig);
219
235
  }
220
236
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
221
237
  const mcpEnabledForAgent = cliMcpEnabled &&
@@ -239,6 +255,14 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
239
255
  }
240
256
  // Open Hands config file is already included above
241
257
  }
258
+ else if (agent.getIdentifier() === 'augmentcode') {
259
+ // *** Special handling for AugmentCode ***
260
+ // AugmentCode handles MCP configuration internally in applyRulerConfig
261
+ // by updating VSCode settings.json with augment.advanced.mcpServers format
262
+ if (dryRun) {
263
+ (0, constants_1.logVerbose)(`DRY RUN: AugmentCode MCP config handled internally via VSCode settings`, verbose);
264
+ }
265
+ }
242
266
  else {
243
267
  if (rulerMcpJson) {
244
268
  const strategy = cliMcpStrategy ??
package/dist/paths/mcp.js CHANGED
@@ -74,6 +74,9 @@ async function getNativeMcpPath(adapterName, projectRoot) {
74
74
  case 'Gemini CLI':
75
75
  candidates.push(path.join(projectRoot, '.gemini', 'settings.json'));
76
76
  break;
77
+ case 'AugmentCode':
78
+ candidates.push(path.join(projectRoot, '.vscode', 'settings.json'));
79
+ break;
77
80
  default:
78
81
  return null;
79
82
  }
package/dist/revert.js ADDED
@@ -0,0 +1,562 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.revertAllAgentConfigs = revertAllAgentConfigs;
37
+ const path = __importStar(require("path"));
38
+ const fs_1 = require("fs");
39
+ const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
40
+ const ConfigLoader_1 = require("./core/ConfigLoader");
41
+ const CopilotAgent_1 = require("./agents/CopilotAgent");
42
+ const ClaudeAgent_1 = require("./agents/ClaudeAgent");
43
+ const CodexCliAgent_1 = require("./agents/CodexCliAgent");
44
+ const CursorAgent_1 = require("./agents/CursorAgent");
45
+ const WindsurfAgent_1 = require("./agents/WindsurfAgent");
46
+ const ClineAgent = __importStar(require("./agents/ClineAgent"));
47
+ const AiderAgent_1 = require("./agents/AiderAgent");
48
+ const FirebaseAgent_1 = require("./agents/FirebaseAgent");
49
+ const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
50
+ const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
51
+ const JulesAgent_1 = require("./agents/JulesAgent");
52
+ const JunieAgent_1 = require("./agents/JunieAgent");
53
+ const AugmentCodeAgent_1 = require("./agents/AugmentCodeAgent");
54
+ const mcp_1 = require("./paths/mcp");
55
+ const constants_1 = require("./constants");
56
+ const settings_1 = require("./vscode/settings");
57
+ const agents = [
58
+ new CopilotAgent_1.CopilotAgent(),
59
+ new ClaudeAgent_1.ClaudeAgent(),
60
+ new CodexCliAgent_1.CodexCliAgent(),
61
+ new CursorAgent_1.CursorAgent(),
62
+ new WindsurfAgent_1.WindsurfAgent(),
63
+ new ClineAgent.ClineAgent(),
64
+ new AiderAgent_1.AiderAgent(),
65
+ new FirebaseAgent_1.FirebaseAgent(),
66
+ new OpenHandsAgent_1.OpenHandsAgent(),
67
+ new GeminiCliAgent_1.GeminiCliAgent(),
68
+ new JulesAgent_1.JulesAgent(),
69
+ new JunieAgent_1.JunieAgent(),
70
+ new AugmentCodeAgent_1.AugmentCodeAgent(),
71
+ ];
72
+ /**
73
+ * Gets all output paths for an agent, taking into account any config overrides.
74
+ * This is a copy of the function from lib.ts to maintain consistency.
75
+ */
76
+ function getAgentOutputPaths(agent, projectRoot, agentConfig) {
77
+ const paths = [];
78
+ const defaults = agent.getDefaultOutputPath(projectRoot);
79
+ if (typeof defaults === 'string') {
80
+ const actualPath = agentConfig?.outputPath ?? defaults;
81
+ paths.push(actualPath);
82
+ }
83
+ else {
84
+ const defaultPaths = defaults;
85
+ if ('instructions' in defaultPaths) {
86
+ const instructionsPath = agentConfig?.outputPathInstructions ?? defaultPaths.instructions;
87
+ paths.push(instructionsPath);
88
+ }
89
+ if ('config' in defaultPaths) {
90
+ const configPath = agentConfig?.outputPathConfig ?? defaultPaths.config;
91
+ paths.push(configPath);
92
+ }
93
+ for (const [key, defaultPath] of Object.entries(defaultPaths)) {
94
+ if (key !== 'instructions' && key !== 'config') {
95
+ paths.push(defaultPath);
96
+ }
97
+ }
98
+ }
99
+ return paths;
100
+ }
101
+ /**
102
+ * Checks if a file exists.
103
+ */
104
+ async function fileExists(filePath) {
105
+ try {
106
+ await fs_1.promises.access(filePath);
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * Restores a file from its backup if the backup exists.
115
+ */
116
+ async function restoreFromBackup(filePath, verbose, dryRun) {
117
+ const backupPath = `${filePath}.bak`;
118
+ const backupExists = await fileExists(backupPath);
119
+ if (!backupExists) {
120
+ (0, constants_1.logVerbose)(`No backup found for: ${filePath}`, verbose);
121
+ return false;
122
+ }
123
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
124
+ if (dryRun) {
125
+ (0, constants_1.logVerbose)(`${actionPrefix} Would restore: ${filePath} from backup`, verbose);
126
+ }
127
+ else {
128
+ await fs_1.promises.copyFile(backupPath, filePath);
129
+ (0, constants_1.logVerbose)(`${actionPrefix} Restored: ${filePath} from backup`, verbose);
130
+ }
131
+ return true;
132
+ }
133
+ /**
134
+ * Removes a file if it exists and has no backup (meaning it was generated by ruler).
135
+ */
136
+ async function removeGeneratedFile(filePath, verbose, dryRun) {
137
+ const fileExistsFlag = await fileExists(filePath);
138
+ const backupExists = await fileExists(`${filePath}.bak`);
139
+ if (!fileExistsFlag) {
140
+ (0, constants_1.logVerbose)(`File does not exist: ${filePath}`, verbose);
141
+ return false;
142
+ }
143
+ if (backupExists) {
144
+ (0, constants_1.logVerbose)(`File has backup, skipping removal: ${filePath}`, verbose);
145
+ return false;
146
+ }
147
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
148
+ if (dryRun) {
149
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove generated file: ${filePath}`, verbose);
150
+ }
151
+ else {
152
+ await fs_1.promises.unlink(filePath);
153
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed generated file: ${filePath}`, verbose);
154
+ }
155
+ return true;
156
+ }
157
+ /**
158
+ * Removes backup files.
159
+ */
160
+ async function removeBackupFile(filePath, verbose, dryRun) {
161
+ const backupPath = `${filePath}.bak`;
162
+ const backupExists = await fileExists(backupPath);
163
+ if (!backupExists) {
164
+ return false;
165
+ }
166
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
167
+ if (dryRun) {
168
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove backup file: ${backupPath}`, verbose);
169
+ }
170
+ else {
171
+ await fs_1.promises.unlink(backupPath);
172
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed backup file: ${backupPath}`, verbose);
173
+ }
174
+ return true;
175
+ }
176
+ /**
177
+ * Recursively checks if a directory contains only empty directories
178
+ */
179
+ async function isDirectoryTreeEmpty(dirPath) {
180
+ try {
181
+ const entries = await fs_1.promises.readdir(dirPath);
182
+ if (entries.length === 0) {
183
+ return true;
184
+ }
185
+ for (const entry of entries) {
186
+ const entryPath = path.join(dirPath, entry);
187
+ const entryStat = await fs_1.promises.stat(entryPath);
188
+ if (entryStat.isFile()) {
189
+ return false;
190
+ }
191
+ else if (entryStat.isDirectory()) {
192
+ const isEmpty = await isDirectoryTreeEmpty(entryPath);
193
+ if (!isEmpty) {
194
+ return false;
195
+ }
196
+ }
197
+ }
198
+ return true;
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
204
+ /**
205
+ * Helper function to execute directory removal with consistent dry-run handling and logging.
206
+ */
207
+ async function executeDirectoryAction(dirPath, action, verbose, dryRun) {
208
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
209
+ const actionText = action === 'remove-tree' ? 'directory tree' : 'directory';
210
+ if (dryRun) {
211
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove empty ${actionText}: ${dirPath}`, verbose);
212
+ }
213
+ else {
214
+ await fs_1.promises.rm(dirPath, { recursive: true });
215
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed empty ${actionText}: ${dirPath}`, verbose);
216
+ }
217
+ return true;
218
+ }
219
+ /**
220
+ * Attempts to remove a single empty directory if it exists and is empty.
221
+ */
222
+ async function removeEmptyDirectory(dirPath, verbose, dryRun, logMissing = false) {
223
+ try {
224
+ const stat = await fs_1.promises.stat(dirPath);
225
+ if (!stat.isDirectory()) {
226
+ return false;
227
+ }
228
+ const isEmpty = await isDirectoryTreeEmpty(dirPath);
229
+ if (isEmpty) {
230
+ return await executeDirectoryAction(dirPath, 'remove-tree', verbose, dryRun);
231
+ }
232
+ return false;
233
+ }
234
+ catch {
235
+ if (logMissing) {
236
+ (0, constants_1.logVerbose)(`Directory ${dirPath} doesn't exist or can't be accessed`, verbose);
237
+ }
238
+ return false;
239
+ }
240
+ }
241
+ /**
242
+ * Handles special cleanup logic for .augment directory and its rules subdirectory.
243
+ */
244
+ async function removeAugmentDirectory(projectRoot, verbose, dryRun) {
245
+ const augmentDir = path.join(projectRoot, '.augment');
246
+ let directoriesRemoved = 0;
247
+ try {
248
+ const augmentStat = await fs_1.promises.stat(augmentDir);
249
+ if (!augmentStat.isDirectory()) {
250
+ return 0;
251
+ }
252
+ const rulesDir = path.join(augmentDir, 'rules');
253
+ const rulesRemoved = await removeEmptyDirectory(rulesDir, verbose, dryRun);
254
+ if (rulesRemoved) {
255
+ directoriesRemoved++;
256
+ }
257
+ const augmentRemoved = await removeEmptyDirectory(augmentDir, verbose, dryRun);
258
+ if (augmentRemoved) {
259
+ directoriesRemoved++;
260
+ }
261
+ }
262
+ catch {
263
+ // .augment directory doesn't exist, that's fine. leaving comment as catch block can't be kept empty.
264
+ }
265
+ return directoriesRemoved;
266
+ }
267
+ /**
268
+ * Removes empty directories that were created by ruler.
269
+ * Only removes directories if they are empty and were likely created by ruler.
270
+ * Special handling for .augment directory to clean up rules subdirectory.
271
+ */
272
+ async function removeEmptyDirectories(projectRoot, verbose, dryRun) {
273
+ const rulerCreatedDirs = [
274
+ '.github',
275
+ '.cursor',
276
+ '.windsurf',
277
+ '.junie',
278
+ '.openhands',
279
+ '.idx',
280
+ '.gemini',
281
+ '.vscode',
282
+ '.augmentcode',
283
+ ];
284
+ let directoriesRemoved = 0;
285
+ // Handle .augment directory with special logic
286
+ directoriesRemoved += await removeAugmentDirectory(projectRoot, verbose, dryRun);
287
+ // Handle all other ruler-created directories
288
+ for (const dirName of rulerCreatedDirs) {
289
+ const dirPath = path.join(projectRoot, dirName);
290
+ const removed = await removeEmptyDirectory(dirPath, verbose, dryRun, true);
291
+ if (removed) {
292
+ directoriesRemoved++;
293
+ }
294
+ }
295
+ return directoriesRemoved;
296
+ }
297
+ /**
298
+ * Removes additional files created by specific agents that aren't covered by their main output paths.
299
+ */
300
+ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
301
+ const additionalFiles = [
302
+ '.gemini/settings.json',
303
+ 'claude_desktop_config.json',
304
+ '.mcp.json',
305
+ '.vscode/mcp.json',
306
+ '.cursor/mcp.json',
307
+ '.openhands/config.toml',
308
+ ];
309
+ let filesRemoved = 0;
310
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
311
+ for (const filePath of additionalFiles) {
312
+ const fullPath = path.join(projectRoot, filePath);
313
+ try {
314
+ const fileExistsFlag = await fileExists(fullPath);
315
+ if (!fileExistsFlag) {
316
+ continue;
317
+ }
318
+ const backupExists = await fileExists(`${fullPath}.bak`);
319
+ if (backupExists) {
320
+ const restored = await restoreFromBackup(fullPath, verbose, dryRun);
321
+ if (restored) {
322
+ filesRemoved++;
323
+ }
324
+ }
325
+ else {
326
+ if (dryRun) {
327
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove additional file: ${fullPath}`, verbose);
328
+ }
329
+ else {
330
+ await fs_1.promises.unlink(fullPath);
331
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed additional file: ${fullPath}`, verbose);
332
+ }
333
+ filesRemoved++;
334
+ }
335
+ }
336
+ catch {
337
+ (0, constants_1.logVerbose)(`Additional file ${fullPath} doesn't exist or can't be accessed`, verbose);
338
+ }
339
+ }
340
+ const settingsPath = (0, settings_1.getVSCodeSettingsPath)(projectRoot);
341
+ const backupPath = `${settingsPath}.bak`;
342
+ if (await fileExists(backupPath)) {
343
+ const restored = await restoreFromBackup(settingsPath, verbose, dryRun);
344
+ if (restored) {
345
+ filesRemoved++;
346
+ (0, constants_1.logVerbose)(`${actionPrefix} Restored VSCode settings from backup`, verbose);
347
+ }
348
+ }
349
+ else if (await fileExists(settingsPath)) {
350
+ try {
351
+ if (dryRun) {
352
+ const settings = await (0, settings_1.readVSCodeSettings)(settingsPath);
353
+ if (settings['augment.advanced']) {
354
+ delete settings['augment.advanced'];
355
+ const remainingKeys = Object.keys(settings);
356
+ if (remainingKeys.length === 0) {
357
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove empty VSCode settings file`, verbose);
358
+ }
359
+ else {
360
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
361
+ }
362
+ filesRemoved++;
363
+ }
364
+ }
365
+ else {
366
+ const settings = await (0, settings_1.readVSCodeSettings)(settingsPath);
367
+ if (settings['augment.advanced']) {
368
+ delete settings['augment.advanced'];
369
+ const remainingKeys = Object.keys(settings);
370
+ if (remainingKeys.length === 0) {
371
+ await fs_1.promises.unlink(settingsPath);
372
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed empty VSCode settings file`, verbose);
373
+ }
374
+ else {
375
+ await (0, settings_1.writeVSCodeSettings)(settingsPath, settings);
376
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed augment.advanced section from VSCode settings`, verbose);
377
+ }
378
+ filesRemoved++;
379
+ }
380
+ else {
381
+ (0, constants_1.logVerbose)(`No augment.advanced section found in ${settingsPath}`, verbose);
382
+ }
383
+ }
384
+ }
385
+ catch (error) {
386
+ (0, constants_1.logVerbose)(`Failed to process VSCode settings.json: ${error}`, verbose);
387
+ }
388
+ }
389
+ return filesRemoved;
390
+ }
391
+ /**
392
+ * Removes the ruler-managed block from .gitignore file.
393
+ */
394
+ async function cleanGitignore(projectRoot, verbose, dryRun) {
395
+ const gitignorePath = path.join(projectRoot, '.gitignore');
396
+ const gitignoreExists = await fileExists(gitignorePath);
397
+ if (!gitignoreExists) {
398
+ (0, constants_1.logVerbose)('No .gitignore file found', verbose);
399
+ return false;
400
+ }
401
+ const content = await fs_1.promises.readFile(gitignorePath, 'utf8');
402
+ const startMarker = '# START Ruler Generated Files';
403
+ const endMarker = '# END Ruler Generated Files';
404
+ const startIndex = content.indexOf(startMarker);
405
+ const endIndex = content.indexOf(endMarker);
406
+ if (startIndex === -1 || endIndex === -1) {
407
+ (0, constants_1.logVerbose)('No ruler-managed block found in .gitignore', verbose);
408
+ return false;
409
+ }
410
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
411
+ if (dryRun) {
412
+ (0, constants_1.logVerbose)(`${actionPrefix} Would remove ruler block from .gitignore`, verbose);
413
+ }
414
+ else {
415
+ const beforeBlock = content.substring(0, startIndex);
416
+ const afterBlock = content.substring(endIndex + endMarker.length);
417
+ let newContent = beforeBlock + afterBlock;
418
+ newContent = newContent.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
419
+ if (newContent.trim() === '') {
420
+ await fs_1.promises.unlink(gitignorePath);
421
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed empty .gitignore file`, verbose);
422
+ }
423
+ else {
424
+ await fs_1.promises.writeFile(gitignorePath, newContent);
425
+ (0, constants_1.logVerbose)(`${actionPrefix} Removed ruler block from .gitignore`, verbose);
426
+ }
427
+ }
428
+ return true;
429
+ }
430
+ /**
431
+ * Reverts ruler configurations for selected AI agents.
432
+ */
433
+ async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, keepBackups = false, verbose = false, dryRun = false, localOnly = false) {
434
+ (0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${projectRoot}`, verbose);
435
+ const config = await (0, ConfigLoader_1.loadConfig)({
436
+ projectRoot,
437
+ cliAgents: includedAgents,
438
+ configPath,
439
+ });
440
+ const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
441
+ if (!rulerDir) {
442
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
443
+ }
444
+ (0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
445
+ const rawConfigs = config.agentConfigs;
446
+ const mappedConfigs = {};
447
+ for (const [key, cfg] of Object.entries(rawConfigs)) {
448
+ const lowerKey = key.toLowerCase();
449
+ for (const agent of agents) {
450
+ const identifier = agent.getIdentifier();
451
+ if (identifier === lowerKey ||
452
+ agent.getName().toLowerCase().includes(lowerKey)) {
453
+ mappedConfigs[identifier] = cfg;
454
+ }
455
+ }
456
+ }
457
+ config.agentConfigs = mappedConfigs;
458
+ let selected = agents;
459
+ if (config.cliAgents && config.cliAgents.length > 0) {
460
+ const filters = config.cliAgents.map((n) => n.toLowerCase());
461
+ selected = agents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
462
+ agent.getName().toLowerCase().includes(f)));
463
+ (0, constants_1.logVerbose)(`Selected agents via CLI filter: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
464
+ }
465
+ else if (config.defaultAgents && config.defaultAgents.length > 0) {
466
+ const defaults = config.defaultAgents.map((n) => n.toLowerCase());
467
+ selected = agents.filter((agent) => {
468
+ const identifier = agent.getIdentifier();
469
+ const override = config.agentConfigs[identifier]?.enabled;
470
+ if (override !== undefined) {
471
+ return override;
472
+ }
473
+ return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
474
+ });
475
+ (0, constants_1.logVerbose)(`Selected agents via config default_agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
476
+ }
477
+ else {
478
+ selected = agents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
479
+ (0, constants_1.logVerbose)(`Selected all enabled agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
480
+ }
481
+ let totalFilesProcessed = 0;
482
+ let totalFilesRestored = 0;
483
+ let totalFilesRemoved = 0;
484
+ let totalBackupsRemoved = 0;
485
+ for (const agent of selected) {
486
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
487
+ console.log(`${actionPrefix} Reverting ${agent.getName()}...`);
488
+ const agentConfig = config.agentConfigs[agent.getIdentifier()];
489
+ const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
490
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
491
+ for (const outputPath of outputPaths) {
492
+ totalFilesProcessed++;
493
+ const restored = await restoreFromBackup(outputPath, verbose, dryRun);
494
+ if (restored) {
495
+ totalFilesRestored++;
496
+ if (!keepBackups) {
497
+ const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun);
498
+ if (backupRemoved) {
499
+ totalBackupsRemoved++;
500
+ }
501
+ }
502
+ }
503
+ else {
504
+ const removed = await removeGeneratedFile(outputPath, verbose, dryRun);
505
+ if (removed) {
506
+ totalFilesRemoved++;
507
+ }
508
+ }
509
+ }
510
+ const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
511
+ if (mcpPath && mcpPath.startsWith(projectRoot)) {
512
+ totalFilesProcessed++;
513
+ if (agent.getName() === 'AugmentCode' &&
514
+ mcpPath.endsWith('.vscode/settings.json')) {
515
+ (0, constants_1.logVerbose)(`Skipping MCP handling for AugmentCode settings.json - handled separately`, verbose);
516
+ }
517
+ else {
518
+ const mcpRestored = await restoreFromBackup(mcpPath, verbose, dryRun);
519
+ if (mcpRestored) {
520
+ totalFilesRestored++;
521
+ if (!keepBackups) {
522
+ const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun);
523
+ if (mcpBackupRemoved) {
524
+ totalBackupsRemoved++;
525
+ }
526
+ }
527
+ }
528
+ else {
529
+ const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun);
530
+ if (mcpRemoved) {
531
+ totalFilesRemoved++;
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+ const gitignoreCleaned = !config.cliAgents || config.cliAgents.length === 0
538
+ ? await cleanGitignore(projectRoot, verbose, dryRun)
539
+ : false;
540
+ const additionalFilesRemoved = await removeAdditionalAgentFiles(projectRoot, verbose, dryRun);
541
+ totalFilesRemoved += additionalFilesRemoved;
542
+ const directoriesRemoved = await removeEmptyDirectories(projectRoot, verbose, dryRun);
543
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
544
+ if (dryRun) {
545
+ console.log(`${actionPrefix} Revert summary (dry run):`);
546
+ }
547
+ else {
548
+ console.log(`${actionPrefix} Revert completed successfully.`);
549
+ }
550
+ console.log(` Files processed: ${totalFilesProcessed}`);
551
+ console.log(` Files restored from backup: ${totalFilesRestored}`);
552
+ console.log(` Generated files removed: ${totalFilesRemoved}`);
553
+ if (!keepBackups) {
554
+ console.log(` Backup files removed: ${totalBackupsRemoved}`);
555
+ }
556
+ if (directoriesRemoved > 0) {
557
+ console.log(` Empty directories removed: ${directoriesRemoved}`);
558
+ }
559
+ if (gitignoreCleaned) {
560
+ console.log(` .gitignore cleaned: yes`);
561
+ }
562
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readVSCodeSettings = readVSCodeSettings;
37
+ exports.writeVSCodeSettings = writeVSCodeSettings;
38
+ exports.transformRulerToAugmentMcp = transformRulerToAugmentMcp;
39
+ exports.mergeAugmentMcpServers = mergeAugmentMcpServers;
40
+ exports.getVSCodeSettingsPath = getVSCodeSettingsPath;
41
+ const fs_1 = require("fs");
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * Read VSCode settings.json file
45
+ */
46
+ async function readVSCodeSettings(settingsPath) {
47
+ try {
48
+ const content = await fs_1.promises.readFile(settingsPath, 'utf8');
49
+ return JSON.parse(content);
50
+ }
51
+ catch (error) {
52
+ if (error.code === 'ENOENT') {
53
+ return {};
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ /**
59
+ * Write VSCode settings.json file
60
+ */
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));
64
+ }
65
+ /**
66
+ * Transform ruler MCP config to Augment MCP server array format
67
+ */
68
+ function transformRulerToAugmentMcp(rulerMcpJson) {
69
+ const servers = [];
70
+ if (rulerMcpJson.mcpServers && typeof rulerMcpJson.mcpServers === 'object') {
71
+ const mcpServers = rulerMcpJson.mcpServers;
72
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
73
+ const augmentServer = {
74
+ name,
75
+ command: serverConfig.command,
76
+ };
77
+ if (serverConfig.args) {
78
+ augmentServer.args = serverConfig.args;
79
+ }
80
+ if (serverConfig.env) {
81
+ augmentServer.env = serverConfig.env;
82
+ }
83
+ servers.push(augmentServer);
84
+ }
85
+ }
86
+ return servers;
87
+ }
88
+ /**
89
+ * Merge MCP servers into VSCode settings using the specified strategy
90
+ */
91
+ function mergeAugmentMcpServers(existingSettings, newServers, strategy) {
92
+ const result = structuredClone(existingSettings);
93
+ if (!result['augment.advanced']) {
94
+ result['augment.advanced'] = {};
95
+ }
96
+ if (strategy === 'overwrite') {
97
+ result['augment.advanced'].mcpServers = newServers;
98
+ }
99
+ else {
100
+ const existingServers = result['augment.advanced'].mcpServers || [];
101
+ const existingServerMap = new Map();
102
+ for (const server of existingServers) {
103
+ existingServerMap.set(server.name, server);
104
+ }
105
+ for (const newServer of newServers) {
106
+ existingServerMap.set(newServer.name, newServer);
107
+ }
108
+ result['augment.advanced'].mcpServers = Array.from(existingServerMap.values());
109
+ }
110
+ return result;
111
+ }
112
+ /**
113
+ * Get the VSCode settings.json path for a project (local)
114
+ */
115
+ function getVSCodeSettingsPath(projectRoot) {
116
+ return path.join(projectRoot, '.vscode', 'settings.json');
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {