UNPKG

@intellectronica/ruler

Version:

Ruler — apply the same rules to all coding agents

414 lines (413 loc) 15.6 kB
"use strict"; 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.revertAgentConfiguration = revertAgentConfiguration; exports.cleanUpAuxiliaryFiles = cleanUpAuxiliaryFiles; const path = __importStar(require("path")); const fs_1 = require("fs"); const agent_utils_1 = require("../agents/agent-utils"); const mcp_1 = require("../paths/mcp"); const constants_1 = require("../constants"); const settings_1 = require("../vscode/settings"); /** * 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 prefix = (0, constants_1.actionPrefix)(dryRun); if (dryRun) { (0, constants_1.logVerbose)(`${prefix} Would restore: ${filePath} from backup`, verbose); } else { await fs_1.promises.copyFile(backupPath, filePath); (0, constants_1.logVerbose)(`${prefix} 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 prefix = (0, constants_1.actionPrefix)(dryRun); if (dryRun) { (0, constants_1.logVerbose)(`${prefix} Would remove generated file: ${filePath}`, verbose); } else { await fs_1.promises.unlink(filePath); (0, constants_1.logVerbose)(`${prefix} 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 prefix = (0, constants_1.actionPrefix)(dryRun); if (dryRun) { (0, constants_1.logVerbose)(`${prefix} Would remove backup file: ${backupPath}`, verbose); } else { await fs_1.promises.unlink(backupPath); (0, constants_1.logVerbose)(`${prefix} 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 prefix = (0, constants_1.actionPrefix)(dryRun); const actionText = action === 'remove-tree' ? 'directory tree' : 'directory'; if (dryRun) { (0, constants_1.logVerbose)(`${prefix} Would remove empty ${actionText}: ${dirPath}`, verbose); } else { await fs_1.promises.rm(dirPath, { recursive: true }); (0, constants_1.logVerbose)(`${prefix} 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', 'config.toml', ]; let filesRemoved = 0; const prefix = (0, constants_1.actionPrefix)(dryRun); 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)(`${prefix} Would remove additional file: ${fullPath}`, verbose); } else { await fs_1.promises.unlink(fullPath); (0, constants_1.logVerbose)(`${prefix} 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)(`${constants_1.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)(`${constants_1.actionPrefix} Would remove empty VSCode settings file`, verbose); } else { (0, constants_1.logVerbose)(`${constants_1.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)(`${constants_1.actionPrefix} Removed empty VSCode settings file`, verbose); } else { await (0, settings_1.writeVSCodeSettings)(settingsPath, settings); (0, constants_1.logVerbose)(`${constants_1.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; } /** * Reverts configuration for a single agent. * @param agent The agent to revert * @param projectRoot Root directory of the project * @param agentConfig Agent-specific configuration * @param keepBackups Whether to keep backup files * @param verbose Whether to enable verbose logging * @param dryRun Whether to perform a dry run * @returns Promise resolving to revert statistics */ async function revertAgentConfiguration(agent, projectRoot, agentConfig, keepBackups, verbose, dryRun) { const result = { restored: 0, removed: 0, backupsRemoved: 0, }; const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig); (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose); for (const outputPath of outputPaths) { const restored = await restoreFromBackup(outputPath, verbose, dryRun); if (restored) { result.restored++; if (!keepBackups) { const backupRemoved = await removeBackupFile(outputPath, verbose, dryRun); if (backupRemoved) { result.backupsRemoved++; } } } else { const removed = await removeGeneratedFile(outputPath, verbose, dryRun); if (removed) { result.removed++; } } } // Handle MCP files const mcpPath = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot); if (mcpPath && mcpPath.startsWith(projectRoot)) { 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) { result.restored++; if (!keepBackups) { const mcpBackupRemoved = await removeBackupFile(mcpPath, verbose, dryRun); if (mcpBackupRemoved) { result.backupsRemoved++; } } } else { const mcpRemoved = await removeGeneratedFile(mcpPath, verbose, dryRun); if (mcpRemoved) { result.removed++; } } } } return result; } /** * Cleans up auxiliary files and directories. * @param projectRoot Root directory of the project * @param verbose Whether to enable verbose logging * @param dryRun Whether to perform a dry run * @returns Promise resolving to cleanup statistics */ async function cleanUpAuxiliaryFiles(projectRoot, verbose, dryRun) { const additionalFilesRemoved = await removeAdditionalAgentFiles(projectRoot, verbose, dryRun); const directoriesRemoved = await removeEmptyDirectories(projectRoot, verbose, dryRun); return { additionalFilesRemoved, directoriesRemoved, }; }