@intellectronica/ruler 0.2.7 → 0.2.9
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 +69 -1
- package/dist/agents/AugmentCodeAgent.js +81 -0
- package/dist/agents/JunieAgent.js +60 -0
- package/dist/cli/commands.js +56 -1
- package/dist/lib.js +4 -0
- package/dist/paths/mcp.js +4 -0
- package/dist/revert.js +453 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
48
48
|
| Firebase Studio | `.idx/airules.md` |
|
|
49
49
|
| Open Hands | `.openhands/microagents/repo.md` and `.openhands/config.toml` |
|
|
50
50
|
| Gemini CLI | `GEMINI.md` and `.gemini/settings.json` |
|
|
51
|
+
| Junie | `.junie/guidelines.md` |
|
|
52
|
+
| AugmentCode | `.augment-guidelines` and `.augmentcode/config.json` |
|
|
51
53
|
|
|
52
54
|
## Getting Started
|
|
53
55
|
|
|
@@ -143,7 +145,7 @@ The `apply` command looks for `.ruler/` in the current directory tree, reading t
|
|
|
143
145
|
| Option | Description |
|
|
144
146
|
| ------------------------------ | --------------------------------------------------------- |
|
|
145
147
|
| `--project-root <path>` | Path to your project's root (default: current directory) |
|
|
146
|
-
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target
|
|
148
|
+
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target (copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie, augmentcode) |
|
|
147
149
|
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
|
|
148
150
|
| `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
|
|
149
151
|
| `--no-mcp` | Disable applying MCP server configurations |
|
|
@@ -191,6 +193,68 @@ ruler apply --verbose
|
|
|
191
193
|
ruler apply --no-mcp --no-gitignore
|
|
192
194
|
```
|
|
193
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
|
+
|
|
194
258
|
## Configuration (`ruler.toml`) in Detail
|
|
195
259
|
|
|
196
260
|
### Location
|
|
@@ -240,6 +304,10 @@ enabled = true
|
|
|
240
304
|
[agents.jules]
|
|
241
305
|
enabled = true
|
|
242
306
|
|
|
307
|
+
[agents.junie]
|
|
308
|
+
enabled = true
|
|
309
|
+
output_path = ".junie/guidelines.md"
|
|
310
|
+
|
|
243
311
|
# Agent-specific MCP configuration
|
|
244
312
|
[agents.cursor.mcp]
|
|
245
313
|
enabled = true
|
|
@@ -0,0 +1,81 @@
|
|
|
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 fs_1 = require("fs");
|
|
39
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
40
|
+
const merge_1 = require("../mcp/merge");
|
|
41
|
+
/**
|
|
42
|
+
* AugmentCode agent adapter.
|
|
43
|
+
* Generates .augment-guidelines configuration file and supports MCP server configuration.
|
|
44
|
+
*/
|
|
45
|
+
class AugmentCodeAgent {
|
|
46
|
+
getIdentifier() {
|
|
47
|
+
return 'augmentcode';
|
|
48
|
+
}
|
|
49
|
+
getName() {
|
|
50
|
+
return 'AugmentCode';
|
|
51
|
+
}
|
|
52
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
|
|
53
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
54
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
55
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
56
|
+
// Handle MCP configuration if provided
|
|
57
|
+
if (rulerMcpJson) {
|
|
58
|
+
const configPath = path.join(projectRoot, '.augmentcode', 'config.json');
|
|
59
|
+
let existingConfig = {};
|
|
60
|
+
try {
|
|
61
|
+
const existingConfigRaw = await fs_1.promises.readFile(configPath, 'utf8');
|
|
62
|
+
existingConfig = JSON.parse(existingConfigRaw);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code !== 'ENOENT') {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const merged = (0, merge_1.mergeMcp)(existingConfig, rulerMcpJson, agentConfig?.mcp?.strategy ?? 'merge', this.getMcpServerKey());
|
|
70
|
+
await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
71
|
+
await fs_1.promises.writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
getDefaultOutputPath(projectRoot) {
|
|
75
|
+
return path.join(projectRoot, '.augment-guidelines');
|
|
76
|
+
}
|
|
77
|
+
getMcpServerKey() {
|
|
78
|
+
return 'mcpServers';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.AugmentCodeAgent = AugmentCodeAgent;
|
|
@@ -0,0 +1,60 @@
|
|
|
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.JunieAgent = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
39
|
+
/**
|
|
40
|
+
* JetBrains Junie agent adapter.
|
|
41
|
+
*/
|
|
42
|
+
class JunieAgent {
|
|
43
|
+
getIdentifier() {
|
|
44
|
+
return 'junie';
|
|
45
|
+
}
|
|
46
|
+
getName() {
|
|
47
|
+
return 'Junie';
|
|
48
|
+
}
|
|
49
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
50
|
+
agentConfig) {
|
|
51
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
52
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
|
|
53
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
54
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
55
|
+
}
|
|
56
|
+
getDefaultOutputPath(projectRoot) {
|
|
57
|
+
return path.join(projectRoot, '.junie', 'guidelines.md');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.JunieAgent = JunieAgent;
|
package/dist/cli/commands.js
CHANGED
|
@@ -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");
|
|
@@ -59,7 +60,7 @@ function run() {
|
|
|
59
60
|
});
|
|
60
61
|
y.option('agents', {
|
|
61
62
|
type: 'string',
|
|
62
|
-
description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli',
|
|
63
|
+
description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider, firebase, gemini-cli, junie',
|
|
63
64
|
});
|
|
64
65
|
y.option('config', {
|
|
65
66
|
type: 'string',
|
|
@@ -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
|
@@ -51,6 +51,8 @@ const FirebaseAgent_1 = require("./agents/FirebaseAgent");
|
|
|
51
51
|
const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
|
|
52
52
|
const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
|
|
53
53
|
const JulesAgent_1 = require("./agents/JulesAgent");
|
|
54
|
+
const JunieAgent_1 = require("./agents/JunieAgent");
|
|
55
|
+
const AugmentCodeAgent_1 = require("./agents/AugmentCodeAgent");
|
|
54
56
|
const merge_1 = require("./mcp/merge");
|
|
55
57
|
const validate_1 = require("./mcp/validate");
|
|
56
58
|
const mcp_1 = require("./paths/mcp");
|
|
@@ -102,6 +104,8 @@ const agents = [
|
|
|
102
104
|
new OpenHandsAgent_1.OpenHandsAgent(),
|
|
103
105
|
new GeminiCliAgent_1.GeminiCliAgent(),
|
|
104
106
|
new JulesAgent_1.JulesAgent(),
|
|
107
|
+
new JunieAgent_1.JunieAgent(),
|
|
108
|
+
new AugmentCodeAgent_1.AugmentCodeAgent(),
|
|
105
109
|
];
|
|
106
110
|
/**
|
|
107
111
|
* Applies ruler configurations for all supported AI agents.
|
package/dist/paths/mcp.js
CHANGED
|
@@ -74,6 +74,10 @@ 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, '.augmentcode', 'config.json'));
|
|
79
|
+
candidates.push(path.join(home, '.augmentcode', 'config.json'));
|
|
80
|
+
break;
|
|
77
81
|
default:
|
|
78
82
|
return null;
|
|
79
83
|
}
|
package/dist/revert.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
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 mcp_1 = require("./paths/mcp");
|
|
54
|
+
const constants_1 = require("./constants");
|
|
55
|
+
const agents = [
|
|
56
|
+
new CopilotAgent_1.CopilotAgent(),
|
|
57
|
+
new ClaudeAgent_1.ClaudeAgent(),
|
|
58
|
+
new CodexCliAgent_1.CodexCliAgent(),
|
|
59
|
+
new CursorAgent_1.CursorAgent(),
|
|
60
|
+
new WindsurfAgent_1.WindsurfAgent(),
|
|
61
|
+
new ClineAgent.ClineAgent(),
|
|
62
|
+
new AiderAgent_1.AiderAgent(),
|
|
63
|
+
new FirebaseAgent_1.FirebaseAgent(),
|
|
64
|
+
new OpenHandsAgent_1.OpenHandsAgent(),
|
|
65
|
+
new GeminiCliAgent_1.GeminiCliAgent(),
|
|
66
|
+
new JulesAgent_1.JulesAgent(),
|
|
67
|
+
new JunieAgent_1.JunieAgent(),
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Gets all output paths for an agent, taking into account any config overrides.
|
|
71
|
+
* This is a copy of the function from lib.ts to maintain consistency.
|
|
72
|
+
*/
|
|
73
|
+
function getAgentOutputPaths(agent, projectRoot, agentConfig) {
|
|
74
|
+
const paths = [];
|
|
75
|
+
const defaults = agent.getDefaultOutputPath(projectRoot);
|
|
76
|
+
if (typeof defaults === 'string') {
|
|
77
|
+
const actualPath = agentConfig?.outputPath ?? defaults;
|
|
78
|
+
paths.push(actualPath);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const defaultPaths = defaults;
|
|
82
|
+
if ('instructions' in defaultPaths) {
|
|
83
|
+
const instructionsPath = agentConfig?.outputPathInstructions ?? defaultPaths.instructions;
|
|
84
|
+
paths.push(instructionsPath);
|
|
85
|
+
}
|
|
86
|
+
if ('config' in defaultPaths) {
|
|
87
|
+
const configPath = agentConfig?.outputPathConfig ?? defaultPaths.config;
|
|
88
|
+
paths.push(configPath);
|
|
89
|
+
}
|
|
90
|
+
for (const [key, defaultPath] of Object.entries(defaultPaths)) {
|
|
91
|
+
if (key !== 'instructions' && key !== 'config') {
|
|
92
|
+
paths.push(defaultPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return paths;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a file exists.
|
|
100
|
+
*/
|
|
101
|
+
async function fileExists(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
await fs_1.promises.access(filePath);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Restores a file from its backup if the backup exists.
|
|
112
|
+
*/
|
|
113
|
+
async function restoreFromBackup(filePath, verbose, dryRun) {
|
|
114
|
+
const backupPath = `${filePath}.bak`;
|
|
115
|
+
const backupExists = await fileExists(backupPath);
|
|
116
|
+
if (!backupExists) {
|
|
117
|
+
(0, constants_1.logVerbose)(`No backup found for: ${filePath}`, verbose);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would restore: ${filePath} from backup`, verbose);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await fs_1.promises.copyFile(backupPath, filePath);
|
|
126
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Restored: ${filePath} from backup`, verbose);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Removes a file if it exists and has no backup (meaning it was generated by ruler).
|
|
132
|
+
*/
|
|
133
|
+
async function removeGeneratedFile(filePath, verbose, dryRun) {
|
|
134
|
+
const fileExistsFlag = await fileExists(filePath);
|
|
135
|
+
const backupExists = await fileExists(`${filePath}.bak`);
|
|
136
|
+
if (!fileExistsFlag) {
|
|
137
|
+
(0, constants_1.logVerbose)(`File does not exist: ${filePath}`, verbose);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (backupExists) {
|
|
141
|
+
(0, constants_1.logVerbose)(`File has backup, skipping removal: ${filePath}`, verbose);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
145
|
+
if (dryRun) {
|
|
146
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove generated file: ${filePath}`, verbose);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
await fs_1.promises.unlink(filePath);
|
|
150
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed generated file: ${filePath}`, verbose);
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Removes backup files.
|
|
156
|
+
*/
|
|
157
|
+
async function removeBackupFile(filePath, verbose, dryRun) {
|
|
158
|
+
const backupPath = `${filePath}.bak`;
|
|
159
|
+
const backupExists = await fileExists(backupPath);
|
|
160
|
+
if (!backupExists) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove backup file: ${backupPath}`, verbose);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await fs_1.promises.unlink(backupPath);
|
|
169
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed backup file: ${backupPath}`, verbose);
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Removes empty directories that were created by ruler.
|
|
175
|
+
* Only removes directories if they are empty and were likely created by ruler.
|
|
176
|
+
*/
|
|
177
|
+
async function removeEmptyDirectories(projectRoot, verbose, dryRun) {
|
|
178
|
+
const rulerCreatedDirs = [
|
|
179
|
+
'.github',
|
|
180
|
+
'.cursor',
|
|
181
|
+
'.windsurf',
|
|
182
|
+
'.junie',
|
|
183
|
+
'.openhands',
|
|
184
|
+
'.idx',
|
|
185
|
+
'.gemini',
|
|
186
|
+
'.vscode',
|
|
187
|
+
];
|
|
188
|
+
let directoriesRemoved = 0;
|
|
189
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
190
|
+
for (const dirName of rulerCreatedDirs) {
|
|
191
|
+
const dirPath = path.join(projectRoot, dirName);
|
|
192
|
+
try {
|
|
193
|
+
const stat = await fs_1.promises.stat(dirPath);
|
|
194
|
+
if (!stat.isDirectory()) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const entries = await fs_1.promises.readdir(dirPath);
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
if (dryRun) {
|
|
200
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove empty directory: ${dirPath}`, verbose);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
await fs_1.promises.rmdir(dirPath);
|
|
204
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed empty directory: ${dirPath}`, verbose);
|
|
205
|
+
}
|
|
206
|
+
directoriesRemoved++;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
let hasNonEmptyContent = false;
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
const entryPath = path.join(dirPath, entry);
|
|
212
|
+
const entryStat = await fs_1.promises.stat(entryPath);
|
|
213
|
+
if (entryStat.isFile()) {
|
|
214
|
+
hasNonEmptyContent = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
else if (entryStat.isDirectory()) {
|
|
218
|
+
const subEntries = await fs_1.promises.readdir(entryPath);
|
|
219
|
+
if (subEntries.length > 0) {
|
|
220
|
+
hasNonEmptyContent = true;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!hasNonEmptyContent) {
|
|
226
|
+
if (dryRun) {
|
|
227
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove directory tree: ${dirPath}`, verbose);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
await fs_1.promises.rm(dirPath, { recursive: true });
|
|
231
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed directory tree: ${dirPath}`, verbose);
|
|
232
|
+
}
|
|
233
|
+
directoriesRemoved++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
(0, constants_1.logVerbose)(`Directory ${dirPath} doesn't exist or can't be accessed`, verbose);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return directoriesRemoved;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Removes additional files created by specific agents that aren't covered by their main output paths.
|
|
245
|
+
*/
|
|
246
|
+
async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
247
|
+
const additionalFiles = [
|
|
248
|
+
'.gemini/settings.json',
|
|
249
|
+
'claude_desktop_config.json',
|
|
250
|
+
'.mcp.json',
|
|
251
|
+
'.vscode/mcp.json',
|
|
252
|
+
'.cursor/mcp.json',
|
|
253
|
+
'.openhands/config.toml',
|
|
254
|
+
];
|
|
255
|
+
let filesRemoved = 0;
|
|
256
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
257
|
+
for (const filePath of additionalFiles) {
|
|
258
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
259
|
+
try {
|
|
260
|
+
const fileExistsFlag = await fileExists(fullPath);
|
|
261
|
+
if (!fileExistsFlag) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const backupExists = await fileExists(`${fullPath}.bak`);
|
|
265
|
+
if (backupExists) {
|
|
266
|
+
const restored = await restoreFromBackup(fullPath, verbose, dryRun);
|
|
267
|
+
if (restored) {
|
|
268
|
+
filesRemoved++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
if (dryRun) {
|
|
273
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove additional file: ${fullPath}`, verbose);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
await fs_1.promises.unlink(fullPath);
|
|
277
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed additional file: ${fullPath}`, verbose);
|
|
278
|
+
}
|
|
279
|
+
filesRemoved++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
(0, constants_1.logVerbose)(`Additional file ${fullPath} doesn't exist or can't be accessed`, verbose);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return filesRemoved;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Removes the ruler-managed block from .gitignore file.
|
|
290
|
+
*/
|
|
291
|
+
async function cleanGitignore(projectRoot, verbose, dryRun) {
|
|
292
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
293
|
+
const gitignoreExists = await fileExists(gitignorePath);
|
|
294
|
+
if (!gitignoreExists) {
|
|
295
|
+
(0, constants_1.logVerbose)('No .gitignore file found', verbose);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const content = await fs_1.promises.readFile(gitignorePath, 'utf8');
|
|
299
|
+
const startMarker = '# START Ruler Generated Files';
|
|
300
|
+
const endMarker = '# END Ruler Generated Files';
|
|
301
|
+
const startIndex = content.indexOf(startMarker);
|
|
302
|
+
const endIndex = content.indexOf(endMarker);
|
|
303
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
304
|
+
(0, constants_1.logVerbose)('No ruler-managed block found in .gitignore', verbose);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
308
|
+
if (dryRun) {
|
|
309
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Would remove ruler block from .gitignore`, verbose);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const beforeBlock = content.substring(0, startIndex);
|
|
313
|
+
const afterBlock = content.substring(endIndex + endMarker.length);
|
|
314
|
+
let newContent = beforeBlock + afterBlock;
|
|
315
|
+
newContent = newContent.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
|
|
316
|
+
if (newContent.trim() === '') {
|
|
317
|
+
await fs_1.promises.unlink(gitignorePath);
|
|
318
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed empty .gitignore file`, verbose);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
await fs_1.promises.writeFile(gitignorePath, newContent);
|
|
322
|
+
(0, constants_1.logVerbose)(`${actionPrefix} Removed ruler block from .gitignore`, verbose);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reverts ruler configurations for selected AI agents.
|
|
329
|
+
*/
|
|
330
|
+
async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, keepBackups = false, verbose = false, dryRun = false, localOnly = false) {
|
|
331
|
+
(0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${projectRoot}`, verbose);
|
|
332
|
+
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
333
|
+
projectRoot,
|
|
334
|
+
cliAgents: includedAgents,
|
|
335
|
+
configPath,
|
|
336
|
+
});
|
|
337
|
+
const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
|
|
338
|
+
if (!rulerDir) {
|
|
339
|
+
throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
|
|
340
|
+
}
|
|
341
|
+
(0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
|
|
342
|
+
const rawConfigs = config.agentConfigs;
|
|
343
|
+
const mappedConfigs = {};
|
|
344
|
+
for (const [key, cfg] of Object.entries(rawConfigs)) {
|
|
345
|
+
const lowerKey = key.toLowerCase();
|
|
346
|
+
for (const agent of agents) {
|
|
347
|
+
const identifier = agent.getIdentifier();
|
|
348
|
+
if (identifier === lowerKey ||
|
|
349
|
+
agent.getName().toLowerCase().includes(lowerKey)) {
|
|
350
|
+
mappedConfigs[identifier] = cfg;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
config.agentConfigs = mappedConfigs;
|
|
355
|
+
let selected = agents;
|
|
356
|
+
if (config.cliAgents && config.cliAgents.length > 0) {
|
|
357
|
+
const filters = config.cliAgents.map((n) => n.toLowerCase());
|
|
358
|
+
selected = agents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
|
|
359
|
+
agent.getName().toLowerCase().includes(f)));
|
|
360
|
+
(0, constants_1.logVerbose)(`Selected agents via CLI filter: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
|
|
361
|
+
}
|
|
362
|
+
else if (config.defaultAgents && config.defaultAgents.length > 0) {
|
|
363
|
+
const defaults = config.defaultAgents.map((n) => n.toLowerCase());
|
|
364
|
+
selected = agents.filter((agent) => {
|
|
365
|
+
const identifier = agent.getIdentifier();
|
|
366
|
+
const override = config.agentConfigs[identifier]?.enabled;
|
|
367
|
+
if (override !== undefined) {
|
|
368
|
+
return override;
|
|
369
|
+
}
|
|
370
|
+
return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
|
|
371
|
+
});
|
|
372
|
+
(0, constants_1.logVerbose)(`Selected agents via config default_agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
selected = agents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
|
|
376
|
+
(0, constants_1.logVerbose)(`Selected all enabled agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
|
|
377
|
+
}
|
|
378
|
+
let totalFilesProcessed = 0;
|
|
379
|
+
let totalFilesRestored = 0;
|
|
380
|
+
let totalFilesRemoved = 0;
|
|
381
|
+
let totalBackupsRemoved = 0;
|
|
382
|
+
for (const agent of selected) {
|
|
383
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
384
|
+
console.log(`${actionPrefix} Reverting ${agent.getName()}...`);
|
|
385
|
+
const agentConfig = config.agentConfigs[agent.getIdentifier()];
|
|
386
|
+
const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
|
|
387
|
+
(0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
|
|
388
|
+
for (const outputPath of outputPaths) {
|
|
389
|
+
totalFilesProcessed++;
|
|
390
|
+
const restored = await restoreFromBackup(outputPath, verbose, dryRun);
|
|
391
|
+
if (restored) {
|
|
392
|
+
totalFilesRestored++;
|
|
393
|
+
if (!keepBackups) {
|
|
394
|
+
const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun);
|
|
395
|
+
if (backupRemoved) {
|
|
396
|
+
totalBackupsRemoved++;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
const removed = await removeGeneratedFile(outputPath, verbose, dryRun);
|
|
402
|
+
if (removed) {
|
|
403
|
+
totalFilesRemoved++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
408
|
+
if (mcpPath && mcpPath.startsWith(projectRoot)) {
|
|
409
|
+
totalFilesProcessed++;
|
|
410
|
+
const mcpRestored = await restoreFromBackup(mcpPath, verbose, dryRun);
|
|
411
|
+
if (mcpRestored) {
|
|
412
|
+
totalFilesRestored++;
|
|
413
|
+
if (!keepBackups) {
|
|
414
|
+
const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun);
|
|
415
|
+
if (mcpBackupRemoved) {
|
|
416
|
+
totalBackupsRemoved++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun);
|
|
422
|
+
if (mcpRemoved) {
|
|
423
|
+
totalFilesRemoved++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const gitignoreCleaned = !config.cliAgents || config.cliAgents.length === 0
|
|
429
|
+
? await cleanGitignore(projectRoot, verbose, dryRun)
|
|
430
|
+
: false;
|
|
431
|
+
const additionalFilesRemoved = await removeAdditionalAgentFiles(projectRoot, verbose, dryRun);
|
|
432
|
+
totalFilesRemoved += additionalFilesRemoved;
|
|
433
|
+
const directoriesRemoved = await removeEmptyDirectories(projectRoot, verbose, dryRun);
|
|
434
|
+
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
435
|
+
if (dryRun) {
|
|
436
|
+
console.log(`${actionPrefix} Revert summary (dry run):`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.log(`${actionPrefix} Revert completed successfully.`);
|
|
440
|
+
}
|
|
441
|
+
console.log(` Files processed: ${totalFilesProcessed}`);
|
|
442
|
+
console.log(` Files restored from backup: ${totalFilesRestored}`);
|
|
443
|
+
console.log(` Generated files removed: ${totalFilesRemoved}`);
|
|
444
|
+
if (!keepBackups) {
|
|
445
|
+
console.log(` Backup files removed: ${totalBackupsRemoved}`);
|
|
446
|
+
}
|
|
447
|
+
if (directoriesRemoved > 0) {
|
|
448
|
+
console.log(` Empty directories removed: ${directoriesRemoved}`);
|
|
449
|
+
}
|
|
450
|
+
if (gitignoreCleaned) {
|
|
451
|
+
console.log(` .gitignore cleaned: yes`);
|
|
452
|
+
}
|
|
453
|
+
}
|