@intellectronica/ruler
Version:
Ruler — apply the same rules to all coding agents
572 lines (571 loc) • 23.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.revertAllAgentConfigs = revertAllAgentConfigs;
const path = __importStar(require("path"));
const fs_1 = require("fs");
const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
const ConfigLoader_1 = require("./core/ConfigLoader");
const CopilotAgent_1 = require("./agents/CopilotAgent");
const ClaudeAgent_1 = require("./agents/ClaudeAgent");
const CodexCliAgent_1 = require("./agents/CodexCliAgent");
const CursorAgent_1 = require("./agents/CursorAgent");
const WindsurfAgent_1 = require("./agents/WindsurfAgent");
const ClineAgent = __importStar(require("./agents/ClineAgent"));
const AiderAgent_1 = require("./agents/AiderAgent");
const FirebaseAgent_1 = require("./agents/FirebaseAgent");
const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
const JulesAgent_1 = require("./agents/JulesAgent");
const JunieAgent_1 = require("./agents/JunieAgent");
const AugmentCodeAgent_1 = require("./agents/AugmentCodeAgent");
const KiloCodeAgent_1 = require("./agents/KiloCodeAgent");
const OpenCodeAgent_1 = require("./agents/OpenCodeAgent");
const GooseAgent_1 = require("./agents/GooseAgent");
const AmpAgent_1 = require("./agents/AmpAgent");
const mcp_1 = require("./paths/mcp");
const constants_1 = require("./constants");
const settings_1 = require("./vscode/settings");
const agents = [
new CopilotAgent_1.CopilotAgent(),
new ClaudeAgent_1.ClaudeAgent(),
new CodexCliAgent_1.CodexCliAgent(),
new CursorAgent_1.CursorAgent(),
new WindsurfAgent_1.WindsurfAgent(),
new ClineAgent.ClineAgent(),
new AiderAgent_1.AiderAgent(),
new FirebaseAgent_1.FirebaseAgent(),
new OpenHandsAgent_1.OpenHandsAgent(),
new GeminiCliAgent_1.GeminiCliAgent(),
new JulesAgent_1.JulesAgent(),
new JunieAgent_1.JunieAgent(),
new AugmentCodeAgent_1.AugmentCodeAgent(),
new KiloCodeAgent_1.KiloCodeAgent(),
new OpenCodeAgent_1.OpenCodeAgent(),
new GooseAgent_1.GooseAgent(),
new AmpAgent_1.AmpAgent(),
];
/**
* Gets all output paths for an agent, taking into account any config overrides.
* This is a copy of the function from lib.ts to maintain consistency.
*/
function getAgentOutputPaths(agent, projectRoot, agentConfig) {
const paths = [];
const defaults = agent.getDefaultOutputPath(projectRoot);
if (typeof defaults === 'string') {
const actualPath = agentConfig?.outputPath ?? defaults;
paths.push(actualPath);
}
else {
const defaultPaths = defaults;
if ('instructions' in defaultPaths) {
const instructionsPath = agentConfig?.outputPathInstructions ?? defaultPaths.instructions;
paths.push(instructionsPath);
}
if ('config' in defaultPaths) {
const configPath = agentConfig?.outputPathConfig ?? defaultPaths.config;
paths.push(configPath);
}
for (const [key, defaultPath] of Object.entries(defaultPaths)) {
if (key !== 'instructions' && key !== 'config') {
paths.push(defaultPath);
}
}
}
return paths;
}
/**
* Checks if a file exists.
*/
async function fileExists(filePath) {
try {
await fs_1.promises.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* Restores a file from its backup if the backup exists.
*/
async function restoreFromBackup(filePath, verbose, dryRun) {
const backupPath = `${filePath}.bak`;
const backupExists = await fileExists(backupPath);
if (!backupExists) {
(0, constants_1.logVerbose)(`No backup found for: ${filePath}`, verbose);
return false;
}
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would restore: ${filePath} from backup`, verbose);
}
else {
await fs_1.promises.copyFile(backupPath, filePath);
(0, constants_1.logVerbose)(`${actionPrefix} Restored: ${filePath} from backup`, verbose);
}
return true;
}
/**
* Removes a file if it exists and has no backup (meaning it was generated by ruler).
*/
async function removeGeneratedFile(filePath, verbose, dryRun) {
const fileExistsFlag = await fileExists(filePath);
const backupExists = await fileExists(`${filePath}.bak`);
if (!fileExistsFlag) {
(0, constants_1.logVerbose)(`File does not exist: ${filePath}`, verbose);
return false;
}
if (backupExists) {
(0, constants_1.logVerbose)(`File has backup, skipping removal: ${filePath}`, verbose);
return false;
}
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove generated file: ${filePath}`, verbose);
}
else {
await fs_1.promises.unlink(filePath);
(0, constants_1.logVerbose)(`${actionPrefix} Removed generated file: ${filePath}`, verbose);
}
return true;
}
/**
* Removes backup files.
*/
async function removeBackupFile(filePath, verbose, dryRun) {
const backupPath = `${filePath}.bak`;
const backupExists = await fileExists(backupPath);
if (!backupExists) {
return false;
}
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove backup file: ${backupPath}`, verbose);
}
else {
await fs_1.promises.unlink(backupPath);
(0, constants_1.logVerbose)(`${actionPrefix} Removed backup file: ${backupPath}`, verbose);
}
return true;
}
/**
* Recursively checks if a directory contains only empty directories
*/
async function isDirectoryTreeEmpty(dirPath) {
try {
const entries = await fs_1.promises.readdir(dirPath);
if (entries.length === 0) {
return true;
}
for (const entry of entries) {
const entryPath = path.join(dirPath, entry);
const entryStat = await fs_1.promises.stat(entryPath);
if (entryStat.isFile()) {
return false;
}
else if (entryStat.isDirectory()) {
const isEmpty = await isDirectoryTreeEmpty(entryPath);
if (!isEmpty) {
return false;
}
}
}
return true;
}
catch {
return false;
}
}
/**
* Helper function to execute directory removal with consistent dry-run handling and logging.
*/
async function executeDirectoryAction(dirPath, action, verbose, dryRun) {
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
const actionText = action === 'remove-tree' ? 'directory tree' : 'directory';
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove empty ${actionText}: ${dirPath}`, verbose);
}
else {
await fs_1.promises.rm(dirPath, { recursive: true });
(0, constants_1.logVerbose)(`${actionPrefix} Removed empty ${actionText}: ${dirPath}`, verbose);
}
return true;
}
/**
* Attempts to remove a single empty directory if it exists and is empty.
*/
async function removeEmptyDirectory(dirPath, verbose, dryRun, logMissing = false) {
try {
const stat = await fs_1.promises.stat(dirPath);
if (!stat.isDirectory()) {
return false;
}
const isEmpty = await isDirectoryTreeEmpty(dirPath);
if (isEmpty) {
return await executeDirectoryAction(dirPath, 'remove-tree', verbose, dryRun);
}
return false;
}
catch {
if (logMissing) {
(0, constants_1.logVerbose)(`Directory ${dirPath} doesn't exist or can't be accessed`, verbose);
}
return false;
}
}
/**
* Handles special cleanup logic for .augment directory and its rules subdirectory.
*/
async function removeAugmentDirectory(projectRoot, verbose, dryRun) {
const augmentDir = path.join(projectRoot, '.augment');
let directoriesRemoved = 0;
try {
const augmentStat = await fs_1.promises.stat(augmentDir);
if (!augmentStat.isDirectory()) {
return 0;
}
const rulesDir = path.join(augmentDir, 'rules');
const rulesRemoved = await removeEmptyDirectory(rulesDir, verbose, dryRun);
if (rulesRemoved) {
directoriesRemoved++;
}
const augmentRemoved = await removeEmptyDirectory(augmentDir, verbose, dryRun);
if (augmentRemoved) {
directoriesRemoved++;
}
}
catch {
// .augment directory doesn't exist, that's fine. leaving comment as catch block can't be kept empty.
}
return directoriesRemoved;
}
/**
* Removes empty directories that were created by ruler.
* Only removes directories if they are empty and were likely created by ruler.
* Special handling for .augment directory to clean up rules subdirectory.
*/
async function removeEmptyDirectories(projectRoot, verbose, dryRun) {
const rulerCreatedDirs = [
'.github',
'.cursor',
'.windsurf',
'.junie',
'.openhands',
'.idx',
'.gemini',
'.vscode',
'.augmentcode',
'.kilocode',
];
let directoriesRemoved = 0;
// Handle .augment directory with special logic
directoriesRemoved += await removeAugmentDirectory(projectRoot, verbose, dryRun);
// Handle all other ruler-created directories
for (const dirName of rulerCreatedDirs) {
const dirPath = path.join(projectRoot, dirName);
const removed = await removeEmptyDirectory(dirPath, verbose, dryRun, true);
if (removed) {
directoriesRemoved++;
}
}
return directoriesRemoved;
}
/**
* Removes additional files created by specific agents that aren't covered by their main output paths.
*/
async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
const additionalFiles = [
'.gemini/settings.json',
'.mcp.json',
'.vscode/mcp.json',
'.cursor/mcp.json',
'.kilocode/mcp.json',
'.openhands/config.toml',
];
let filesRemoved = 0;
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
for (const filePath of additionalFiles) {
const fullPath = path.join(projectRoot, filePath);
try {
const fileExistsFlag = await fileExists(fullPath);
if (!fileExistsFlag) {
continue;
}
const backupExists = await fileExists(`${fullPath}.bak`);
if (backupExists) {
const restored = await restoreFromBackup(fullPath, verbose, dryRun);
if (restored) {
filesRemoved++;
}
}
else {
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove additional file: ${fullPath}`, verbose);
}
else {
await fs_1.promises.unlink(fullPath);
(0, constants_1.logVerbose)(`${actionPrefix} Removed additional file: ${fullPath}`, verbose);
}
filesRemoved++;
}
}
catch {
(0, constants_1.logVerbose)(`Additional file ${fullPath} doesn't exist or can't be accessed`, verbose);
}
}
const settingsPath = (0, settings_1.getVSCodeSettingsPath)(projectRoot);
const backupPath = `${settingsPath}.bak`;
if (await fileExists(backupPath)) {
const restored = await restoreFromBackup(settingsPath, verbose, dryRun);
if (restored) {
filesRemoved++;
(0, constants_1.logVerbose)(`${actionPrefix} Restored VSCode settings from backup`, verbose);
}
}
else if (await fileExists(settingsPath)) {
try {
if (dryRun) {
const settings = await (0, settings_1.readVSCodeSettings)(settingsPath);
if (settings['augment.advanced']) {
delete settings['augment.advanced'];
const remainingKeys = Object.keys(settings);
if (remainingKeys.length === 0) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove empty VSCode settings file`, verbose);
}
else {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove augment.advanced section from ${settingsPath}`, verbose);
}
filesRemoved++;
}
}
else {
const settings = await (0, settings_1.readVSCodeSettings)(settingsPath);
if (settings['augment.advanced']) {
delete settings['augment.advanced'];
const remainingKeys = Object.keys(settings);
if (remainingKeys.length === 0) {
await fs_1.promises.unlink(settingsPath);
(0, constants_1.logVerbose)(`${actionPrefix} Removed empty VSCode settings file`, verbose);
}
else {
await (0, settings_1.writeVSCodeSettings)(settingsPath, settings);
(0, constants_1.logVerbose)(`${actionPrefix} Removed augment.advanced section from VSCode settings`, verbose);
}
filesRemoved++;
}
else {
(0, constants_1.logVerbose)(`No augment.advanced section found in ${settingsPath}`, verbose);
}
}
}
catch (error) {
(0, constants_1.logVerbose)(`Failed to process VSCode settings.json: ${error}`, verbose);
}
}
return filesRemoved;
}
/**
* Removes the ruler-managed block from .gitignore file.
*/
async function cleanGitignore(projectRoot, verbose, dryRun) {
const gitignorePath = path.join(projectRoot, '.gitignore');
const gitignoreExists = await fileExists(gitignorePath);
if (!gitignoreExists) {
(0, constants_1.logVerbose)('No .gitignore file found', verbose);
return false;
}
const content = await fs_1.promises.readFile(gitignorePath, 'utf8');
const startMarker = '# START Ruler Generated Files';
const endMarker = '# END Ruler Generated Files';
const startIndex = content.indexOf(startMarker);
const endIndex = content.indexOf(endMarker);
if (startIndex === -1 || endIndex === -1) {
(0, constants_1.logVerbose)('No ruler-managed block found in .gitignore', verbose);
return false;
}
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
if (dryRun) {
(0, constants_1.logVerbose)(`${actionPrefix} Would remove ruler block from .gitignore`, verbose);
}
else {
const beforeBlock = content.substring(0, startIndex);
const afterBlock = content.substring(endIndex + endMarker.length);
let newContent = beforeBlock + afterBlock;
newContent = newContent.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
if (newContent.trim() === '') {
await fs_1.promises.unlink(gitignorePath);
(0, constants_1.logVerbose)(`${actionPrefix} Removed empty .gitignore file`, verbose);
}
else {
await fs_1.promises.writeFile(gitignorePath, newContent);
(0, constants_1.logVerbose)(`${actionPrefix} Removed ruler block from .gitignore`, verbose);
}
}
return true;
}
/**
* Reverts ruler configurations for selected AI agents.
*/
async function revertAllAgentConfigs(projectRoot, includedAgents, configPath, keepBackups = false, verbose = false, dryRun = false, localOnly = false) {
(0, constants_1.logVerbose)(`Loading configuration for revert from project root: ${projectRoot}`, verbose);
const config = await (0, ConfigLoader_1.loadConfig)({
projectRoot,
cliAgents: includedAgents,
configPath,
});
const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
if (!rulerDir) {
throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
}
(0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
const rawConfigs = config.agentConfigs;
const mappedConfigs = {};
for (const [key, cfg] of Object.entries(rawConfigs)) {
const lowerKey = key.toLowerCase();
for (const agent of agents) {
const identifier = agent.getIdentifier();
if (identifier === lowerKey ||
agent.getName().toLowerCase().includes(lowerKey)) {
mappedConfigs[identifier] = cfg;
}
}
}
config.agentConfigs = mappedConfigs;
let selected = agents;
if (config.cliAgents && config.cliAgents.length > 0) {
const filters = config.cliAgents.map((n) => n.toLowerCase());
selected = agents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
agent.getName().toLowerCase().includes(f)));
(0, constants_1.logVerbose)(`Selected agents via CLI filter: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
}
else if (config.defaultAgents && config.defaultAgents.length > 0) {
const defaults = config.defaultAgents.map((n) => n.toLowerCase());
selected = agents.filter((agent) => {
const identifier = agent.getIdentifier();
const override = config.agentConfigs[identifier]?.enabled;
if (override !== undefined) {
return override;
}
return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
});
(0, constants_1.logVerbose)(`Selected agents via config default_agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
}
else {
selected = agents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
(0, constants_1.logVerbose)(`Selected all enabled agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
}
let totalFilesProcessed = 0;
let totalFilesRestored = 0;
let totalFilesRemoved = 0;
let totalBackupsRemoved = 0;
for (const agent of selected) {
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
console.log(`${actionPrefix} Reverting ${agent.getName()}...`);
const agentConfig = config.agentConfigs[agent.getIdentifier()];
const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
(0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
for (const outputPath of outputPaths) {
totalFilesProcessed++;
const restored = await restoreFromBackup(outputPath, verbose, dryRun);
if (restored) {
totalFilesRestored++;
if (!keepBackups) {
const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun);
if (backupRemoved) {
totalBackupsRemoved++;
}
}
}
else {
const removed = await removeGeneratedFile(outputPath, verbose, dryRun);
if (removed) {
totalFilesRemoved++;
}
}
}
const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
if (mcpPath && mcpPath.startsWith(projectRoot)) {
totalFilesProcessed++;
if (agent.getName() === 'AugmentCode' &&
mcpPath.endsWith('.vscode/settings.json')) {
(0, constants_1.logVerbose)(`Skipping MCP handling for AugmentCode settings.json - handled separately`, verbose);
}
else {
const mcpRestored = await restoreFromBackup(mcpPath, verbose, dryRun);
if (mcpRestored) {
totalFilesRestored++;
if (!keepBackups) {
const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun);
if (mcpBackupRemoved) {
totalBackupsRemoved++;
}
}
}
else {
const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun);
if (mcpRemoved) {
totalFilesRemoved++;
}
}
}
}
}
const gitignoreCleaned = !config.cliAgents || config.cliAgents.length === 0
? await cleanGitignore(projectRoot, verbose, dryRun)
: false;
const additionalFilesRemoved = await removeAdditionalAgentFiles(projectRoot, verbose, dryRun);
totalFilesRemoved += additionalFilesRemoved;
const directoriesRemoved = await removeEmptyDirectories(projectRoot, verbose, dryRun);
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
if (dryRun) {
console.log(`${actionPrefix} Revert summary (dry run):`);
}
else {
console.log(`${actionPrefix} Revert completed successfully.`);
}
console.log(` Files processed: ${totalFilesProcessed}`);
console.log(` Files restored from backup: ${totalFilesRestored}`);
console.log(` Generated files removed: ${totalFilesRemoved}`);
if (!keepBackups) {
console.log(` Backup files removed: ${totalBackupsRemoved}`);
}
if (directoriesRemoved > 0) {
console.log(` Empty directories removed: ${directoriesRemoved}`);
}
if (gitignoreCleaned) {
console.log(` .gitignore cleaned: yes`);
}
}