UNPKG

agent-rules-kit

Version:

CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.

1,111 lines (970 loc) 40.8 kB
#!/usr/bin/env node /** * Main entry point for Agent Rules Kit * New services architecture v1.0.0 * Optimized for performance v1.x.x */ import chalk from 'chalk'; import fs from 'fs-extra'; import inquirer from 'inquirer'; import path from 'path'; import { fileURLToPath } from 'url'; // Only import essential services at startup import { BaseService } from './services/base-service.js'; import { CliService } from './services/cli-service.js'; import { ConfigService } from './services/config-service.js'; import { FileService } from './services/file-service.js'; import { McpService } from './services/mcp/mcp-service.js'; import { StackService } from './services/stack-service.js'; // Path configuration const __dirname = path.dirname(fileURLToPath(import.meta.url)); const templatesDir = path.join(__dirname, '../templates'); // Parse command line arguments const args = process.argv.slice(2); function parseCliArgs(argv) { const opts = { debug: argv.includes('--debug'), update: argv.includes('--update'), info: argv.includes('--info'), global: null, stack: null, architecture: null, version: null, projectPath: null, appDirectory: null, cursorPath: null, mcpTools: [], stateManagement: null, includeSignals: null, autoInstall: argv.includes('--auto-install') || argv.includes('--auto'), ide: null }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--no-global') { opts.global = false; continue; } if (arg === '--global') { opts.global = true; continue; } if (arg.startsWith('--stack=')) { opts.stack = arg.split('=')[1]; continue; } if (arg === '--stack' && i + 1 < argv.length) { opts.stack = argv[++i]; continue; } if (arg.startsWith('--architecture=')) { opts.architecture = arg.split('=')[1]; continue; } if (arg === '--architecture' && i + 1 < argv.length) { opts.architecture = argv[++i]; continue; } if (arg.startsWith('--version=')) { opts.version = arg.split('=')[1]; continue; } if (arg === '--version' && i + 1 < argv.length) { opts.version = argv[++i]; continue; } if (arg.startsWith('--project-path=')) { opts.projectPath = arg.split('=')[1]; continue; } if (arg === '--project-path' && i + 1 < argv.length) { opts.projectPath = argv[++i]; continue; } if (arg.startsWith('--cursor-path=')) { opts.cursorPath = arg.split('=')[1]; continue; } if (arg === '--cursor-path' && i + 1 < argv.length) { opts.cursorPath = argv[++i]; continue; } if (arg.startsWith('--app-directory=')) { opts.appDirectory = arg.split('=')[1]; continue; } if (arg === '--app-directory' && i + 1 < argv.length) { opts.appDirectory = argv[++i]; continue; } if (arg.startsWith('--mcp-tools=')) { const val = arg.split('=')[1]; opts.mcpTools = val.split(',').map(v => v.trim()).filter(Boolean); continue; } if (arg.startsWith('--state-management=')) { opts.stateManagement = arg.split('=')[1]; continue; } if (arg === '--state-management' && i + 1 < argv.length) { opts.stateManagement = argv[++i]; continue; } if (arg === '--include-signals') { opts.includeSignals = true; continue; } if (arg === '--no-signals') { opts.includeSignals = false; continue; } if (arg.startsWith('--ide=')) { opts.ide = arg.split('=')[1]; continue; } if (arg === '--ide' && i + 1 < argv.length) { opts.ide = argv[++i]; continue; } } return opts; } const cliOptions = parseCliArgs(args); const debugMode = cliOptions.debug; const updateFlag = cliOptions.update; const infoFlag = cliOptions.info; // Initialize essential services const baseService = new BaseService({ debug: debugMode }); const configService = new ConfigService({ debug: debugMode, templatesDir }); const fileService = new FileService({ debug: debugMode, templatesDir }); const cliService = new CliService({ debug: debugMode }); const stackService = new StackService({ debug: debugMode, configService, fileService, templatesDir }); const mcpService = new McpService({ debug: debugMode, configService, fileService, templatesDir }); // Map to store dynamically loaded stack services const stackServices = new Map(); /** * Dynamically load a service based on the selected stack * @param {string} stack - Selected stack name * @returns {Promise<object>} - Stack service instance */ async function loadStackService(stack) { // Return from cache if already loaded if (stackServices.has(stack)) { return stackServices.get(stack); } try { // Try to dynamically import the required service const servicePath = `./services/stack/${stack}-service.js`; try { const serviceModule = await import(servicePath); // Get the service class (follows naming convention like LaravelService) const ServiceClass = serviceModule[`${stack.charAt(0).toUpperCase() + stack.slice(1)}Service`]; if (!ServiceClass) { throw new Error(`Service class not found for stack: ${stack}`); } // Instantiate the service with stackService methods const serviceInstance = new ServiceClass({ debug: debugMode, fileService, configService, templatesDir, stackService // Pass stackService to inherit its methods }); // Cache the instance stackServices.set(stack, serviceInstance); baseService.debugLog(`Loaded specific service for ${stack}`); return serviceInstance; } catch (importError) { // If specific service doesn't exist, fall back to base StackService baseService.debugLog(`No specific service found for ${stack}, using base StackService`); // Cache the base stackService for this stack stackServices.set(stack, stackService); return stackService; } } catch (error) { baseService.debugLog(`Error loading service for ${stack}: ${error.message}`); throw error; } } // Get package version from package.json const packageJsonPath = path.join(__dirname, '../package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const version = packageJson.version; /** * IDE configurations for different targets */ const IDE_CONFIGS = { cursor: { name: 'Cursor', multiple: true, dir: '.cursor/rules/rules-kit', extension: '.mdc', keepFrontMatter: true }, vscode: { name: 'VS Code / GitHub Copilot', multiple: false, file: '.github/copilot-instructions.md', extension: '.md', keepFrontMatter: false }, windsurf: { name: 'Windsurf', multiple: true, dir: '.windsurf/rules', extension: '.md', keepFrontMatter: false }, continue: { name: 'Continue', multiple: true, dir: '.continue/rules', extension: '.md', keepFrontMatter: true }, zed: { name: 'Zed', multiple: false, file: '.rules', extension: '.rules', keepFrontMatter: false }, claude: { name: 'Claude Code', multiple: false, file: 'CLAUDE.md', extension: '.md', keepFrontMatter: false }, gemini: { name: 'Gemini Code', multiple: false, file: 'GEMINI.md', extension: '.md', keepFrontMatter: false }, codex: { name: 'OpenAI Codex', multiple: false, file: 'AGENTS.md', extension: '.md', keepFrontMatter: false }, cline: { name: 'Cline', multiple: false, file: '.clinerules', extension: '.md', keepFrontMatter: false } }; /** * Format rules directory path based on selected IDE * @param {string} ide - Selected IDE * @param {string} projectPath - Project path * @returns {string} - Rules directory path */ function formatRulesDirForIde(ide, projectPath) { const config = IDE_CONFIGS[ide]; if (!config) { throw new Error(`Unknown IDE: ${ide}`); } if (config.multiple) { return path.join(projectPath, config.dir); } else { return path.join(projectPath, path.dirname(config.file)); } } /** * Display comprehensive project information */ function showProjectInfo() { // Get available stacks dynamically from the service const availableStacks = stackService.getAvailableStacks(); const config = configService.loadKitConfig(templatesDir); // Get available MCP tools const mcpTools = mcpService.getAvailableMcpTools(); console.log(`\n${chalk.bold('Agent Rules Kit')} ${chalk.blue(`v${version}`)}`); console.log('='.repeat(50)); console.log(`\n${chalk.green('📝 Description:')}`); console.log(' CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks.'); console.log(' Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.'); console.log(' This tool helps you generate and maintain project-specific rules for multiple frameworks and architectures.'); // Show supported stacks with versions and architectures console.log(`\n${chalk.green('📚 Supported Stacks:')} (${availableStacks.length} total)`); availableStacks.forEach(stack => { const stackConfig = config[stack] || {}; const versions = stackConfig.version_ranges ? Object.keys(stackConfig.version_ranges) : []; const architectures = stackService.getAvailableArchitectures(stack); console.log(`\n ${chalk.bold(stack.toUpperCase())}`); if (versions.length > 0) { console.log(` 🏷️ Versions: ${versions.join(', ')}`); } if (architectures.length > 0) { const archNames = architectures.map(arch => arch.value).join(', '); console.log(` 🏗️ Architectures: ${archNames}`); if (stackConfig.default_architecture) { console.log(` ⭐ Default: ${stackConfig.default_architecture}`); } } }); // Show supported MCP tools console.log(`\n${chalk.green('🔧 MCP Tools:')} (${mcpTools.length} available)`); mcpTools.forEach(tool => { console.log(` • ${chalk.bold(tool.name)}`); console.log(` ${tool.description}`); }); // Show supported IDEs console.log(`\n${chalk.green('🎯 Supported IDEs:')} (${Object.keys(IDE_CONFIGS).length} targets)`); Object.entries(IDE_CONFIGS).forEach(([key, config]) => { const formatInfo = config.multiple ? `Multiple files in ${config.dir}` : `Single file: ${config.file}`; console.log(` • ${chalk.bold(config.name)} (${key}): ${formatInfo}`); }); console.log(`\n${chalk.green('💡 Usage Examples:')}`); console.log(' npx agent-rules-kit # Interactive mode'); console.log(' npx agent-rules-kit --info # Show this information'); console.log(' npx agent-rules-kit --update # Update existing rules'); console.log(' npx agent-rules-kit --stack=laravel # Direct stack installation'); console.log(' npx agent-rules-kit --ide=vscode # Target specific IDE'); console.log(' npx agent-rules-kit --global --auto # Global rules only'); console.log(`\n${chalk.green('🔗 Links:')}`); console.log(' Repository: https://github.com/tecnomanu/agent-rules-kit'); console.log(' Documentation: https://github.com/tecnomanu/agent-rules-kit/blob/main/README.md'); console.log(' Agent Guide: https://github.com/tecnomanu/agent-rules-kit/blob/main/AGENTS.md'); console.log(`\n${chalk.green('👨‍💻 Author:')}`); console.log(' Manuel Bruña'); console.log('\n' + '='.repeat(50)); } /** * Main function */ async function main() { cliService.showBanner(`🚀 Agent Rules Kit v${version}`, true); // Handle info flag if (infoFlag) { showProjectInfo(); return; } // Handle update flag if (updateFlag) { await handleUpdate(); return; } // Welcome and introduction cliService.showWelcome(); // Wait for user to press enter to continue unless auto-install if (!cliOptions.autoInstall) { await cliService.askContinue(); } // Ask for IDE selection FIRST (or use CLI option) let selectedIde = cliOptions.ide; if (!selectedIde) { if (cliOptions.autoInstall) { selectedIde = 'cursor'; // Default to Cursor in auto-install } else { const { ide } = await inquirer.prompt([ { type: 'list', name: 'ide', message: `${cliService.emoji.config} Select IDE/Agent for rules installation:`, choices: [ { name: 'Cursor', value: 'cursor' }, { name: 'VS Code / GitHub Copilot', value: 'vscode' }, { name: 'Windsurf', value: 'windsurf' }, { name: 'Continue', value: 'continue' }, { name: 'Zed', value: 'zed' }, { name: 'Claude Code', value: 'claude' }, { name: 'Gemini Code', value: 'gemini' }, { name: 'OpenAI Codex', value: 'codex' }, { name: 'Cline', value: 'cline' } ], default: 'cursor' } ]); selectedIde = ide; } } // Get IDE configuration to adapt path questions const selectedIdeConfig = IDE_CONFIGS[selectedIde]; // Ask for the relative path to the project (or use CLI option) // Adapt the question based on the selected IDE const projectPath = cliOptions.projectPath ? cliOptions.projectPath : cliOptions.autoInstall ? '.' : await cliService.askProjectPath(selectedIde, selectedIdeConfig); // Ask for the application directory within the project (or use CLI option) const appDirectory = cliOptions.appDirectory ? cliOptions.appDirectory : cliOptions.autoInstall ? '.' : await cliService.askAppDirectory(); // Format the rules directory path based on selected IDE const rulesDir = formatRulesDirForIde(selectedIde, projectPath); // Initialize variables for collection user choices let selectedStack = null; let additionalOptions = {}; let includeGlobalRules = false; // First question: Do you want rules for a specific stack? let wantStack = true; if (!cliOptions.stack) { if (cliOptions.autoInstall) { wantStack = false; } else { const resp = await inquirer.prompt([ { type: 'confirm', name: 'wantStack', message: `${cliService.emoji.stack} Do you want to install rules for a specific stack?`, default: true } ]); wantStack = resp.wantStack; } } if (wantStack) { // Get available stacks from the service const availableStacks = stackService.getAvailableStacks().map(stack => ({ name: stack.charAt(0).toUpperCase() + stack.slice(1), value: stack })); if (availableStacks.length === 0) { cliService.error('No stacks found in kit-config.json or stacks directory'); process.exit(1); } if (cliOptions.stack) { selectedStack = cliOptions.stack; } else { // Ask for the stack to use selectedStack = await cliService.askStack(availableStacks); } // If user chose to continue without stack, selectedStack will be null if (selectedStack === null) { cliService.info('📝 Continuing without specific stack selection...'); } else { // Try to detect the stack version let detectedVersion = cliOptions.version || stackService.detectStackVersion(selectedStack, appDirectory !== './' ? appDirectory : projectPath); if (!cliOptions.version && detectedVersion) { cliService.info(`Detected ${selectedStack} version: ${detectedVersion}`); } // Load the appropriate stack service dynamically const stackSpecificService = await loadStackService(selectedStack); // Stack-specific questions if (selectedStack === 'laravel') { // Get available architectures for Laravel const architectures = stackSpecificService.getAvailableArchitectures(selectedStack); const architecture = cliOptions.architecture || (cliOptions.autoInstall ? architectures[0].value : await cliService.askArchitecture(architectures, selectedStack)); // Get available versions for Laravel const versions = stackSpecificService.getAvailableVersions(selectedStack); let version; if (cliOptions.version) { version = cliOptions.version; } else if (cliOptions.autoInstall) { version = detectedVersion || versions[0].value; } else { version = await cliService.askVersion(versions, detectedVersion); } // Map specific version to version range if needed const versionRange = stackSpecificService.mapVersionToRange(selectedStack, version); // Get formatted version name for display const formattedVersionName = stackSpecificService.getFormattedVersionName(selectedStack, version); additionalOptions = { architecture, detectedVersion: version, versionRange, formattedVersionName }; } else if (selectedStack === 'nextjs') { // Get available architectures for Next.js const architectures = stackSpecificService.getAvailableArchitectures(selectedStack); const architecture = cliOptions.architecture || (cliOptions.autoInstall ? architectures[0].value : await cliService.askArchitecture(architectures, selectedStack)); // Get available versions for Next.js const versions = stackSpecificService.getAvailableVersions(selectedStack); let version; if (cliOptions.version) { version = cliOptions.version; } else if (cliOptions.autoInstall) { version = detectedVersion || versions[0].value; } else { version = await cliService.askVersion(versions, detectedVersion); } // Map specific version to version range if needed const versionRange = stackSpecificService.mapVersionToRange(selectedStack, version); // Get formatted version name for display const formattedVersionName = stackSpecificService.getFormattedVersionName(selectedStack, version); additionalOptions = { architecture, detectedVersion: version, versionRange, formattedVersionName }; } else if (selectedStack === 'react') { // Get available architectures for React const architectures = stackSpecificService.getAvailableArchitectures(selectedStack); const architecture = cliOptions.architecture || (cliOptions.autoInstall ? architectures[0].value : await cliService.askArchitecture(architectures, selectedStack)); // Get available state management options const stateManagementOptions = [ 'redux', 'redux-toolkit', 'context', 'react-query', 'zustand' ]; const stateManagement = cliOptions.stateManagement || (cliOptions.autoInstall ? stateManagementOptions[0] : await cliService.askStateManagement(stateManagementOptions)); // Get available versions for React const versions = stackSpecificService.getAvailableVersions(selectedStack); let version; if (cliOptions.version) { version = cliOptions.version; } else if (cliOptions.autoInstall) { version = detectedVersion || versions[0].value; } else { version = await cliService.askVersion(versions, detectedVersion); } // Map specific version to version range if needed const versionRange = stackSpecificService.mapVersionToRange(selectedStack, version); // Get formatted version name for display const formattedVersionName = stackSpecificService.getFormattedVersionName(selectedStack, version); additionalOptions = { architecture, stateManagement, detectedVersion: version, versionRange, formattedVersionName }; } else if (selectedStack === 'angular') { // Get available architectures for Angular const architectures = stackSpecificService.getAvailableArchitectures(selectedStack); const architecture = cliOptions.architecture || (cliOptions.autoInstall ? architectures[0].value : await cliService.askArchitecture(architectures, selectedStack)); // Ask if Angular signals should be included let includeSignals; if (cliOptions.includeSignals !== null) { includeSignals = cliOptions.includeSignals; } else if (cliOptions.autoInstall) { includeSignals = true; } else { const resp = await inquirer.prompt([ { type: 'confirm', name: 'includeSignals', message: `${cliService.emoji.config} Include Angular Signals rules?`, default: true } ]); includeSignals = resp.includeSignals; } // Get available versions for Angular const versions = stackSpecificService.getAvailableVersions(selectedStack); let version; if (cliOptions.version) { version = cliOptions.version; } else if (cliOptions.autoInstall) { version = detectedVersion || versions[0].value; } else { version = await cliService.askVersion(versions, detectedVersion); } // Map specific version to version range if needed const versionRange = stackSpecificService.mapVersionToRange(selectedStack, version); // Get formatted version name for display const formattedVersionName = stackSpecificService.getFormattedVersionName(selectedStack, version); additionalOptions = { architecture, includeSignals, detectedVersion: version, versionRange, formattedVersionName }; } else { // For other stacks, fetch available versions and architectures when available const architectures = stackSpecificService.getAvailableArchitectures(selectedStack); if (architectures.length > 0) { additionalOptions.architecture = cliOptions.architecture || (cliOptions.autoInstall ? architectures[0].value : await cliService.askArchitecture(architectures, selectedStack)); } const versions = stackSpecificService.getAvailableVersions(selectedStack); if (versions.length > 0) { let version; if (cliOptions.version) { version = cliOptions.version; } else if (cliOptions.autoInstall) { version = detectedVersion || versions[0].value; } else { version = await cliService.askVersion(versions, detectedVersion); } const versionRange = stackSpecificService.mapVersionToRange(selectedStack, version); const formattedVersionName = stackSpecificService.getFormattedVersionName(selectedStack, version); additionalOptions = { ...additionalOptions, detectedVersion: version, versionRange, formattedVersionName }; } } } } // Second question: Do you want global rules? if (cliOptions.global !== null) { includeGlobalRules = cliOptions.global; } else if (cliOptions.autoInstall) { includeGlobalRules = true; } else { const { wantGlobalRules } = await inquirer.prompt([ { type: 'confirm', name: 'wantGlobalRules', message: `${cliService.emoji.global} Do you want to include global best practice rules?`, default: true } ]); includeGlobalRules = wantGlobalRules; } // Third question: Do you want MCP tools rules? let selectedMcpTools = []; let wantMcpTools = cliOptions.mcpTools.length > 0 ? true : null; if (wantMcpTools === null) { if (cliOptions.autoInstall) { wantMcpTools = true; } else { const resp = await inquirer.prompt([ { type: 'confirm', name: 'wantMcpTools', message: `${cliService.emoji.tools} Do you want to install MCP (Model Context Protocol) tools rules?`, default: false } ]); wantMcpTools = resp.wantMcpTools; } } if (wantMcpTools) { const availableMcpTools = mcpService.getAvailableMcpTools(); if (availableMcpTools.length > 0) { if (cliOptions.mcpTools.length > 0) { selectedMcpTools = cliOptions.mcpTools; } else if (cliOptions.autoInstall) { selectedMcpTools = ['pampa']; } else { const { mcpTools } = await inquirer.prompt([ { type: 'checkbox', name: 'mcpTools', message: 'Select MCP tools to install rules for:', choices: availableMcpTools.map(tool => ({ name: `${tool.name} - ${tool.description}`, value: tool.key, checked: tool.key === 'pampa' })) } ]); selectedMcpTools = mcpTools; } } else { cliService.info('No MCP tools available in configuration'); } } // If no stack, no global rules, and no MCP tools, exit if (!selectedStack && !includeGlobalRules && selectedMcpTools.length === 0) { cliService.info('💭 No rules selected for installation. See you later!'); process.exit(0); } // Check if target already exists and create backup if needed const ideConfig = IDE_CONFIGS[selectedIde]; let needsBackup = false; let targetPath = ''; if (ideConfig.multiple) { // Multi-file IDE: check if rules directory exists targetPath = rulesDir; needsBackup = await baseService.directoryExistsAsync(rulesDir); } else { // Single-file IDE: check if specific file exists targetPath = path.join(projectPath, ideConfig.file); needsBackup = await fs.pathExists(targetPath); } if (needsBackup) { let action; if (cliOptions.autoInstall) { action = 'backup'; } else { const targetName = ideConfig.multiple ? path.basename(targetPath) : ideConfig.file; action = await cliService.askFileAction(targetName, ideConfig.multiple); } if (action === 'cancel') { cliService.info('❌ Operation canceled by user'); process.exit(0); } if (action === 'backup') { let backupPath; if (ideConfig.multiple) { backupPath = await stackService.createBackupAsync(targetPath); } else { backupPath = await createFileBackup(targetPath); } if (backupPath) { cliService.backupCreated(targetPath, backupPath); } } } // Ensure the rules directory exists await baseService.ensureDirectoryExistsAsync(rulesDir); // Show installation summary cliService.showInstallationSummary(selectedStack, includeGlobalRules, additionalOptions, selectedMcpTools, selectedIde); // Get the start time for measuring performance const startGeneration = Date.now(); // Generate rules with progress tracking cliService.startProgress('Generating rules'); // Prepare metadata for rule generation const meta = { projectPath: appDirectory, cursorPath: cliOptions.cursorPath || projectPath, stack: selectedStack, debug: debugMode, ...additionalOptions }; // Load config only once to improve performance const config = configService.getConfig(); try { let totalFiles = 0; const ideConfig = IDE_CONFIGS[selectedIde]; if (ideConfig.multiple) { // Multi-file IDE (Cursor, Windsurf, Continue) - generate individual files // Use existing logic but adjust file extension and front matter // If global rules are requested, copy them if (includeGlobalRules) { const globalMeta = { projectPath: appDirectory, cursorPath: cliOptions.cursorPath || projectPath, debug: debugMode }; const globalCount = await stackService.copyGlobalRules(rulesDir, globalMeta, config); totalFiles += globalCount; // Convert files to appropriate format if not Cursor if (selectedIde !== 'cursor') { await convertRulesForIde(path.join(rulesDir, 'global'), ideConfig); } } // If MCP tools are selected, copy them if (selectedMcpTools.length > 0) { const mcpCount = await mcpService.copyMcpToolsRules(rulesDir, selectedMcpTools, meta, config); totalFiles += mcpCount; // Convert files to appropriate format if not Cursor if (selectedIde !== 'cursor') { await convertRulesForIde(path.join(rulesDir, 'mcp-tools'), ideConfig); } } // If stack is selected, generate stack-specific rules if (selectedStack) { const stackCount = await stackService.countStackRules(meta); totalFiles += stackCount; // Generate stack-specific rules only (global rules already copied) await stackService.generateRulesAsync(rulesDir, meta, config, () => { }, false); // Convert files to appropriate format if not Cursor if (selectedIde !== 'cursor') { await convertRulesForIde(path.join(rulesDir, selectedStack), ideConfig); } } } else { // Single-file IDE (VS Code, Zed, Claude, etc.) - consolidate all rules // First generate in temp directory as .mdc files const tempDir = path.join(projectPath, '.temp-rules'); await fs.ensureDir(tempDir); try { // Generate all rules in temp directory if (includeGlobalRules) { const globalMeta = { projectPath: appDirectory, cursorPath: cliOptions.cursorPath || projectPath, debug: debugMode }; await stackService.copyGlobalRules(tempDir, globalMeta, config); } if (selectedMcpTools.length > 0) { await mcpService.copyMcpToolsRules(tempDir, selectedMcpTools, meta, config); } if (selectedStack) { await stackService.generateRulesAsync(tempDir, meta, config, () => { }, false); } // Collect all generated files and consolidate const allRules = await collectGeneratedRules(tempDir); const consolidatedContent = consolidateRulesForSingleFile(allRules, ideConfig); const outputPath = path.join(projectPath, ideConfig.file); await fs.outputFile(outputPath, consolidatedContent, 'utf8'); totalFiles = 1; } finally { // Clean up temp directory await fs.remove(tempDir); } } // End progress tracking cliService.completeProgress(); // Get the end time and calculate duration const endGeneration = Date.now(); const durationMs = endGeneration - startGeneration; const durationFormatted = (durationMs / 1000).toFixed(2); // Show success message cliService.showSuccess(totalFiles, rulesDir, durationFormatted, selectedStack, additionalOptions, selectedIde); // Clean up cached templates to free memory fileService.clearCache(); } catch (error) { cliService.completeProgress(); cliService.error(`Error generating rules: ${error.message}`); if (debugMode) { console.error(error); } process.exit(1); } } /** * Convert rules in a directory to appropriate format for IDE * @param {string} rulesDir - Directory containing .mdc files * @param {Object} ideConfig - IDE configuration */ async function convertRulesForIde(rulesDir, ideConfig) { if (!(await fs.pathExists(rulesDir))) { return; } const files = await fs.readdir(rulesDir); const mdcFiles = files.filter(f => f.endsWith('.mdc')); for (const file of mdcFiles) { const filePath = path.join(rulesDir, file); let content = await fs.readFile(filePath, 'utf8'); // Remove front matter if IDE doesn't support it if (!ideConfig.keepFrontMatter && content.startsWith('---')) { const endIndex = content.indexOf('\n---', 3); if (endIndex !== -1) { content = content.slice(endIndex + 4).replace(/^\n+/, ''); } } // Change extension if needed if (ideConfig.extension !== '.mdc') { const newFileName = file.replace('.mdc', ideConfig.extension); const newFilePath = path.join(rulesDir, newFileName); await fs.writeFile(newFilePath, content, 'utf8'); await fs.remove(filePath); // Remove old .mdc file } } } /** * Collect all generated rules from temp directory * @param {string} tempDir - Temporary directory with generated rules * @returns {Promise<Array>} - Array of rule objects */ async function collectGeneratedRules(tempDir) { const rules = []; // Recursively find all .mdc files const findMdcFiles = async (dir) => { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await findMdcFiles(fullPath); } else if (entry.name.endsWith('.mdc')) { const content = await fs.readFile(fullPath, 'utf8'); const relativePath = path.relative(tempDir, fullPath); rules.push({ filename: entry.name, path: relativePath, content: content, title: entry.name.replace('.mdc', '') }); } } }; if (await fs.pathExists(tempDir)) { await findMdcFiles(tempDir); } return rules; } /** * Consolidate rules into a single file content * @param {Array} rules - Array of rule objects * @param {Object} ideConfig - IDE configuration * @returns {string} - Consolidated content */ function consolidateRulesForSingleFile(rules, ideConfig) { const processedRules = rules.map(rule => { let content = rule.content; // Remove front matter if IDE doesn't support it if (!ideConfig.keepFrontMatter && content.startsWith('---')) { const endIndex = content.indexOf('\n---', 3); if (endIndex !== -1) { content = content.slice(endIndex + 4).replace(/^\n+/, ''); } } return { title: rule.title || rule.filename.replace(/\.(md|mdc)$/, ''), body: content }; }); // Sort rules by category (global, stack, mcp-tools) processedRules.sort((a, b) => { const getCategory = (rule) => { if (rule.path && rule.path.includes('global')) return 0; if (rule.path && rule.path.includes('mcp-tools')) return 2; return 1; // stack rules }; return getCategory(a) - getCategory(b); }); // Create index and sections const index = processedRules.map((r, i) => `${i + 1}. ${r.title}`).join('\n'); const sections = processedRules.map(r => `## ${r.title}\n\n${r.body}`).join('\n\n'); return `# Agent Rules\n\n${index}\n\n${sections}\n`; } /** * Create backup of a single file * @param {string} filePath - Path to file to backup * @returns {Promise<string|null>} - Path to backup file or null if failed */ async function createFileBackup(filePath) { try { if (!(await fs.pathExists(filePath))) { return null; } const date = new Date(); const timestamp = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}`; const backupPath = `${filePath}.backup-${timestamp}`; await fs.copy(filePath, backupPath); return backupPath; } catch (error) { console.error(`Failed to create file backup: ${error.message}`); return null; } } /** * Handle the --update flag for updating rules */ async function handleUpdate() { cliService.info('Updating rules...'); // Implementation for update mode // TODO: Add implementation for update mode cliService.success('Rules updated successfully!'); } // Run the main function main().catch(error => { cliService.error(`Error: ${error.message}`); if (debugMode) { console.error(error); } process.exit(1); });