2025-09-28 23:17:07 -05:00
const path = require ( 'node:path' ) ;
const { BaseIdeSetup } = require ( './_base-ide' ) ;
const chalk = require ( 'chalk' ) ;
2025-11-09 20:24:56 -06:00
const { AgentCommandGenerator } = require ( './shared/agent-command-generator' ) ;
2025-09-28 23:17:07 -05:00
/ * *
* Roo IDE setup handler
2025-12-03 19:41:12 -06:00
* Creates custom commands in . roo / commands directory
2025-09-28 23:17:07 -05:00
* /
class RooSetup extends BaseIdeSetup {
constructor ( ) {
super ( 'roo' , 'Roo Code' ) ;
2025-12-03 19:41:12 -06:00
this . configDir = '.roo' ;
this . commandsDir = 'commands' ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Setup Roo IDE configuration
* @ param { string } projectDir - Project directory
* @ param { string } bmadDir - BMAD installation directory
* @ param { Object } options - Setup options
* /
async setup ( projectDir , bmadDir , options = { } ) {
console . log ( chalk . cyan ( ` Setting up ${ this . name } ... ` ) ) ;
2025-12-03 19:41:12 -06:00
// Create .roo/commands directory
const rooCommandsDir = path . join ( projectDir , this . configDir , this . commandsDir ) ;
await this . ensureDir ( rooCommandsDir ) ;
2025-09-28 23:17:07 -05:00
2025-12-03 19:41:12 -06:00
// Generate agent launchers
2025-11-09 20:24:56 -06:00
const agentGen = new AgentCommandGenerator ( this . bmadFolderName ) ;
const { artifacts : agentArtifacts } = await agentGen . collectAgentArtifacts ( bmadDir , options . selectedModules || [ ] ) ;
2025-09-28 23:17:07 -05:00
let addedCount = 0 ;
let skippedCount = 0 ;
2025-11-09 20:24:56 -06:00
for ( const artifact of agentArtifacts ) {
2025-12-03 19:41:12 -06:00
const commandName = ` bmad- ${ artifact . module } -agent- ${ artifact . name } ` ;
const commandPath = path . join ( rooCommandsDir , ` ${ commandName } .md ` ) ;
2025-09-28 23:17:07 -05:00
// Skip if already exists
2025-12-03 19:41:12 -06:00
if ( await this . pathExists ( commandPath ) ) {
console . log ( chalk . dim ( ` Skipping ${ commandName } - already exists ` ) ) ;
2025-09-28 23:17:07 -05:00
skippedCount ++ ;
continue ;
}
2025-12-15 15:08:19 +08:00
// artifact.sourcePath contains the full path to the agent file
if ( ! artifact . sourcePath ) {
console . error ( ` Error: Missing sourcePath for artifact ${ artifact . name } from module ${ artifact . module } ` ) ;
console . error ( ` Artifact object: ` , artifact ) ;
throw new Error ( ` Missing sourcePath for agent: ${ artifact . name } ` ) ;
}
const content = await this . readFile ( artifact . sourcePath ) ;
2025-11-09 20:24:56 -06:00
2025-12-13 16:22:34 +08:00
// Create command file that references the actual _bmad agent
2025-12-15 15:08:19 +08:00
await this . createCommandFile (
{ module : artifact . module , name : artifact . name , path : artifact . sourcePath } ,
content ,
commandPath ,
projectDir ,
) ;
2025-09-28 23:17:07 -05:00
addedCount ++ ;
2025-12-03 19:41:12 -06:00
console . log ( chalk . green ( ` ✓ Added command: ${ commandName } ` ) ) ;
2025-09-28 23:17:07 -05:00
}
console . log ( chalk . green ( ` ✓ ${ this . name } configured: ` ) ) ;
2025-12-03 19:41:12 -06:00
console . log ( chalk . dim ( ` - ${ addedCount } commands added ` ) ) ;
2025-09-28 23:17:07 -05:00
if ( skippedCount > 0 ) {
2025-12-03 19:41:12 -06:00
console . log ( chalk . dim ( ` - ${ skippedCount } commands skipped (already exist) ` ) ) ;
2025-09-28 23:17:07 -05:00
}
2025-12-03 19:41:12 -06:00
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 ` ) ) ;
2025-09-28 23:17:07 -05:00
return {
success : true ,
2025-12-03 19:41:12 -06:00
commands : addedCount ,
2025-09-28 23:17:07 -05:00
skipped : skippedCount ,
} ;
}
/ * *
2025-12-03 19:41:12 -06:00
* 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
2025-09-28 23:17:07 -05:00
* /
2025-12-03 19:41:12 -06:00
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 += ` <agent-activation CRITICAL="TRUE"> \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 += ` </agent-activation> \n ` ;
// Write command file
await this . writeFile ( commandPath , commandContent ) ;
}
/ * *
* Create a command file for an agent
* /
async createCommandFile ( agent , content , commandPath , projectDir ) {
2025-09-28 23:17:07 -05:00
// Extract metadata from agent content
const titleMatch = content . match ( /title="([^"]+)"/ ) ;
const title = titleMatch ? titleMatch [ 1 ] : this . formatTitle ( agent . name ) ;
const iconMatch = content . match ( /icon="([^"]+)"/ ) ;
const icon = iconMatch ? iconMatch [ 1 ] : '🤖' ;
const whenToUseMatch = content . match ( /whenToUse="([^"]+)"/ ) ;
const whenToUse = whenToUseMatch ? whenToUseMatch [ 1 ] : ` Use for ${ title } tasks ` ;
// Get relative path
const relativePath = path . relative ( projectDir , agent . path ) . replaceAll ( '\\' , '/' ) ;
2025-12-03 19:41:12 -06:00
// Use unified method
await this . createAgentCommandFile ( commandPath , {
name : title ,
description : whenToUse ,
agentPath : relativePath ,
icon : icon ,
} ) ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Format name as title
* /
formatTitle ( name ) {
return name
. split ( '-' )
. map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( ' ' ) ;
}
/ * *
* Cleanup Roo configuration
* /
async cleanup ( projectDir ) {
const fs = require ( 'fs-extra' ) ;
2025-12-03 19:41:12 -06:00
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 ++ ;
}
}
2025-09-28 23:17:07 -05:00
2025-12-03 19:41:12 -06:00
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' ) ;
2025-09-28 23:17:07 -05:00
if ( await fs . pathExists ( roomodesPath ) ) {
const content = await fs . readFile ( roomodesPath , 'utf8' ) ;
// Remove BMAD modes only
const lines = content . split ( '\n' ) ;
const filteredLines = [ ] ;
let skipMode = false ;
let removedCount = 0 ;
for ( const line of lines ) {
if ( /^\s*- slug: bmad-/ . test ( line ) ) {
skipMode = true ;
removedCount ++ ;
} else if ( skipMode && /^\s*- slug: / . test ( line ) ) {
skipMode = false ;
}
if ( ! skipMode ) {
filteredLines . push ( line ) ;
}
}
// Write back filtered content
await fs . writeFile ( roomodesPath , filteredLines . join ( '\n' ) ) ;
2025-12-03 19:41:12 -06:00
if ( removedCount > 0 ) {
console . log ( chalk . dim ( ` Removed ${ removedCount } BMAD modes from legacy .roomodes file ` ) ) ;
}
2025-09-28 23:17:07 -05:00
}
}
2025-11-22 17:10:53 -06:00
/ * *
* Install a custom agent launcher for Roo
* @ 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 )
2025-12-03 19:41:12 -06:00
* @ param { Object } metadata - Agent metadata ( unused , kept for compatibility )
2025-11-22 17:10:53 -06:00
* @ returns { Object } Installation result
* /
async installCustomAgentLauncher ( projectDir , agentName , agentPath , metadata ) {
2025-12-03 19:41:12 -06:00
const rooCommandsDir = path . join ( projectDir , this . configDir , this . commandsDir ) ;
await this . ensureDir ( rooCommandsDir ) ;
2025-11-22 17:10:53 -06:00
2025-12-03 19:41:12 -06:00
const commandName = ` bmad-custom-agent- ${ agentName . toLowerCase ( ) } ` ;
const commandPath = path . join ( rooCommandsDir , ` ${ commandName } .md ` ) ;
2025-11-22 17:10:53 -06:00
2025-12-03 19:41:12 -06:00
// Check if command already exists
if ( await this . pathExists ( commandPath ) ) {
2025-11-22 17:10:53 -06:00
return {
ide : 'roo' ,
2025-12-03 19:41:12 -06:00
path : path . join ( this . configDir , this . commandsDir , ` ${ commandName } .md ` ) ,
command : commandName ,
2025-11-22 17:10:53 -06:00
type : 'custom-agent-launcher' ,
alreadyExists : true ,
} ;
}
2025-12-03 19:41:12 -06:00
// Read the custom agent file to extract metadata (same as regular agents)
const fullAgentPath = path . join ( projectDir , agentPath ) ;
const content = await this . readFile ( fullAgentPath ) ;
// 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 ` ;
2025-11-22 17:10:53 -06:00
2025-12-03 19:41:12 -06:00
// Use unified method without extra content (clean)
await this . createAgentCommandFile ( commandPath , {
name : title ,
description : whenToUse ,
agentPath : agentPath ,
icon : icon ,
} ) ;
2025-11-22 17:10:53 -06:00
return {
ide : 'roo' ,
2025-12-03 19:41:12 -06:00
path : path . join ( this . configDir , this . commandsDir , ` ${ commandName } .md ` ) ,
command : commandName ,
2025-11-22 17:10:53 -06:00
type : 'custom-agent-launcher' ,
} ;
}
2025-09-28 23:17:07 -05:00
}
module . exports = { RooSetup } ;