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