From 73db5538bf50cf08402444f24b9a82e91c3ac5df Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 3 Dec 2025 19:41:12 -0600 Subject: [PATCH] roo installer improovement --- .gitignore | 3 +- tools/cli/installers/lib/ide/roo.js | 304 +++++++----------- .../lib/ide/shared/bmad-artifacts.js | 10 + 3 files changed, 132 insertions(+), 185 deletions(-) diff --git a/.gitignore b/.gitignore index 01026ba6..47a82e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,5 @@ z*/ .github/chatmodes .agent .agentvibes/ -.kiro/ \ No newline at end of file +.kiro/ +.roo diff --git a/tools/cli/installers/lib/ide/roo.js b/tools/cli/installers/lib/ide/roo.js index 22f333f6..1352b311 100644 --- a/tools/cli/installers/lib/ide/roo.js +++ b/tools/cli/installers/lib/ide/roo.js @@ -5,34 +5,13 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator'); /** * Roo IDE setup handler - * Creates custom modes in .roomodes file + * Creates custom commands in .roo/commands directory */ class RooSetup extends BaseIdeSetup { constructor() { super('roo', 'Roo Code'); - this.configFile = '.roomodes'; - this.defaultPermissions = { - dev: { - description: 'Development files', - fileRegex: String.raw`.*\.(js|jsx|ts|tsx|py|java|cpp|c|h|cs|go|rs|php|rb|swift)$`, - }, - config: { - description: 'Configuration files', - fileRegex: String.raw`.*\.(json|yaml|yml|toml|xml|ini|env|config)$`, - }, - docs: { - description: 'Documentation files', - fileRegex: String.raw`.*\.(md|mdx|rst|txt|doc|docx)$`, - }, - styles: { - description: 'Style and design files', - fileRegex: String.raw`.*\.(css|scss|sass|less|stylus)$`, - }, - all: { - description: 'All files', - fileRegex: '.*', - }, - }; + this.configDir = '.roo'; + this.commandsDir = 'commands'; } /** @@ -44,94 +23,96 @@ class RooSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Check for existing .roomodes file - const roomodesPath = path.join(projectDir, this.configFile); - let existingModes = []; - let existingContent = ''; + // Create .roo/commands directory + const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); + await this.ensureDir(rooCommandsDir); - if (await this.pathExists(roomodesPath)) { - existingContent = await this.readFile(roomodesPath); - // Parse existing modes to avoid duplicates - const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); - for (const match of modeMatches) { - existingModes.push(match[1]); - } - console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`)); - } - - // Generate agent launchers (though Roo will reference the actual .bmad agents) + // Generate agent launchers const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Always use 'all' permissions - users can customize in .roomodes file - const permissionChoice = 'all'; - - // Create modes content - let newModesContent = ''; let addedCount = 0; let skippedCount = 0; for (const artifact of agentArtifacts) { - const slug = `bmad-${artifact.module}-${artifact.name}`; + const commandName = `bmad-${artifact.module}-agent-${artifact.name}`; + const commandPath = path.join(rooCommandsDir, `${commandName}.md`); // Skip if already exists - if (existingModes.includes(slug)) { - console.log(chalk.dim(` Skipping ${slug} - already exists`)); + if (await this.pathExists(commandPath)) { + console.log(chalk.dim(` Skipping ${commandName} - already exists`)); skippedCount++; continue; } - // Read the actual agent file from .bmad for metadata extraction + // Read the actual agent file from .bmad for metadata extraction (installed agents are .md files) const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`); const content = await this.readFile(agentPath); - // Create mode entry that references the actual .bmad agent - const modeEntry = await this.createModeEntry( - { module: artifact.module, name: artifact.name, path: agentPath }, - content, - permissionChoice, - projectDir, - ); + // Create command file that references the actual .bmad agent + await this.createCommandFile({ module: artifact.module, name: artifact.name, path: agentPath }, content, commandPath, projectDir); - newModesContent += modeEntry; addedCount++; - console.log(chalk.green(` ✓ Added mode: ${slug}`)); + console.log(chalk.green(` ✓ Added command: ${commandName}`)); } - // Build final content - let finalContent = ''; - if (existingContent) { - // Append to existing content - finalContent = existingContent.trim() + '\n' + newModesContent; - } else { - // Create new .roomodes file - finalContent = 'customModes:\n' + newModesContent; - } - - // Write .roomodes file - await this.writeFile(roomodesPath, finalContent); - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${addedCount} modes added`)); + console.log(chalk.dim(` - ${addedCount} commands added`)); if (skippedCount > 0) { - console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`)); + console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`)); } - console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); - console.log(chalk.dim(` - Permission level: all (unrestricted)`)); - console.log(chalk.yellow(`\n 💡 Tip: Edit ${this.configFile} to customize file permissions per agent`)); - console.log(chalk.dim(` Modes will be available when you open this project in Roo Code`)); + console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/bmad/`)); + console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`)); return { success: true, - modes: addedCount, + commands: addedCount, skipped: skippedCount, }; } /** - * Create a mode entry for an agent + * Create a unified command file for agents + * @param {string} commandPath - Path where to write the command file + * @param {Object} options - Command options + * @param {string} options.name - Display name for the command + * @param {string} options.description - Description for the command + * @param {string} options.agentPath - Path to the agent file (relative to project root) + * @param {string} [options.icon] - Icon emoji (defaults to 🤖) + * @param {string} [options.extraContent] - Additional content to include before activation */ - async createModeEntry(agent, content, permissionChoice, projectDir) { + async createAgentCommandFile(commandPath, options) { + const { name, description, agentPath, icon = '🤖', extraContent = '' } = options; + + // Build command content with YAML frontmatter + let commandContent = `---\n`; + commandContent += `name: '${icon} ${name}'\n`; + commandContent += `description: '${description}'\n`; + commandContent += `---\n\n`; + + commandContent += `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n\n`; + + // Add any extra content (e.g., warnings for custom agents) + if (extraContent) { + commandContent += `${extraContent}\n\n`; + } + + commandContent += `\n`; + commandContent += `1. LOAD the FULL agent file from @${agentPath}\n`; + commandContent += `2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n`; + commandContent += `3. Execute ALL activation steps exactly as written in the agent file\n`; + commandContent += `4. Follow the agent's persona and menu system precisely\n`; + commandContent += `5. Stay in character throughout the session\n`; + commandContent += `\n`; + + // Write command file + await this.writeFile(commandPath, commandContent); + } + + /** + * Create a command file for an agent + */ + async createCommandFile(agent, content, commandPath, projectDir) { // Extract metadata from agent content const titleMatch = content.match(/title="([^"]+)"/); const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); @@ -142,66 +123,16 @@ class RooSetup extends BaseIdeSetup { const whenToUseMatch = content.match(/whenToUse="([^"]+)"/); const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - // Get the activation header from central template - const activationHeader = await this.getAgentCommandHeader(); - - const roleDefinitionMatch = content.match(/roleDefinition="([^"]+)"/); - const roleDefinition = roleDefinitionMatch - ? roleDefinitionMatch[1] - : `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`; - // Get relative path const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/'); - // Determine permissions - const permissions = this.getPermissionsForAgent(agent, permissionChoice); - - // Build mode entry - const slug = `bmad-${agent.module}-${agent.name}`; - let modeEntry = ` - slug: ${slug}\n`; - modeEntry += ` name: '${icon} ${title}'\n`; - - if (permissions && permissions.description) { - modeEntry += ` description: '${permissions.description}'\n`; - } - - modeEntry += ` roleDefinition: ${roleDefinition}\n`; - modeEntry += ` whenToUse: ${whenToUse}\n`; - modeEntry += ` customInstructions: ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; - modeEntry += ` groups:\n`; - modeEntry += ` - read\n`; - - if (permissions && permissions.fileRegex) { - modeEntry += ` - - edit\n`; - modeEntry += ` - fileRegex: ${permissions.fileRegex}\n`; - modeEntry += ` description: ${permissions.description}\n`; - } else { - modeEntry += ` - edit\n`; - } - - return modeEntry; - } - - /** - * Get permissions configuration for an agent - */ - getPermissionsForAgent(agent, permissionChoice) { - if (permissionChoice === 'custom') { - // Custom logic based on agent name/module - if (agent.name.includes('dev') || agent.name.includes('code')) { - return this.defaultPermissions.dev; - } else if (agent.name.includes('doc') || agent.name.includes('write')) { - return this.defaultPermissions.docs; - } else if (agent.name.includes('config') || agent.name.includes('setup')) { - return this.defaultPermissions.config; - } else if (agent.name.includes('style') || agent.name.includes('css')) { - return this.defaultPermissions.styles; - } - // Default to all for custom agents - return this.defaultPermissions.all; - } - - return this.defaultPermissions[permissionChoice] || null; + // Use unified method + await this.createAgentCommandFile(commandPath, { + name: title, + description: whenToUse, + agentPath: relativePath, + icon: icon, + }); } /** @@ -219,8 +150,26 @@ class RooSetup extends BaseIdeSetup { */ async cleanup(projectDir) { const fs = require('fs-extra'); - const roomodesPath = path.join(projectDir, this.configFile); + const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); + if (await fs.pathExists(rooCommandsDir)) { + const files = await fs.readdir(rooCommandsDir); + let removedCount = 0; + + for (const file of files) { + if (file.startsWith('bmad-') && file.endsWith('.md')) { + await fs.remove(path.join(rooCommandsDir, file)); + removedCount++; + } + } + + if (removedCount > 0) { + console.log(chalk.dim(`Removed ${removedCount} BMAD commands from .roo/commands/`)); + } + } + + // Also clean up old .roomodes file if it exists + const roomodesPath = path.join(projectDir, '.roomodes'); if (await fs.pathExists(roomodesPath)) { const content = await fs.readFile(roomodesPath, 'utf8'); @@ -245,7 +194,9 @@ class RooSetup extends BaseIdeSetup { // Write back filtered content await fs.writeFile(roomodesPath, filteredLines.join('\n')); - console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .roomodes`)); + if (removedCount > 0) { + console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`)); + } } } @@ -254,68 +205,53 @@ class RooSetup extends BaseIdeSetup { * @param {string} projectDir - Project directory * @param {string} agentName - Agent name (e.g., "fred-commit-poet") * @param {string} agentPath - Path to compiled agent (relative to project root) - * @param {Object} metadata - Agent metadata + * @param {Object} metadata - Agent metadata (unused, kept for compatibility) * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const roomodesPath = path.join(projectDir, this.configFile); - let existingContent = ''; + const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); + await this.ensureDir(rooCommandsDir); - // Read existing .roomodes file - if (await this.pathExists(roomodesPath)) { - existingContent = await this.readFile(roomodesPath); - } + const commandName = `bmad-custom-agent-${agentName.toLowerCase()}`; + const commandPath = path.join(rooCommandsDir, `${commandName}.md`); - // Create custom agent mode entry - const slug = `bmad-custom-${agentName.toLowerCase()}`; - const modeEntry = ` - slug: ${slug} - name: 'BMAD Custom: ${agentName}' - description: | - Custom BMAD agent: ${agentName} - - **⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - - This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file. - prompt: | - @${agentPath} - always: false - permissions: all -`; - - // Check if mode already exists - if (existingContent.includes(slug)) { + // Check if command already exists + if (await this.pathExists(commandPath)) { return { ide: 'roo', - path: this.configFile, - command: agentName, + path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), + command: commandName, type: 'custom-agent-launcher', alreadyExists: true, }; } - // Build final content - let finalContent = ''; - if (existingContent) { - // Find customModes section or add it - if (existingContent.includes('customModes:')) { - // Append to existing customModes - finalContent = existingContent + modeEntry; - } else { - // Add customModes section - finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry; - } - } else { - // Create new .roomodes file with customModes - finalContent = 'customModes:\n' + modeEntry; - } + // Read the custom agent file to extract metadata (same as regular agents) + const fullAgentPath = path.join(projectDir, agentPath); + const content = await this.readFile(fullAgentPath); - // Write .roomodes file - await this.writeFile(roomodesPath, finalContent); + // Extract metadata from agent content + const titleMatch = content.match(/title="([^"]+)"/); + const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName); + + const iconMatch = content.match(/icon="([^"]+)"/); + const icon = iconMatch ? iconMatch[1] : '🤖'; + + const whenToUseMatch = content.match(/whenToUse="([^"]+)"/); + const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; + + // Use unified method without extra content (clean) + await this.createAgentCommandFile(commandPath, { + name: title, + description: whenToUse, + agentPath: agentPath, + icon: icon, + }); return { ide: 'roo', - path: this.configFile, - command: slug, + path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), + command: commandName, type: 'custom-agent-launcher', }; } diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index 27184bc9..d05b985e 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -90,6 +90,11 @@ async function getAgentsFromDir(dirPath, moduleName) { continue; } + // Skip README files and other non-agent files + if (file.toLowerCase() === 'readme.md' || file.toLowerCase().startsWith('readme-')) { + continue; + } + if (file.includes('.customize.')) { continue; } @@ -101,6 +106,11 @@ async function getAgentsFromDir(dirPath, moduleName) { continue; } + // Only include files that have agent-specific content (compiled agents have tag) + if (!content.includes('