@intellectronica/ruler
Version:
Ruler — apply the same rules to all coding agents
414 lines (413 loc) • 15.6 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.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,
};
}