UNPKG

@intellectronica/ruler

Version:

Ruler — apply the same rules to all coding agents

572 lines (571 loc) 23.3 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.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`); } }