UNPKG

@intellectronica/ruler

Version:

Ruler — apply the same rules to all coding agents

351 lines (350 loc) 17.5 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.applyAllAgentConfigs = applyAllAgentConfigs; const path = __importStar(require("path")); const fs_1 = require("fs"); const FileSystemUtils = __importStar(require("./core/FileSystemUtils")); const RuleProcessor_1 = require("./core/RuleProcessor"); const ConfigLoader_1 = require("./core/ConfigLoader"); const GitignoreUtils_1 = require("./core/GitignoreUtils"); 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_1 = 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 CrushAgent_1 = require("./agents/CrushAgent"); const GooseAgent_1 = require("./agents/GooseAgent"); const AmpAgent_1 = require("./agents/AmpAgent"); const merge_1 = require("./mcp/merge"); const validate_1 = require("./mcp/validate"); const mcp_1 = require("./paths/mcp"); const propagateOpenHandsMcp_1 = require("./mcp/propagateOpenHandsMcp"); const propagateOpenCodeMcp_1 = require("./mcp/propagateOpenCodeMcp"); const constants_1 = require("./constants"); /** * Gets all output paths for an agent, taking into account any config overrides. */ function getAgentOutputPaths(agent, projectRoot, agentConfig) { const paths = []; const defaults = agent.getDefaultOutputPath(projectRoot); if (typeof defaults === 'string') { // Single output path (most agents) const actualPath = agentConfig?.outputPath ?? defaults; paths.push(actualPath); } else { // Multiple output paths (e.g., AiderAgent) const defaultPaths = defaults; // Handle instructions path if ('instructions' in defaultPaths) { const instructionsPath = agentConfig?.outputPathInstructions ?? defaultPaths.instructions; paths.push(instructionsPath); } // Handle config path if ('config' in defaultPaths) { const configPath = agentConfig?.outputPathConfig ?? defaultPaths.config; paths.push(configPath); } // Handle any other paths in the default paths record for (const [key, defaultPath] of Object.entries(defaultPaths)) { if (key !== 'instructions' && key !== 'config') { // For unknown path types, use the default since we don't have specific config overrides paths.push(defaultPath); } } } return paths; } const agents = [ new CopilotAgent_1.CopilotAgent(), new ClaudeAgent_1.ClaudeAgent(), new CodexCliAgent_1.CodexCliAgent(), new CursorAgent_1.CursorAgent(), new WindsurfAgent_1.WindsurfAgent(), new ClineAgent_1.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 CrushAgent_1.CrushAgent(), new AmpAgent_1.AmpAgent(), ]; /** * Applies ruler configurations for all supported AI agents. * @param projectRoot Root directory of the project */ /** * Applies ruler configurations for selected AI agents. * @param projectRoot Root directory of the project * @param includedAgents Optional list of agent name filters (case-insensitive substrings) */ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false) { // Load configuration (default_agents, per-agent overrides, CLI filters) (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose); if (configPath) { (0, constants_1.logVerbose)(`Using custom config path: ${configPath}`, verbose); } const config = await (0, ConfigLoader_1.loadConfig)({ projectRoot, cliAgents: includedAgents, configPath, }); (0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(config.agentConfigs).length} agent configs`, verbose); // Normalize per-agent config keys to agent identifiers (exact match or substring match) 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(); // Exact match with identifier or substring match with display name for backwards compatibility if (identifier === lowerKey || agent.getName().toLowerCase().includes(lowerKey)) { mappedConfigs[identifier] = cfg; } } } config.agentConfigs = mappedConfigs; 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 files = await FileSystemUtils.readMarkdownFiles(rulerDir); (0, constants_1.logVerbose)(`Found ${files.length} markdown files in ruler configuration directory`, verbose); const concatenated = (0, RuleProcessor_1.concatenateRules)(files); (0, constants_1.logVerbose)(`Concatenated rules length: ${concatenated.length} characters`, verbose); const mcpFile = path.join(rulerDir, 'mcp.json'); let rulerMcpJson = null; try { const raw = await fs_1.promises.readFile(mcpFile, 'utf8'); rulerMcpJson = JSON.parse(raw); (0, validate_1.validateMcp)(rulerMcpJson); (0, constants_1.logVerbose)(`Loaded MCP configuration from: ${mcpFile}`, verbose); } catch (err) { if (err.code !== 'ENOENT') { throw (0, constants_1.createRulerError)(`Failed to load MCP configuration`, `File: ${mcpFile}, Error: ${err.message}`); } (0, constants_1.logVerbose)(`No MCP configuration found at: ${mcpFile}`, verbose); } // Determine which agents to run: // CLI --agents > config.default_agents > per-agent.enabled flags > default all let selected = agents; if (config.cliAgents && config.cliAgents.length > 0) { const filters = config.cliAgents.map((n) => n.toLowerCase()); // Check if any of the specified agents don't exist const validAgentIdentifiers = new Set(agents.map((agent) => agent.getIdentifier())); const validAgentNames = new Set(agents.map((agent) => agent.getName().toLowerCase())); const invalidAgents = filters.filter((filter) => !validAgentIdentifiers.has(filter) && ![...validAgentNames].some((name) => name.includes(filter))); if (invalidAgents.length > 0) { throw (0, constants_1.createRulerError)(`Invalid agent specified: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`); } 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()); // Check if any of the default agents don't exist const validAgentIdentifiers = new Set(agents.map((agent) => agent.getIdentifier())); const validAgentNames = new Set(agents.map((agent) => agent.getName().toLowerCase())); const invalidAgents = defaults.filter((filter) => !validAgentIdentifiers.has(filter) && ![...validAgentNames].some((name) => name.includes(filter))); if (invalidAgents.length > 0) { throw (0, constants_1.createRulerError)(`Invalid agent specified in default_agents: ${invalidAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`); } 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); } // Collect all generated file paths for .gitignore const generatedPaths = []; let agentsMdWritten = false; for (const agent of selected) { const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]'; console.log(`${actionPrefix} Applying rules for ${agent.getName()}...`); (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose); const agentConfig = config.agentConfigs[agent.getIdentifier()]; // Collect output paths for .gitignore const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig); (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose); generatedPaths.push(...outputPaths); // Also add the backup file paths to the gitignore list const backupPaths = outputPaths.map((p) => `${p}.bak`); generatedPaths.push(...backupPaths); if (dryRun) { (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose); } else { if (agent.getIdentifier() === 'jules' || agent.getIdentifier() === 'codex') { if (agentsMdWritten) { continue; } agentsMdWritten = true; } let finalAgentConfig = agentConfig; if (agent.getIdentifier() === 'augmentcode' && rulerMcpJson) { const resolvedStrategy = cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy ?? 'merge'; finalAgentConfig = { ...agentConfig, mcp: { ...agentConfig?.mcp, strategy: resolvedStrategy, }, }; } await agent.applyRulerConfig(concatenated, projectRoot, rulerMcpJson, finalAgentConfig); } const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot); const mcpEnabledForAgent = cliMcpEnabled && (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true); const rulerMcpFile = path.join(rulerDir, 'mcp.json'); if (dest && mcpEnabledForAgent) { // Include MCP config file in .gitignore only if it's within the project directory if (dest.startsWith(projectRoot)) { const relativeDest = path.relative(projectRoot, dest); generatedPaths.push(relativeDest); // Also add the backup for the MCP file generatedPaths.push(`${relativeDest}.bak`); } if (agent.getIdentifier() === 'openhands') { // *** Special handling for Open Hands *** if (dryRun) { (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose); } else { await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(rulerMcpFile, dest); } // Open Hands config file is already included above } else if (agent.getIdentifier() === 'augmentcode') { // *** Special handling for AugmentCode *** // AugmentCode handles MCP configuration internally in applyRulerConfig // by updating VSCode settings.json with augment.advanced.mcpServers format if (dryRun) { (0, constants_1.logVerbose)(`DRY RUN: AugmentCode MCP config handled internally via VSCode settings`, verbose); } } else if (agent.getIdentifier() === 'opencode') { // *** Special handling for OpenCode *** if (dryRun) { (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose); } else { await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(rulerMcpFile, dest); } } else { if (rulerMcpJson) { const strategy = cliMcpStrategy ?? agentConfig?.mcp?.strategy ?? config.mcp?.strategy ?? 'merge'; // Determine the correct server key for the agent const serverKey = agent.getMcpServerKey?.() ?? 'mcpServers'; (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy} and key: ${serverKey}`, verbose); if (dryRun) { (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, true); } else { const existing = await (0, mcp_1.readNativeMcp)(dest); const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy, serverKey); await (0, mcp_1.writeNativeMcp)(dest, merged); } } } } } // Handle .gitignore updates // Configuration precedence: CLI > TOML > Default (enabled) let gitignoreEnabled; if (cliGitignoreEnabled !== undefined) { gitignoreEnabled = cliGitignoreEnabled; } else if (config.gitignore?.enabled !== undefined) { gitignoreEnabled = config.gitignore.enabled; } else { gitignoreEnabled = true; // Default enabled } if (gitignoreEnabled && generatedPaths.length > 0) { const uniquePaths = [...new Set(generatedPaths)]; // Add wildcard pattern for backup files uniquePaths.push('*.bak'); if (uniquePaths.length > 0) { const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]'; if (dryRun) { console.log(`${actionPrefix} Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`); } else { await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths); console.log(`${actionPrefix} Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`); } } } }