2025-08-16 19:08:39 -05:00
const path = require ( 'node:path' ) ;
const fs = require ( 'fs-extra' ) ;
const yaml = require ( 'js-yaml' ) ;
const chalk = require ( 'chalk' ) ;
const inquirer = require ( 'inquirer' ) ;
2025-09-12 00:44:41 +02:00
const cjson = require ( 'comment-json' ) ;
2025-08-16 19:08:39 -05:00
const fileManager = require ( './file-manager' ) ;
const configLoader = require ( './config-loader' ) ;
const { extractYamlFromAgent } = require ( '../../lib/yaml-utils' ) ;
const BaseIdeSetup = require ( './ide-base-setup' ) ;
const resourceLocator = require ( './resource-locator' ) ;
2025-06-15 14:07:25 -05:00
2025-07-18 23:51:16 -05:00
class IdeSetup extends BaseIdeSetup {
2025-06-28 02:22:57 -05:00
constructor ( ) {
2025-07-18 23:51:16 -05:00
super ( ) ;
2025-06-28 02:22:57 -05:00
this . ideAgentConfig = null ;
}
async loadIdeAgentConfig ( ) {
if ( this . ideAgentConfig ) return this . ideAgentConfig ;
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
try {
2025-07-02 19:59:49 -05:00
const configPath = path . join ( _ _dirname , '..' , 'config' , 'ide-agent-config.yaml' ) ;
2025-06-28 02:22:57 -05:00
const configContent = await fs . readFile ( configPath , 'utf8' ) ;
this . ideAgentConfig = yaml . load ( configContent ) ;
return this . ideAgentConfig ;
2025-08-16 19:08:39 -05:00
} catch {
2025-06-28 02:22:57 -05:00
console . warn ( 'Failed to load IDE agent configuration, using defaults' ) ;
return {
'roo-permissions' : { } ,
2025-08-16 19:08:39 -05:00
'cline-order' : { } ,
2025-06-28 02:22:57 -05:00
} ;
}
}
2025-07-07 20:46:55 -05:00
async setup ( ide , installDir , selectedAgent = null , spinner = null , preConfiguredSettings = null ) {
2025-06-12 22:38:24 -05:00
const ideConfig = await configLoader . getIdeConfiguration ( ide ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
if ( ! ideConfig ) {
console . log ( chalk . yellow ( ` \n No configuration available for ${ ide } ` ) ) ;
return false ;
}
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
switch ( ide ) {
2025-08-16 19:08:39 -05:00
case 'cursor' : {
2025-06-12 22:38:24 -05:00
return this . setupCursor ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
2025-09-12 00:44:41 +02:00
case 'opencode' : {
return this . setupOpenCode ( installDir , selectedAgent , spinner , preConfiguredSettings ) ;
}
2025-08-16 19:08:39 -05:00
case 'claude-code' : {
2025-06-12 22:38:24 -05:00
return this . setupClaudeCode ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
2025-09-07 02:44:48 +08:00
case 'iflow-cli' : {
return this . setupIFlowCli ( installDir , selectedAgent ) ;
}
2025-08-16 19:08:39 -05:00
case 'crush' : {
2025-08-16 00:38:44 -03:00
return this . setupCrush ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'windsurf' : {
2025-06-12 22:38:24 -05:00
return this . setupWindsurf ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'trae' : {
2025-07-05 21:08:26 -05:00
return this . setupTrae ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'roo' : {
2025-06-14 15:06:41 -05:00
return this . setupRoo ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'cline' : {
2025-06-24 04:47:21 +02:00
return this . setupCline ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'kilo' : {
2025-08-03 15:49:39 +01:00
return this . setupKilocode ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'gemini' : {
2025-06-26 09:33:58 +07:00
return this . setupGeminiCli ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
case 'github-copilot' : {
2025-07-07 20:46:55 -05:00
return this . setupGitHubCopilot ( installDir , selectedAgent , spinner , preConfiguredSettings ) ;
2025-08-16 19:08:39 -05:00
}
case 'qwen-code' : {
2025-08-03 21:24:09 -04:00
return this . setupQwenCode ( installDir , selectedAgent ) ;
2025-08-16 19:08:39 -05:00
}
2025-09-03 00:16:26 +07:00
case 'auggie-cli' : {
return this . setupAuggieCLI ( installDir , selectedAgent , spinner , preConfiguredSettings ) ;
}
2025-08-31 18:48:03 -04:00
case 'codex' : {
return this . setupCodex ( installDir , selectedAgent , { webEnabled : false } ) ;
}
case 'codex-web' : {
return this . setupCodex ( installDir , selectedAgent , { webEnabled : true } ) ;
}
2025-08-16 19:08:39 -05:00
default : {
2025-06-12 22:38:24 -05:00
console . log ( chalk . yellow ( ` \n IDE ${ ide } not yet supported ` ) ) ;
return false ;
2025-08-16 19:08:39 -05:00
}
2025-06-12 22:38:24 -05:00
}
}
2025-09-12 00:44:41 +02:00
async setupOpenCode ( installDir , selectedAgent , spinner = null , preConfiguredSettings = null ) {
// Minimal JSON-only integration per plan:
// - If opencode.json or opencode.jsonc exists: only ensure instructions include .bmad-core/core-config.yaml
// - If none exists: create minimal opencode.jsonc with $schema and instructions array including that file
const jsonPath = path . join ( installDir , 'opencode.json' ) ;
const jsoncPath = path . join ( installDir , 'opencode.jsonc' ) ;
const hasJson = await fileManager . pathExists ( jsonPath ) ;
const hasJsonc = await fileManager . pathExists ( jsoncPath ) ;
// Determine key prefix preferences (with sensible defaults)
// Defaults: non-prefixed (agents = "dev", commands = "create-doc")
let useAgentPrefix = false ;
let useCommandPrefix = false ;
// Allow pre-configuration (if passed) to skip prompts
const pre = preConfiguredSettings && preConfiguredSettings . opencode ;
if ( pre && typeof pre . useAgentPrefix === 'boolean' ) useAgentPrefix = pre . useAgentPrefix ;
if ( pre && typeof pre . useCommandPrefix === 'boolean' ) useCommandPrefix = pre . useCommandPrefix ;
// If no pre-config and in interactive mode, prompt the user
if ( ! pre ) {
// Pause spinner during prompts if active
let spinnerWasActive = false ;
if ( spinner && spinner . isSpinning ) {
spinner . stop ( ) ;
spinnerWasActive = true ;
}
try {
const resp = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'useAgentPrefix' ,
message :
"Prefix agent keys with 'bmad-'? (Recommended to avoid collisions, e.g., 'bmad-dev')" ,
default : true ,
} ,
{
type : 'confirm' ,
name : 'useCommandPrefix' ,
message :
"Prefix command keys with 'bmad:tasks:'? (Recommended, e.g., 'bmad:tasks:create-doc')" ,
default : true ,
} ,
] ) ;
useAgentPrefix = resp . useAgentPrefix ;
useCommandPrefix = resp . useCommandPrefix ;
} catch {
// Keep defaults if prompt fails or is not interactive
} finally {
if ( spinner && spinnerWasActive ) spinner . start ( ) ;
}
}
const ensureInstructionRef = ( obj ) => {
const preferred = '.bmad-core/core-config.yaml' ;
const alt = './.bmad-core/core-config.yaml' ;
if ( ! obj . instructions ) obj . instructions = [ ] ;
if ( ! Array . isArray ( obj . instructions ) ) obj . instructions = [ obj . instructions ] ;
// Normalize alternative form (with './') to preferred without './'
obj . instructions = obj . instructions . map ( ( it ) =>
typeof it === 'string' && it === alt ? preferred : it ,
) ;
const hasPreferred = obj . instructions . some (
( it ) => typeof it === 'string' && it === preferred ,
) ;
if ( ! hasPreferred ) obj . instructions . push ( preferred ) ;
return obj ;
} ;
const mergeBmadAgentsAndCommands = async ( configObj ) => {
// Ensure objects exist
if ( ! configObj . agent || typeof configObj . agent !== 'object' ) configObj . agent = { } ;
if ( ! configObj . command || typeof configObj . command !== 'object' ) configObj . command = { } ;
if ( ! configObj . instructions ) configObj . instructions = [ ] ;
if ( ! Array . isArray ( configObj . instructions ) ) configObj . instructions = [ configObj . instructions ] ;
// Track a concise summary of changes
const summary = {
target : null ,
created : false ,
agentsAdded : 0 ,
agentsUpdated : 0 ,
agentsSkipped : 0 ,
commandsAdded : 0 ,
commandsUpdated : 0 ,
commandsSkipped : 0 ,
} ;
// Determine package scope: previously SELECTED packages in installer UI
const selectedPackages = preConfiguredSettings ? . selectedPackages || {
includeCore : true ,
packs : [ ] ,
} ;
// Helper: ensure an instruction path is present without './' prefix, de-duplicating './' variants
const ensureInstructionPath = ( pathNoDot ) => {
const withDot = ` ./ ${ pathNoDot } ` ;
// Normalize any existing './' variant to non './'
configObj . instructions = configObj . instructions . map ( ( it ) =>
typeof it === 'string' && it === withDot ? pathNoDot : it ,
) ;
const has = configObj . instructions . some ( ( it ) => typeof it === 'string' && it === pathNoDot ) ;
if ( ! has ) configObj . instructions . push ( pathNoDot ) ;
} ;
// Helper: detect orchestrator agents to set as primary mode
const isOrchestratorAgent = ( agentId ) => / ( ^ | - ) orchestrator$ / i . test ( agentId ) ;
// Helper: extract whenToUse string from an agent markdown file
const extractWhenToUseFromFile = async ( absPath ) => {
try {
const raw = await fileManager . readFile ( absPath ) ;
const yamlMatch = raw . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
const yamlBlock = yamlMatch ? yamlMatch [ 1 ] . trim ( ) : null ;
if ( ! yamlBlock ) return null ;
// Try quoted first, then unquoted
const quoted = yamlBlock . match ( /whenToUse:\s*"([^"]+)"/i ) ;
if ( quoted && quoted [ 1 ] ) return quoted [ 1 ] . trim ( ) ;
const unquoted = yamlBlock . match ( /whenToUse:\s*([^\n\r]+)/i ) ;
if ( unquoted && unquoted [ 1 ] ) return unquoted [ 1 ] . trim ( ) ;
} catch {
// ignore
}
return null ;
} ;
// Helper: extract Purpose string from a task file (YAML fenced block, Markdown heading, or inline 'Purpose:')
const extractTaskPurposeFromFile = async ( absPath ) => {
const cleanupAndSummarize = ( text ) => {
if ( ! text ) return null ;
let t = String ( text ) ;
// Drop code fences and HTML comments
t = t . replaceAll ( /```[\s\S]*?```/g , '' ) ;
t = t . replaceAll ( /<!--([\s\S]*?)-->/g , '' ) ;
// Normalize line endings
t = t . replaceAll ( /\r\n?/g , '\n' ) ;
// Take the first non-empty paragraph
const paragraphs = t . split ( /\n\s*\n/g ) . map ( ( p ) => p . trim ( ) ) ;
let first = paragraphs . find ( ( p ) => p . length > 0 ) || '' ;
// Remove leading list markers, quotes, and headings remnants
first = first . replaceAll ( /^\s*[>*-]\s+/gm , '' ) ;
first = first . replaceAll ( /^#{1,6}\s+/gm , '' ) ;
// Strip simple Markdown formatting
first = first . replaceAll ( /\*\*([^*]+)\*\*/g , '$1' ) . replaceAll ( /\*([^*]+)\*/g , '$1' ) ;
first = first . replaceAll ( /`([^`]+)`/g , '$1' ) ;
// Collapse whitespace
first = first . replaceAll ( /\s+/g , ' ' ) . trim ( ) ;
if ( ! first ) return null ;
// Prefer ending at a sentence boundary if long
const maxLen = 320 ;
if ( first . length > maxLen ) {
const boundary = first . slice ( 0 , maxLen + 40 ) . match ( /^[\s\S]*?[.!?](\s|$)/ ) ;
const cut = boundary ? boundary [ 0 ] : first . slice ( 0 , maxLen ) ;
return cut . trim ( ) ;
}
return first ;
} ;
try {
const raw = await fileManager . readFile ( absPath ) ;
// 1) YAML fenced block: look for Purpose fields
const yamlMatch = raw . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
const yamlBlock = yamlMatch ? yamlMatch [ 1 ] . trim ( ) : null ;
if ( yamlBlock ) {
try {
const data = yaml . load ( yamlBlock ) ;
if ( data ) {
let val = data . Purpose ? ? data . purpose ;
if ( ! val && data . task && ( data . task . Purpose || data . task . purpose ) ) {
val = data . task . Purpose ? ? data . task . purpose ;
}
if ( typeof val === 'string' ) {
const cleaned = cleanupAndSummarize ( val ) ;
if ( cleaned ) return cleaned ;
}
}
} catch {
// ignore YAML parse errors
}
// Fallback regex inside YAML block
const quoted = yamlBlock . match ( /(?:^|\n)\s*(?:Purpose|purpose):\s*"([^"]+)"/ ) ;
if ( quoted && quoted [ 1 ] ) {
const cleaned = cleanupAndSummarize ( quoted [ 1 ] ) ;
if ( cleaned ) return cleaned ;
}
const unquoted = yamlBlock . match ( /(?:^|\n)\s*(?:Purpose|purpose):\s*([^\n\r]+)/ ) ;
if ( unquoted && unquoted [ 1 ] ) {
const cleaned = cleanupAndSummarize ( unquoted [ 1 ] ) ;
if ( cleaned ) return cleaned ;
}
}
// 2) Markdown heading section: ## Purpose (any level >= 2)
const headingRe = /^(#{2,6})\s*Purpose\s*$/im ;
const headingMatch = headingRe . exec ( raw ) ;
if ( headingMatch ) {
const headingLevel = headingMatch [ 1 ] . length ;
const sectionStart = headingMatch . index + headingMatch [ 0 ] . length ;
const rest = raw . slice ( sectionStart ) ;
// Next heading of same or higher level ends the section
const nextHeadingRe = new RegExp ( ` ^#{1, ${ headingLevel } } \\ s+[^ \n ]+ ` , 'im' ) ;
const nextMatch = nextHeadingRe . exec ( rest ) ;
const section = nextMatch ? rest . slice ( 0 , nextMatch . index ) : rest ;
const cleaned = cleanupAndSummarize ( section ) ;
if ( cleaned ) return cleaned ;
}
// 3) Inline single-line fallback: Purpose: ...
const inline = raw . match ( /(?:^|\n)\s*Purpose\s*:\s*([^\n\r]+)/i ) ;
if ( inline && inline [ 1 ] ) {
const cleaned = cleanupAndSummarize ( inline [ 1 ] ) ;
if ( cleaned ) return cleaned ;
}
} catch {
// ignore
}
return null ;
} ;
// Build core sets
const coreAgentIds = new Set ( ) ;
const coreTaskIds = new Set ( ) ;
if ( selectedPackages . includeCore ) {
for ( const id of await this . getCoreAgentIds ( installDir ) ) coreAgentIds . add ( id ) ;
for ( const id of await this . getCoreTaskIds ( installDir ) ) coreTaskIds . add ( id ) ;
}
// Build packs info: { packId, packPath, packKey, agents:Set, tasks:Set }
const packsInfo = [ ] ;
if ( Array . isArray ( selectedPackages . packs ) ) {
for ( const packId of selectedPackages . packs ) {
const dotPackPath = path . join ( installDir , ` . ${ packId } ` ) ;
const altPackPath = path . join ( installDir , 'expansion-packs' , packId ) ;
const packPath = ( await fileManager . pathExists ( dotPackPath ) )
? dotPackPath
: ( await fileManager . pathExists ( altPackPath ) )
? altPackPath
: null ;
if ( ! packPath ) continue ;
// Ensure pack config.yaml is added to instructions (relative path, no './')
const packConfigAbs = path . join ( packPath , 'config.yaml' ) ;
if ( await fileManager . pathExists ( packConfigAbs ) ) {
const relCfg = path . relative ( installDir , packConfigAbs ) . replaceAll ( '\\' , '/' ) ;
ensureInstructionPath ( relCfg ) ;
}
const packKey = packId . replace ( /^bmad-/ , '' ) . replaceAll ( '/' , '-' ) ;
const info = { packId , packPath , packKey , agents : new Set ( ) , tasks : new Set ( ) } ;
const glob = require ( 'glob' ) ;
const agentsDir = path . join ( packPath , 'agents' ) ;
if ( await fileManager . pathExists ( agentsDir ) ) {
const files = glob . sync ( '*.md' , { cwd : agentsDir } ) ;
for ( const f of files ) info . agents . add ( path . basename ( f , '.md' ) ) ;
}
const tasksDir = path . join ( packPath , 'tasks' ) ;
if ( await fileManager . pathExists ( tasksDir ) ) {
const files = glob . sync ( '*.md' , { cwd : tasksDir } ) ;
for ( const f of files ) info . tasks . add ( path . basename ( f , '.md' ) ) ;
}
packsInfo . push ( info ) ;
}
}
// Generate agents - core first (respect optional agent prefix)
for ( const agentId of coreAgentIds ) {
const p = await this . findAgentPath ( agentId , installDir ) ; // prefers core
if ( ! p ) continue ;
const rel = path . relative ( installDir , p ) . replaceAll ( '\\' , '/' ) ;
const fileRef = ` {file:./ ${ rel } } ` ;
const baseKey = agentId ;
const key = useAgentPrefix
? baseKey . startsWith ( 'bmad-' )
? baseKey
: ` bmad- ${ baseKey } `
: baseKey ;
const existing = configObj . agent [ key ] ;
const whenToUse = await extractWhenToUseFromFile ( p ) ;
const agentDef = {
prompt : fileRef ,
mode : isOrchestratorAgent ( agentId ) ? 'primary' : 'all' ,
tools : { write : true , edit : true , bash : true } ,
... ( whenToUse ? { description : whenToUse } : { } ) ,
} ;
if ( ! existing ) {
configObj . agent [ key ] = agentDef ;
summary . agentsAdded ++ ;
} else if (
existing &&
typeof existing === 'object' &&
typeof existing . prompt === 'string' &&
existing . prompt . includes ( rel )
) {
existing . prompt = agentDef . prompt ;
existing . mode = agentDef . mode ;
if ( whenToUse ) existing . description = whenToUse ;
existing . tools = { write : true , edit : true , bash : true } ;
configObj . agent [ key ] = existing ;
summary . agentsUpdated ++ ;
} else {
summary . agentsSkipped ++ ;
// Collision warning: key exists but does not appear BMAD-managed (different prompt path)
console . log (
chalk . yellow (
` ⚠︎ Skipped agent key ' ${ key } ' (existing entry not BMAD-managed). Tip: enable agent prefixes to avoid collisions. ` ,
) ,
) ;
}
}
// Generate agents - expansion packs (forced pack-specific prefix)
for ( const pack of packsInfo ) {
for ( const agentId of pack . agents ) {
const p = path . join ( pack . packPath , 'agents' , ` ${ agentId } .md ` ) ;
if ( ! ( await fileManager . pathExists ( p ) ) ) continue ;
const rel = path . relative ( installDir , p ) . replaceAll ( '\\' , '/' ) ;
const fileRef = ` {file:./ ${ rel } } ` ;
const prefixedKey = ` bmad- ${ pack . packKey } - ${ agentId } ` ;
const existing = configObj . agent [ prefixedKey ] ;
const whenToUse = await extractWhenToUseFromFile ( p ) ;
const agentDef = {
prompt : fileRef ,
mode : isOrchestratorAgent ( agentId ) ? 'primary' : 'all' ,
tools : { write : true , edit : true , bash : true } ,
... ( whenToUse ? { description : whenToUse } : { } ) ,
} ;
if ( ! existing ) {
configObj . agent [ prefixedKey ] = agentDef ;
summary . agentsAdded ++ ;
} else if (
existing &&
typeof existing === 'object' &&
typeof existing . prompt === 'string' &&
existing . prompt . includes ( rel )
) {
existing . prompt = agentDef . prompt ;
existing . mode = agentDef . mode ;
if ( whenToUse ) existing . description = whenToUse ;
existing . tools = { write : true , edit : true , bash : true } ;
configObj . agent [ prefixedKey ] = existing ;
summary . agentsUpdated ++ ;
} else {
summary . agentsSkipped ++ ;
console . log (
chalk . yellow (
` ⚠︎ Skipped agent key ' ${ prefixedKey } ' (existing entry not BMAD-managed). Tip: enable agent prefixes to avoid collisions. ` ,
) ,
) ;
}
}
}
// Generate commands - core first (respect optional command prefix)
for ( const taskId of coreTaskIds ) {
const p = await this . findTaskPath ( taskId , installDir ) ; // prefers core/common
if ( ! p ) continue ;
const rel = path . relative ( installDir , p ) . replaceAll ( '\\' , '/' ) ;
const fileRef = ` {file:./ ${ rel } } ` ;
const key = useCommandPrefix ? ` bmad:tasks: ${ taskId } ` : ` ${ taskId } ` ;
const existing = configObj . command [ key ] ;
const purpose = await extractTaskPurposeFromFile ( p ) ;
const cmdDef = { template : fileRef , ... ( purpose ? { description : purpose } : { } ) } ;
if ( ! existing ) {
configObj . command [ key ] = cmdDef ;
summary . commandsAdded ++ ;
} else if (
existing &&
typeof existing === 'object' &&
typeof existing . template === 'string' &&
existing . template . includes ( rel )
) {
existing . template = cmdDef . template ;
if ( purpose ) existing . description = purpose ;
configObj . command [ key ] = existing ;
summary . commandsUpdated ++ ;
} else {
summary . commandsSkipped ++ ;
console . log (
chalk . yellow (
` ⚠︎ Skipped command key ' ${ key } ' (existing entry not BMAD-managed). Tip: enable command prefixes to avoid collisions. ` ,
) ,
) ;
}
}
// Generate commands - expansion packs (forced pack-specific prefix)
for ( const pack of packsInfo ) {
for ( const taskId of pack . tasks ) {
const p = path . join ( pack . packPath , 'tasks' , ` ${ taskId } .md ` ) ;
if ( ! ( await fileManager . pathExists ( p ) ) ) continue ;
const rel = path . relative ( installDir , p ) . replaceAll ( '\\' , '/' ) ;
const fileRef = ` {file:./ ${ rel } } ` ;
const prefixedKey = ` bmad: ${ pack . packKey } : ${ taskId } ` ;
const existing = configObj . command [ prefixedKey ] ;
const purpose = await extractTaskPurposeFromFile ( p ) ;
const cmdDef = { template : fileRef , ... ( purpose ? { description : purpose } : { } ) } ;
if ( ! existing ) {
configObj . command [ prefixedKey ] = cmdDef ;
summary . commandsAdded ++ ;
} else if (
existing &&
typeof existing === 'object' &&
typeof existing . template === 'string' &&
existing . template . includes ( rel )
) {
existing . template = cmdDef . template ;
if ( purpose ) existing . description = purpose ;
configObj . command [ prefixedKey ] = existing ;
summary . commandsUpdated ++ ;
} else {
summary . commandsSkipped ++ ;
console . log (
chalk . yellow (
` ⚠︎ Skipped command key ' ${ prefixedKey } ' (existing entry not BMAD-managed). Tip: enable command prefixes to avoid collisions. ` ,
) ,
) ;
}
}
}
return { configObj , summary } ;
} ;
// Helper: generate AGENTS.md section for OpenCode (acts as system prompt memory)
const generateOpenCodeAgentsMd = async ( ) => {
try {
const filePath = path . join ( installDir , 'AGENTS.md' ) ;
const startMarker = '<!-- BEGIN: BMAD-AGENTS-OPENCODE -->' ;
const endMarker = '<!-- END: BMAD-AGENTS-OPENCODE -->' ;
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
const tasks = await this . getAllTaskIds ( installDir ) ;
let section = '' ;
section += ` ${ startMarker } \n ` ;
section += ` # BMAD-METHOD Agents and Tasks (OpenCode) \n \n ` ;
section += ` OpenCode reads AGENTS.md during initialization and uses it as part of its system prompt for the session. This section is auto-generated by BMAD-METHOD for OpenCode. \n \n ` ;
section += ` ## How To Use With OpenCode \n \n ` ;
section += ` - Run \` opencode \` in this project. OpenCode will read \` AGENTS.md \` and your OpenCode config (opencode.json[c]). \n ` ;
section += ` - Reference a role naturally, e.g., "As dev, implement ..." or use commands defined in your BMAD tasks. \n ` ;
section += ` - Commit \` .bmad-core \` and \` AGENTS.md \` if you want teammates to share the same configuration. \n ` ;
section += ` - Refresh this section after BMAD updates: \` npx bmad-method install -f -i opencode \` . \n \n ` ;
section += ` ### Helpful Commands \n \n ` ;
section += ` - List agents: \` npx bmad-method list:agents \` \n ` ;
section += ` - Reinstall BMAD core and regenerate this section: \` npx bmad-method install -f -i opencode \` \n ` ;
section += ` - Validate configuration: \` npx bmad-method validate \` \n \n ` ;
// Brief context note for modes and tools
section += ` Note \n ` ;
section += ` - Orchestrators run as mode: primary; other agents as all. \n ` ;
section += ` - All agents have tools enabled: write, edit, bash. \n \n ` ;
section += ` ## Agents \n \n ` ;
section += ` ### Directory \n \n ` ;
section += ` | Title | ID | When To Use | \n |---|---|---| \n ` ;
// Fallback descriptions for core agents (used if whenToUse is missing)
const fallbackDescriptions = {
'ux-expert' :
'Use for UI/UX design, wireframes, prototypes, front-end specs, and user experience optimization' ,
sm : 'Use for story creation, epic management, retrospectives in party-mode, and agile process guidance' ,
qa : 'Ensure quality strategy, test design, risk profiling, and QA gates across features' ,
po : 'Backlog management, story refinement, acceptance criteria, sprint planning, prioritization decisions' ,
pm : 'PRDs, product strategy, feature prioritization, roadmap planning, and stakeholder communication' ,
dev : 'Code implementation, debugging, refactoring, and development best practices' ,
'bmad-orchestrator' :
'Workflow coordination, multi-agent tasks, role switching guidance, and when unsure which specialist to consult' ,
'bmad-master' :
'Comprehensive cross-domain execution for tasks that do not require a specific persona' ,
architect :
'System design, architecture docs, technology selection, API design, and infrastructure planning' ,
analyst :
'Discovery/research, competitive analysis, project briefs, initial discovery, and brownfield documentation' ,
} ;
const sanitizeDesc = ( s ) => {
if ( ! s ) return '' ;
let t = String ( s ) . trim ( ) ;
// Drop surrounding single/double/backtick quotes
t = t . replaceAll ( /^['"`]+|['"`]+$/g , '' ) ;
// Collapse whitespace
t = t . replaceAll ( /\s+/g , ' ' ) . trim ( ) ;
return t ;
} ;
const agentSummaries = [ ] ;
for ( const agentId of agents ) {
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( ! agentPath ) continue ;
let whenToUse = '' ;
try {
const raw = await fileManager . readFile ( agentPath ) ;
const yamlMatch = raw . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
const yamlBlock = yamlMatch ? yamlMatch [ 1 ] . trim ( ) : null ;
if ( yamlBlock ) {
try {
const data = yaml . load ( yamlBlock ) ;
if ( data && typeof data . whenToUse === 'string' ) {
whenToUse = data . whenToUse ;
}
} catch {
// ignore YAML parse errors
}
if ( ! whenToUse ) {
// Fallback regex supporting single or double quotes
const m1 = yamlBlock . match ( /whenToUse:\s*"([^\n"]+)"/i ) ;
const m2 = yamlBlock . match ( /whenToUse:\s*'([^\n']+)'/i ) ;
const m3 = yamlBlock . match ( /whenToUse:\s*([^\n\r]+)/i ) ;
whenToUse = ( m1 ? . [ 1 ] || m2 ? . [ 1 ] || m3 ? . [ 1 ] || '' ) . trim ( ) ;
}
}
} catch {
// ignore read/parse errors for agent metadata extraction
}
const title = await this . getAgentTitle ( agentId , installDir ) ;
const finalDesc = sanitizeDesc ( whenToUse ) || fallbackDescriptions [ agentId ] || '—' ;
agentSummaries . push ( { agentId , title , whenToUse : finalDesc , path : agentPath } ) ;
// Strict 3-column row
section += ` | ${ title } | ${ agentId } | ${ finalDesc } | \n ` ;
}
section += ` \n ` ;
for ( const { agentId , title , whenToUse , path : agentPath } of agentSummaries ) {
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
section += ` ### ${ title } (id: ${ agentId } ) \n ` ;
section += ` Source: [ ${ relativePath } ]( ${ relativePath } ) \n \n ` ;
if ( whenToUse ) section += ` - When to use: ${ whenToUse } \n ` ;
section += ` - How to activate: Mention "As ${ agentId } , ..." to get role-aligned behavior \n ` ;
section += ` - Full definition: open the source file above (content not embedded) \n \n ` ;
}
if ( tasks && tasks . length > 0 ) {
section += ` ## Tasks \n \n ` ;
section += ` These are reusable task briefs; use the paths to open them as needed. \n \n ` ;
for ( const taskId of tasks ) {
const taskPath = await this . findTaskPath ( taskId , installDir ) ;
if ( ! taskPath ) continue ;
const relativePath = path . relative ( installDir , taskPath ) . replaceAll ( '\\' , '/' ) ;
section += ` ### Task: ${ taskId } \n ` ;
section += ` Source: [ ${ relativePath } ]( ${ relativePath } ) \n ` ;
section += ` - How to use: Reference the task in your prompt or execute via your configured commands. \n ` ;
section += ` - Full brief: open the source file above (content not embedded) \n \n ` ;
}
}
section += ` ${ endMarker } \n ` ;
let finalContent = '' ;
if ( await fileManager . pathExists ( filePath ) ) {
const existing = await fileManager . readFile ( filePath ) ;
if ( existing . includes ( startMarker ) && existing . includes ( endMarker ) ) {
const pattern = String . raw ` ${ startMarker } [ \s \S ]*? ${ endMarker } ` ;
const replaced = existing . replace ( new RegExp ( pattern , 'm' ) , section ) ;
finalContent = replaced ;
} else {
finalContent = existing . trimEnd ( ) + ` \n \n ` + section ;
}
} else {
finalContent += '# Project Agents\n\n' ;
finalContent += 'This file provides guidance and memory for your coding CLI.\n\n' ;
finalContent += section ;
}
await fileManager . writeFile ( filePath , finalContent ) ;
console . log ( chalk . green ( '✓ Created/updated AGENTS.md for OpenCode CLI integration' ) ) ;
console . log (
chalk . dim (
'OpenCode reads AGENTS.md automatically on init. Run `opencode` in this project to use BMAD agents.' ,
) ,
) ;
} catch {
console . log ( chalk . yellow ( '⚠︎ Skipped creating AGENTS.md for OpenCode (write failed)' ) ) ;
}
} ;
if ( hasJson || hasJsonc ) {
// Preserve existing top-level fields; only touch instructions
const targetPath = hasJsonc ? jsoncPath : jsonPath ;
try {
const raw = await fs . readFile ( targetPath , 'utf8' ) ;
// Use comment-json for both .json and .jsonc for resilience
const parsed = cjson . parse ( raw , undefined , true ) ;
ensureInstructionRef ( parsed ) ;
const { configObj , summary } = await mergeBmadAgentsAndCommands ( parsed ) ;
const output = cjson . stringify ( parsed , null , 2 ) ;
await fs . writeFile ( targetPath , output + ( output . endsWith ( '\n' ) ? '' : '\n' ) ) ;
console . log (
chalk . green (
'✓ Updated OpenCode config: ensured BMAD instructions and merged agents/commands' ,
) ,
) ;
// Summary output
console . log (
chalk . dim (
` File: ${ path . basename ( targetPath ) } | Agents + ${ summary . agentsAdded } ~ ${ summary . agentsUpdated } ⨯ ${ summary . agentsSkipped } | Commands + ${ summary . commandsAdded } ~ ${ summary . commandsUpdated } ⨯ ${ summary . commandsSkipped } ` ,
) ,
) ;
// Ensure AGENTS.md is created/updated for OpenCode as well
await generateOpenCodeAgentsMd ( ) ;
} catch ( error ) {
console . log ( chalk . red ( '✗ Failed to update existing OpenCode config' ) , error . message ) ;
return false ;
}
return true ;
}
// Create minimal opencode.jsonc
const minimal = {
$schema : 'https://opencode.ai/config.json' ,
instructions : [ '.bmad-core/core-config.yaml' ] ,
agent : { } ,
command : { } ,
} ;
try {
const { configObj , summary } = await mergeBmadAgentsAndCommands ( minimal ) ;
const output = cjson . stringify ( minimal , null , 2 ) ;
await fs . writeFile ( jsoncPath , output + ( output . endsWith ( '\n' ) ? '' : '\n' ) ) ;
console . log (
chalk . green ( '✓ Created opencode.jsonc with BMAD instructions, agents, and commands' ) ,
) ;
console . log (
chalk . dim (
` File: opencode.jsonc | Agents + ${ summary . agentsAdded } | Commands + ${ summary . commandsAdded } ` ,
) ,
) ;
// Also create/update AGENTS.md for OpenCode on new-config path
await generateOpenCodeAgentsMd ( ) ;
return true ;
} catch ( error ) {
console . log ( chalk . red ( '✗ Failed to create opencode.jsonc' ) , error . message ) ;
return false ;
}
}
2025-08-31 18:48:03 -04:00
async setupCodex ( installDir , selectedAgent , options ) {
options = options ? ? { webEnabled : false } ;
// Codex reads AGENTS.md at the project root as project memory (CLI & Web).
// Inject/update a BMAD section with guidance, directory, and details.
const filePath = path . join ( installDir , 'AGENTS.md' ) ;
const startMarker = '<!-- BEGIN: BMAD-AGENTS -->' ;
const endMarker = '<!-- END: BMAD-AGENTS -->' ;
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
const tasks = await this . getAllTaskIds ( installDir ) ;
// Build BMAD section content
let section = '' ;
section += ` ${ startMarker } \n ` ;
section += ` # BMAD-METHOD Agents and Tasks \n \n ` ;
section += ` This section is auto-generated by BMAD-METHOD for Codex. Codex merges this AGENTS.md into context. \n \n ` ;
section += ` ## How To Use With Codex \n \n ` ;
section += ` - Codex CLI: run \` codex \` in this project. Reference an agent naturally, e.g., "As dev, implement ...". \n ` ;
section += ` - Codex Web: open this repo and reference roles the same way; Codex reads \` AGENTS.md \` . \n ` ;
section += ` - Commit \` .bmad-core \` and this \` AGENTS.md \` file to your repo so Codex (Web/CLI) can read full agent definitions. \n ` ;
section += ` - Refresh this section after agent updates: \` npx bmad-method install -f -i codex \` . \n \n ` ;
section += ` ### Helpful Commands \n \n ` ;
section += ` - List agents: \` npx bmad-method list:agents \` \n ` ;
section += ` - Reinstall BMAD core and regenerate AGENTS.md: \` npx bmad-method install -f -i codex \` \n ` ;
section += ` - Validate configuration: \` npx bmad-method validate \` \n \n ` ;
// Agents directory table
section += ` ## Agents \n \n ` ;
section += ` ### Directory \n \n ` ;
section += ` | Title | ID | When To Use | \n |---|---|---| \n ` ;
const agentSummaries = [ ] ;
for ( const agentId of agents ) {
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( ! agentPath ) continue ;
const raw = await fileManager . readFile ( agentPath ) ;
const yamlMatch = raw . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
const yamlBlock = yamlMatch ? yamlMatch [ 1 ] . trim ( ) : null ;
const title = await this . getAgentTitle ( agentId , installDir ) ;
const whenToUse = yamlBlock ? . match ( /whenToUse:\s*"?([^\n"]+)"?/i ) ? . [ 1 ] ? . trim ( ) || '' ;
agentSummaries . push ( { agentId , title , whenToUse , yamlBlock , raw , path : agentPath } ) ;
section += ` | ${ title } | ${ agentId } | ${ whenToUse || '—' } | \n ` ;
}
section += ` \n ` ;
// Detailed agent sections
for ( const { agentId , title , whenToUse , yamlBlock , raw , path : agentPath } of agentSummaries ) {
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
section += ` ### ${ title } (id: ${ agentId } ) \n ` ;
section += ` Source: ${ relativePath } \n \n ` ;
if ( whenToUse ) section += ` - When to use: ${ whenToUse } \n ` ;
section += ` - How to activate: Mention "As ${ agentId } , ..." or "Use ${ title } to ..." \n \n ` ;
if ( yamlBlock ) {
section += '```yaml\n' + yamlBlock + '\n```\n\n' ;
} else {
section += '```md\n' + raw . trim ( ) + '\n```\n\n' ;
}
}
// Tasks
if ( tasks && tasks . length > 0 ) {
section += ` ## Tasks \n \n ` ;
section += ` These are reusable task briefs you can reference directly in Codex. \n \n ` ;
for ( const taskId of tasks ) {
const taskPath = await this . findTaskPath ( taskId , installDir ) ;
if ( ! taskPath ) continue ;
const raw = await fileManager . readFile ( taskPath ) ;
const relativePath = path . relative ( installDir , taskPath ) . replaceAll ( '\\' , '/' ) ;
section += ` ### Task: ${ taskId } \n ` ;
section += ` Source: ${ relativePath } \n ` ;
section += ` - How to use: "Use task ${ taskId } with the appropriate agent" and paste relevant parts as needed. \n \n ` ;
section += '```md\n' + raw . trim ( ) + '\n```\n\n' ;
}
}
section += ` ${ endMarker } \n ` ;
// Write or update AGENTS.md
let finalContent = '' ;
if ( await fileManager . pathExists ( filePath ) ) {
const existing = await fileManager . readFile ( filePath ) ;
if ( existing . includes ( startMarker ) && existing . includes ( endMarker ) ) {
// Replace existing BMAD block
const pattern = String . raw ` ${ startMarker } [ \s \S ]*? ${ endMarker } ` ;
const replaced = existing . replace ( new RegExp ( pattern , 'm' ) , section ) ;
finalContent = replaced ;
} else {
// Append BMAD block to existing file
finalContent = existing . trimEnd ( ) + ` \n \n ` + section ;
}
} else {
// Create fresh AGENTS.md with a small header and BMAD block
finalContent += '# Project Agents\n\n' ;
finalContent += 'This file provides guidance and memory for Codex CLI.\n\n' ;
finalContent += section ;
}
await fileManager . writeFile ( filePath , finalContent ) ;
console . log ( chalk . green ( '✓ Created/updated AGENTS.md for Codex CLI integration' ) ) ;
console . log (
chalk . dim (
'Codex reads AGENTS.md automatically. Run `codex` in this project to use BMAD agents.' ,
) ,
) ;
// Optionally add helpful npm scripts if a package.json exists
try {
const pkgPath = path . join ( installDir , 'package.json' ) ;
if ( await fileManager . pathExists ( pkgPath ) ) {
const pkgRaw = await fileManager . readFile ( pkgPath ) ;
const pkg = JSON . parse ( pkgRaw ) ;
pkg . scripts = pkg . scripts || { } ;
const updated = { ... pkg . scripts } ;
if ( ! updated [ 'bmad:refresh' ] ) updated [ 'bmad:refresh' ] = 'bmad-method install -f -i codex' ;
if ( ! updated [ 'bmad:list' ] ) updated [ 'bmad:list' ] = 'bmad-method list:agents' ;
if ( ! updated [ 'bmad:validate' ] ) updated [ 'bmad:validate' ] = 'bmad-method validate' ;
const changed = JSON . stringify ( updated ) !== JSON . stringify ( pkg . scripts ) ;
if ( changed ) {
const newPkg = { ... pkg , scripts : updated } ;
await fileManager . writeFile ( pkgPath , JSON . stringify ( newPkg , null , 2 ) + '\n' ) ;
console . log ( chalk . green ( '✓ Added npm scripts: bmad:refresh, bmad:list, bmad:validate' ) ) ;
}
}
} catch {
console . log (
chalk . yellow ( '⚠︎ Skipped adding npm scripts (package.json not writable or invalid)' ) ,
) ;
}
// Adjust .gitignore behavior depending on Codex mode
try {
const gitignorePath = path . join ( installDir , '.gitignore' ) ;
const ignoreLines = [ '# BMAD (local only)' , '.bmad-core/' , '.bmad-*/' ] ;
const exists = await fileManager . pathExists ( gitignorePath ) ;
if ( options . webEnabled ) {
if ( exists ) {
let gi = await fileManager . readFile ( gitignorePath ) ;
const updated = gi
. split ( /\r?\n/ )
. filter ( ( l ) => ! /^\s*\.bmad-core\/?\s*$/ . test ( l ) && ! /^\s*\.bmad-\*\/?\s*$/ . test ( l ) )
. join ( '\n' ) ;
if ( updated !== gi ) {
await fileManager . writeFile ( gitignorePath , updated . trimEnd ( ) + '\n' ) ;
console . log ( chalk . green ( '✓ Updated .gitignore to include .bmad-core in commits' ) ) ;
}
}
} else {
// Local-only: add ignores if missing
let base = exists ? await fileManager . readFile ( gitignorePath ) : '' ;
const haveCore = base . includes ( '.bmad-core/' ) ;
const haveStar = base . includes ( '.bmad-*/' ) ;
if ( ! haveCore || ! haveStar ) {
const sep = base . endsWith ( '\n' ) || base . length === 0 ? '' : '\n' ;
const add = [ ! haveCore || ! haveStar ? ignoreLines . join ( '\n' ) : '' ]
. filter ( Boolean )
. join ( '\n' ) ;
const out = base + sep + add + '\n' ;
await fileManager . writeFile ( gitignorePath , out ) ;
console . log ( chalk . green ( '✓ Added .bmad-core/* to .gitignore for local-only Codex setup' ) ) ;
}
}
} catch {
console . log ( chalk . yellow ( '⚠︎ Could not update .gitignore (skipping)' ) ) ;
}
return true ;
}
2025-06-12 22:38:24 -05:00
async setupCursor ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const cursorRulesDir = path . join ( installDir , '.cursor' , 'rules' , 'bmad' ) ;
2025-06-19 12:55:16 -05:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
await fileManager . ensureDirectory ( cursorRulesDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
for ( const agentId of agents ) {
2025-06-28 02:22:57 -05:00
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-28 02:22:57 -05:00
if ( agentPath ) {
2025-07-18 23:51:16 -05:00
const mdcContent = await this . createAgentRuleContent ( agentId , agentPath , installDir , 'mdc' ) ;
2025-06-12 22:38:24 -05:00
const mdcPath = path . join ( cursorRulesDir , ` ${ agentId } .mdc ` ) ;
await fileManager . writeFile ( mdcPath , mdcContent ) ;
console . log ( chalk . green ( ` ✓ Created rule: ${ agentId } .mdc ` ) ) ;
}
}
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
console . log ( chalk . green ( ` \n ✓ Created Cursor rules in ${ cursorRulesDir } ` ) ) ;
return true ;
}
2025-08-16 00:38:44 -03:00
async setupCrush ( installDir , selectedAgent ) {
// Setup bmad-core commands
const coreSlashPrefix = await this . getCoreSlashPrefix ( installDir ) ;
const coreAgents = selectedAgent ? [ selectedAgent ] : await this . getCoreAgentIds ( installDir ) ;
const coreTasks = await this . getCoreTaskIds ( installDir ) ;
2025-08-16 19:08:39 -05:00
await this . setupCrushForPackage (
installDir ,
'core' ,
coreSlashPrefix ,
coreAgents ,
coreTasks ,
'.bmad-core' ,
) ;
2025-08-16 00:38:44 -03:00
// Setup expansion pack commands
const expansionPacks = await this . getInstalledExpansionPacks ( installDir ) ;
for ( const packInfo of expansionPacks ) {
const packSlashPrefix = await this . getExpansionPackSlashPrefix ( packInfo . path ) ;
const packAgents = await this . getExpansionPackAgents ( packInfo . path ) ;
const packTasks = await this . getExpansionPackTasks ( packInfo . path ) ;
if ( packAgents . length > 0 || packTasks . length > 0 ) {
// Use the actual directory name where the expansion pack is installed
const rootPath = path . relative ( installDir , packInfo . path ) ;
2025-08-16 19:08:39 -05:00
await this . setupCrushForPackage (
installDir ,
packInfo . name ,
packSlashPrefix ,
packAgents ,
packTasks ,
rootPath ,
) ;
2025-08-16 00:38:44 -03:00
}
}
return true ;
}
2025-06-12 22:38:24 -05:00
async setupClaudeCode ( installDir , selectedAgent ) {
2025-07-12 21:08:13 -05:00
// Setup bmad-core commands
const coreSlashPrefix = await this . getCoreSlashPrefix ( installDir ) ;
const coreAgents = selectedAgent ? [ selectedAgent ] : await this . getCoreAgentIds ( installDir ) ;
const coreTasks = await this . getCoreTaskIds ( installDir ) ;
2025-08-16 19:08:39 -05:00
await this . setupClaudeCodeForPackage (
installDir ,
'core' ,
coreSlashPrefix ,
coreAgents ,
coreTasks ,
'.bmad-core' ,
) ;
2025-07-12 21:08:13 -05:00
// Setup expansion pack commands
const expansionPacks = await this . getInstalledExpansionPacks ( installDir ) ;
for ( const packInfo of expansionPacks ) {
const packSlashPrefix = await this . getExpansionPackSlashPrefix ( packInfo . path ) ;
const packAgents = await this . getExpansionPackAgents ( packInfo . path ) ;
const packTasks = await this . getExpansionPackTasks ( packInfo . path ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
if ( packAgents . length > 0 || packTasks . length > 0 ) {
// Use the actual directory name where the expansion pack is installed
const rootPath = path . relative ( installDir , packInfo . path ) ;
2025-08-16 19:08:39 -05:00
await this . setupClaudeCodeForPackage (
installDir ,
packInfo . name ,
packSlashPrefix ,
packAgents ,
packTasks ,
rootPath ,
) ;
2025-07-12 21:08:13 -05:00
}
}
2025-06-14 15:06:41 -05:00
2025-07-12 21:08:13 -05:00
return true ;
}
2025-06-14 15:06:41 -05:00
2025-08-16 19:08:39 -05:00
async setupClaudeCodeForPackage (
installDir ,
packageName ,
slashPrefix ,
agentIds ,
taskIds ,
rootPath ,
) {
const commandsBaseDir = path . join ( installDir , '.claude' , 'commands' , slashPrefix ) ;
const agentsDir = path . join ( commandsBaseDir , 'agents' ) ;
const tasksDir = path . join ( commandsBaseDir , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
// Ensure directories exist
await fileManager . ensureDirectory ( agentsDir ) ;
await fileManager . ensureDirectory ( tasksDir ) ;
// Setup agents
for ( const agentId of agentIds ) {
// Find the agent file - for expansion packs, prefer the expansion pack version
let agentPath ;
2025-08-16 19:08:39 -05:00
if ( packageName === 'core' ) {
// For core, use the normal search
agentPath = await this . findAgentPath ( agentId , installDir ) ;
} else {
2025-07-12 21:08:13 -05:00
// For expansion packs, first try to find the agent in the expansion pack directory
2025-08-16 19:08:39 -05:00
const expansionPackPath = path . join ( installDir , rootPath , 'agents' , ` ${ agentId } .md ` ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( expansionPackPath ) ) {
agentPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
agentPath = await this . findAgentPath ( agentId , installDir ) ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
const commandPath = path . join ( agentsDir , ` ${ agentId } .md ` ) ;
2025-06-14 15:06:41 -05:00
2025-06-28 02:22:57 -05:00
if ( agentPath ) {
2025-06-12 22:38:24 -05:00
// Create command file with agent content
2025-07-12 21:08:13 -05:00
let agentContent = await fileManager . readFile ( agentPath ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Replace {root} placeholder with the appropriate root path for this context
2025-08-16 19:08:39 -05:00
agentContent = agentContent . replaceAll ( '{root}' , rootPath ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
// Add command header
let commandContent = ` # / ${ agentId } Command \n \n ` ;
commandContent += ` When this command is used, adopt the following agent persona: \n \n ` ;
commandContent += agentContent ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
await fileManager . writeFile ( commandPath , commandContent ) ;
2025-07-12 21:08:13 -05:00
console . log ( chalk . green ( ` ✓ Created agent command: / ${ agentId } ` ) ) ;
2025-06-12 22:38:24 -05:00
}
}
2025-06-14 15:06:41 -05:00
2025-07-12 21:08:13 -05:00
// Setup tasks
for ( const taskId of taskIds ) {
// Find the task file - for expansion packs, prefer the expansion pack version
let taskPath ;
2025-08-16 19:08:39 -05:00
if ( packageName === 'core' ) {
// For core, use the normal search
taskPath = await this . findTaskPath ( taskId , installDir ) ;
} else {
2025-07-12 21:08:13 -05:00
// For expansion packs, first try to find the task in the expansion pack directory
2025-08-16 19:08:39 -05:00
const expansionPackPath = path . join ( installDir , rootPath , 'tasks' , ` ${ taskId } .md ` ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( expansionPackPath ) ) {
taskPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
taskPath = await this . findTaskPath ( taskId , installDir ) ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
const commandPath = path . join ( tasksDir , ` ${ taskId } .md ` ) ;
2025-06-14 15:06:41 -05:00
2025-07-12 21:08:13 -05:00
if ( taskPath ) {
// Create command file with task content
let taskContent = await fileManager . readFile ( taskPath ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Replace {root} placeholder with the appropriate root path for this context
2025-08-16 19:08:39 -05:00
taskContent = taskContent . replaceAll ( '{root}' , rootPath ) ;
2025-07-12 21:08:13 -05:00
// Add command header
let commandContent = ` # / ${ taskId } Task \n \n ` ;
commandContent += ` When this command is used, execute the following task: \n \n ` ;
commandContent += taskContent ;
await fileManager . writeFile ( commandPath , commandContent ) ;
console . log ( chalk . green ( ` ✓ Created task command: / ${ taskId } ` ) ) ;
}
}
2025-08-16 19:08:39 -05:00
console . log (
chalk . green ( ` \n ✓ Created Claude Code commands for ${ packageName } in ${ commandsBaseDir } ` ) ,
) ;
2025-07-12 21:08:13 -05:00
console . log ( chalk . dim ( ` - Agents in: ${ agentsDir } ` ) ) ;
console . log ( chalk . dim ( ` - Tasks in: ${ tasksDir } ` ) ) ;
2025-06-12 22:38:24 -05:00
}
2025-09-07 02:44:48 +08:00
async setupIFlowCli ( installDir , selectedAgent ) {
// Setup bmad-core commands
const coreSlashPrefix = await this . getCoreSlashPrefix ( installDir ) ;
const coreAgents = selectedAgent ? [ selectedAgent ] : await this . getCoreAgentIds ( installDir ) ;
const coreTasks = await this . getCoreTaskIds ( installDir ) ;
await this . setupIFlowCliForPackage (
installDir ,
'core' ,
coreSlashPrefix ,
coreAgents ,
coreTasks ,
'.bmad-core' ,
) ;
// Setup expansion pack commands
const expansionPacks = await this . getInstalledExpansionPacks ( installDir ) ;
for ( const packInfo of expansionPacks ) {
const packSlashPrefix = await this . getExpansionPackSlashPrefix ( packInfo . path ) ;
const packAgents = await this . getExpansionPackAgents ( packInfo . path ) ;
const packTasks = await this . getExpansionPackTasks ( packInfo . path ) ;
if ( packAgents . length > 0 || packTasks . length > 0 ) {
// Use the actual directory name where the expansion pack is installed
const rootPath = path . relative ( installDir , packInfo . path ) ;
await this . setupIFlowCliForPackage (
installDir ,
packInfo . name ,
packSlashPrefix ,
packAgents ,
packTasks ,
rootPath ,
) ;
}
}
return true ;
}
async setupIFlowCliForPackage ( installDir , packageName , slashPrefix , agentIds , taskIds , rootPath ) {
const commandsBaseDir = path . join ( installDir , '.iflow' , 'commands' , slashPrefix ) ;
const agentsDir = path . join ( commandsBaseDir , 'agents' ) ;
const tasksDir = path . join ( commandsBaseDir , 'tasks' ) ;
// Ensure directories exist
await fileManager . ensureDirectory ( agentsDir ) ;
await fileManager . ensureDirectory ( tasksDir ) ;
// Setup agents
for ( const agentId of agentIds ) {
// Find the agent file - for expansion packs, prefer the expansion pack version
let agentPath ;
if ( packageName === 'core' ) {
// For core, use the normal search
agentPath = await this . findAgentPath ( agentId , installDir ) ;
} else {
// For expansion packs, first try to find the agent in the expansion pack directory
const expansionPackPath = path . join ( installDir , rootPath , 'agents' , ` ${ agentId } .md ` ) ;
if ( await fileManager . pathExists ( expansionPackPath ) ) {
agentPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
agentPath = await this . findAgentPath ( agentId , installDir ) ;
}
}
const commandPath = path . join ( agentsDir , ` ${ agentId } .md ` ) ;
if ( agentPath ) {
// Create command file with agent content
let agentContent = await fileManager . readFile ( agentPath ) ;
// Replace {root} placeholder with the appropriate root path for this context
agentContent = agentContent . replaceAll ( '{root}' , rootPath ) ;
// Add command header
let commandContent = ` # / ${ agentId } Command \n \n ` ;
commandContent += ` When this command is used, adopt the following agent persona: \n \n ` ;
commandContent += agentContent ;
await fileManager . writeFile ( commandPath , commandContent ) ;
console . log ( chalk . green ( ` ✓ Created agent command: / ${ agentId } ` ) ) ;
}
}
// Setup tasks
for ( const taskId of taskIds ) {
// Find the task file - for expansion packs, prefer the expansion pack version
let taskPath ;
if ( packageName === 'core' ) {
// For core, use the normal search
taskPath = await this . findTaskPath ( taskId , installDir ) ;
} else {
// For expansion packs, first try to find the task in the expansion pack directory
const expansionPackPath = path . join ( installDir , rootPath , 'tasks' , ` ${ taskId } .md ` ) ;
if ( await fileManager . pathExists ( expansionPackPath ) ) {
taskPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
taskPath = await this . findTaskPath ( taskId , installDir ) ;
}
}
const commandPath = path . join ( tasksDir , ` ${ taskId } .md ` ) ;
if ( taskPath ) {
// Create command file with task content
let taskContent = await fileManager . readFile ( taskPath ) ;
// Replace {root} placeholder with the appropriate root path for this context
taskContent = taskContent . replaceAll ( '{root}' , rootPath ) ;
// Add command header
let commandContent = ` # / ${ taskId } Task \n \n ` ;
commandContent += ` When this command is used, execute the following task: \n \n ` ;
commandContent += taskContent ;
await fileManager . writeFile ( commandPath , commandContent ) ;
console . log ( chalk . green ( ` ✓ Created task command: / ${ taskId } ` ) ) ;
}
}
console . log (
chalk . green ( ` \n ✓ Created iFlow CLI commands for ${ packageName } in ${ commandsBaseDir } ` ) ,
) ;
console . log ( chalk . dim ( ` - Agents in: ${ agentsDir } ` ) ) ;
console . log ( chalk . dim ( ` - Tasks in: ${ tasksDir } ` ) ) ;
}
2025-08-16 00:38:44 -03:00
async setupCrushForPackage ( installDir , packageName , slashPrefix , agentIds , taskIds , rootPath ) {
2025-08-16 19:08:39 -05:00
const commandsBaseDir = path . join ( installDir , '.crush' , 'commands' , slashPrefix ) ;
const agentsDir = path . join ( commandsBaseDir , 'agents' ) ;
const tasksDir = path . join ( commandsBaseDir , 'tasks' ) ;
2025-08-16 00:38:44 -03:00
// Ensure directories exist
await fileManager . ensureDirectory ( agentsDir ) ;
await fileManager . ensureDirectory ( tasksDir ) ;
// Setup agents
for ( const agentId of agentIds ) {
// Find the agent file - for expansion packs, prefer the expansion pack version
let agentPath ;
2025-08-16 19:08:39 -05:00
if ( packageName === 'core' ) {
// For core, use the normal search
agentPath = await this . findAgentPath ( agentId , installDir ) ;
} else {
2025-08-16 00:38:44 -03:00
// For expansion packs, first try to find the agent in the expansion pack directory
2025-08-16 19:08:39 -05:00
const expansionPackPath = path . join ( installDir , rootPath , 'agents' , ` ${ agentId } .md ` ) ;
2025-08-16 00:38:44 -03:00
if ( await fileManager . pathExists ( expansionPackPath ) ) {
agentPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
agentPath = await this . findAgentPath ( agentId , installDir ) ;
}
}
const commandPath = path . join ( agentsDir , ` ${ agentId } .md ` ) ;
if ( agentPath ) {
// Create command file with agent content
let agentContent = await fileManager . readFile ( agentPath ) ;
// Replace {root} placeholder with the appropriate root path for this context
2025-08-16 19:08:39 -05:00
agentContent = agentContent . replaceAll ( '{root}' , rootPath ) ;
2025-08-16 00:38:44 -03:00
// Add command header
let commandContent = ` # / ${ agentId } Command \n \n ` ;
commandContent += ` When this command is used, adopt the following agent persona: \n \n ` ;
commandContent += agentContent ;
await fileManager . writeFile ( commandPath , commandContent ) ;
console . log ( chalk . green ( ` ✓ Created agent command: / ${ agentId } ` ) ) ;
}
}
// Setup tasks
for ( const taskId of taskIds ) {
// Find the task file - for expansion packs, prefer the expansion pack version
let taskPath ;
2025-08-16 19:08:39 -05:00
if ( packageName === 'core' ) {
// For core, use the normal search
taskPath = await this . findTaskPath ( taskId , installDir ) ;
} else {
2025-08-16 00:38:44 -03:00
// For expansion packs, first try to find the task in the expansion pack directory
2025-08-16 19:08:39 -05:00
const expansionPackPath = path . join ( installDir , rootPath , 'tasks' , ` ${ taskId } .md ` ) ;
2025-08-16 00:38:44 -03:00
if ( await fileManager . pathExists ( expansionPackPath ) ) {
taskPath = expansionPackPath ;
} else {
// Fall back to core if not found in expansion pack
taskPath = await this . findTaskPath ( taskId , installDir ) ;
}
}
const commandPath = path . join ( tasksDir , ` ${ taskId } .md ` ) ;
if ( taskPath ) {
// Create command file with task content
let taskContent = await fileManager . readFile ( taskPath ) ;
// Replace {root} placeholder with the appropriate root path for this context
2025-08-16 19:08:39 -05:00
taskContent = taskContent . replaceAll ( '{root}' , rootPath ) ;
2025-08-16 00:38:44 -03:00
// Add command header
let commandContent = ` # / ${ taskId } Task \n \n ` ;
commandContent += ` When this command is used, execute the following task: \n \n ` ;
commandContent += taskContent ;
await fileManager . writeFile ( commandPath , commandContent ) ;
console . log ( chalk . green ( ` ✓ Created task command: / ${ taskId } ` ) ) ;
}
}
console . log ( chalk . green ( ` \n ✓ Created Crush commands for ${ packageName } in ${ commandsBaseDir } ` ) ) ;
console . log ( chalk . dim ( ` - Agents in: ${ agentsDir } ` ) ) ;
console . log ( chalk . dim ( ` - Tasks in: ${ tasksDir } ` ) ) ;
}
2025-06-12 22:38:24 -05:00
async setupWindsurf ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const windsurfWorkflowDir = path . join ( installDir , '.windsurf' , 'workflows' ) ;
2025-06-19 12:55:16 -05:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-06-14 15:06:41 -05:00
2025-08-16 19:08:39 -05:00
await fileManager . ensureDirectory ( windsurfWorkflowDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
for ( const agentId of agents ) {
2025-06-28 02:22:57 -05:00
// Find the agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-28 02:22:57 -05:00
if ( agentPath ) {
2025-06-12 22:38:24 -05:00
const agentContent = await fileManager . readFile ( agentPath ) ;
2025-08-16 19:08:39 -05:00
const mdPath = path . join ( windsurfWorkflowDir , ` ${ agentId } .md ` ) ;
2025-06-14 15:06:41 -05:00
2025-08-16 19:08:39 -05:00
// Write the agent file contents prefixed with Windsurf frontmatter
let mdContent = ` --- \n ` ;
mdContent += ` description: ${ agentId } \n ` ;
mdContent += ` auto_execution_mode: 3 \n ` ;
mdContent += ` --- \n \n ` ;
mdContent += agentContent ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
await fileManager . writeFile ( mdPath , mdContent ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( ` ✓ Created workflow: ${ agentId } .md ` ) ) ;
2025-06-12 22:38:24 -05:00
}
}
2025-06-14 15:06:41 -05:00
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( ` \n ✓ Created Windsurf workflows in ${ windsurfWorkflowDir } ` ) ) ;
2025-06-14 15:06:41 -05:00
2025-06-12 22:38:24 -05:00
return true ;
}
2025-07-05 21:08:26 -05:00
async setupTrae ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const traeRulesDir = path . join ( installDir , '.trae' , 'rules' ) ;
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-05 21:08:26 -05:00
await fileManager . ensureDirectory ( traeRulesDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-05 21:08:26 -05:00
for ( const agentId of agents ) {
// Find the agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-05 21:08:26 -05:00
if ( agentPath ) {
const agentContent = await fileManager . readFile ( agentPath ) ;
const mdPath = path . join ( traeRulesDir , ` ${ agentId } .md ` ) ;
2025-08-16 00:38:44 -03:00
2025-07-05 21:08:26 -05:00
// Create MD content (similar to Cursor but without frontmatter)
let mdContent = ` # ${ agentId . toUpperCase ( ) } Agent Rule \n \n ` ;
mdContent += ` This rule is triggered when the user types \` @ ${ agentId } \` and activates the ${ await this . getAgentTitle (
agentId ,
2025-08-16 19:08:39 -05:00
installDir ,
2025-07-05 21:08:26 -05:00
) } agent persona . \ n \ n ` ;
2025-08-16 19:08:39 -05:00
mdContent += '## Agent Activation\n\n' ;
2025-07-05 21:08:26 -05:00
mdContent +=
2025-08-16 19:08:39 -05:00
'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n' ;
mdContent += '```yaml\n' ;
2025-07-05 21:08:26 -05:00
// Extract just the YAML content from the agent file
const yamlContent = extractYamlFromAgent ( agentContent ) ;
if ( yamlContent ) {
mdContent += yamlContent ;
2025-08-16 19:08:39 -05:00
} else {
2025-07-05 21:08:26 -05:00
// If no YAML found, include the whole content minus the header
2025-08-16 19:08:39 -05:00
mdContent += agentContent . replace ( /^#.*$/m , '' ) . trim ( ) ;
2025-07-05 21:08:26 -05:00
}
2025-08-16 19:08:39 -05:00
mdContent += '\n```\n\n' ;
mdContent += '## File Reference\n\n' ;
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-07-05 21:08:26 -05:00
mdContent += ` The complete agent definition is available in [ ${ relativePath } ]( ${ relativePath } ). \n \n ` ;
2025-08-16 19:08:39 -05:00
mdContent += '## Usage\n\n' ;
2025-07-05 21:08:26 -05:00
mdContent += ` When the user types \` @ ${ agentId } \` , activate this ${ await this . getAgentTitle (
agentId ,
2025-08-16 19:08:39 -05:00
installDir ,
2025-07-05 21:08:26 -05:00
) } persona and follow all instructions defined in the YAML configuration above . \ n ` ;
2025-08-16 00:38:44 -03:00
2025-07-05 21:08:26 -05:00
await fileManager . writeFile ( mdPath , mdContent ) ;
console . log ( chalk . green ( ` ✓ Created rule: ${ agentId } .md ` ) ) ;
}
}
}
2025-06-28 02:22:57 -05:00
async findAgentPath ( agentId , installDir ) {
// Try to find the agent file in various locations
const possiblePaths = [
2025-08-16 19:08:39 -05:00
path . join ( installDir , '.bmad-core' , 'agents' , ` ${ agentId } .md ` ) ,
path . join ( installDir , 'agents' , ` ${ agentId } .md ` ) ,
2025-06-28 02:22:57 -05:00
] ;
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
// Also check expansion pack directories
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const expansionDirectories = glob . sync ( '.*/agents' , { cwd : installDir } ) ;
for ( const expDir of expansionDirectories ) {
2025-06-28 02:22:57 -05:00
possiblePaths . push ( path . join ( installDir , expDir , ` ${ agentId } .md ` ) ) ;
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
for ( const agentPath of possiblePaths ) {
if ( await fileManager . pathExists ( agentPath ) ) {
return agentPath ;
}
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
return null ;
}
2025-06-12 22:38:24 -05:00
async getAllAgentIds ( installDir ) {
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
2025-06-28 02:22:57 -05:00
const allAgentIds = [ ] ;
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
// Check core agents in .bmad-core or root
2025-08-16 19:08:39 -05:00
let agentsDir = path . join ( installDir , '.bmad-core' , 'agents' ) ;
2025-06-14 15:06:41 -05:00
if ( ! ( await fileManager . pathExists ( agentsDir ) ) ) {
2025-08-16 19:08:39 -05:00
agentsDir = path . join ( installDir , 'agents' ) ;
2025-06-12 22:38:24 -05:00
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
if ( await fileManager . pathExists ( agentsDir ) ) {
2025-08-16 19:08:39 -05:00
const agentFiles = glob . sync ( '*.md' , { cwd : agentsDir } ) ;
allAgentIds . push ( ... agentFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-06-28 02:22:57 -05:00
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
// Also check for expansion pack agents in dot folders
2025-08-16 19:08:39 -05:00
const expansionDirectories = glob . sync ( '.*/agents' , { cwd : installDir } ) ;
for ( const expDir of expansionDirectories ) {
2025-06-28 02:22:57 -05:00
const fullExpDir = path . join ( installDir , expDir ) ;
2025-08-16 19:08:39 -05:00
const expAgentFiles = glob . sync ( '*.md' , { cwd : fullExpDir } ) ;
allAgentIds . push ( ... expAgentFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-06-28 02:22:57 -05:00
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
// Remove duplicates
return [ ... new Set ( allAgentIds ) ] ;
2025-06-12 22:38:24 -05:00
}
2025-07-12 21:08:13 -05:00
async getCoreAgentIds ( installDir ) {
const allAgentIds = [ ] ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check core agents in .bmad-core or root only
2025-08-16 19:08:39 -05:00
let agentsDir = path . join ( installDir , '.bmad-core' , 'agents' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( agentsDir ) ) ) {
2025-08-16 19:08:39 -05:00
agentsDir = path . join ( installDir , 'bmad-core' , 'agents' ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( agentsDir ) ) {
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const agentFiles = glob . sync ( '*.md' , { cwd : agentsDir } ) ;
allAgentIds . push ( ... agentFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
return [ ... new Set ( allAgentIds ) ] ;
}
async getCoreTaskIds ( installDir ) {
const allTaskIds = [ ] ;
2025-09-06 19:16:10 +01:00
const glob = require ( 'glob' ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check core tasks in .bmad-core or root only
2025-08-16 19:08:39 -05:00
let tasksDir = path . join ( installDir , '.bmad-core' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( tasksDir ) ) ) {
2025-08-16 19:08:39 -05:00
tasksDir = path . join ( installDir , 'bmad-core' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( tasksDir ) ) {
2025-08-16 19:08:39 -05:00
const taskFiles = glob . sync ( '*.md' , { cwd : tasksDir } ) ;
allTaskIds . push ( ... taskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check common tasks
2025-08-16 19:08:39 -05:00
const commonTasksDir = path . join ( installDir , 'common' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( commonTasksDir ) ) {
2025-09-07 02:38:29 +08:00
const glob = require ( 'glob' ) ;
2025-08-16 19:08:39 -05:00
const commonTaskFiles = glob . sync ( '*.md' , { cwd : commonTasksDir } ) ;
allTaskIds . push ( ... commonTaskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
return [ ... new Set ( allTaskIds ) ] ;
}
2025-06-28 01:01:26 -05:00
async getAgentTitle ( agentId , installDir ) {
2025-06-28 02:22:57 -05:00
// Try to find the agent file in various locations
const possiblePaths = [
2025-08-16 19:08:39 -05:00
path . join ( installDir , '.bmad-core' , 'agents' , ` ${ agentId } .md ` ) ,
path . join ( installDir , 'agents' , ` ${ agentId } .md ` ) ,
2025-06-28 02:22:57 -05:00
] ;
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
// Also check expansion pack directories
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const expansionDirectories = glob . sync ( '.*/agents' , { cwd : installDir } ) ;
for ( const expDir of expansionDirectories ) {
2025-06-28 02:22:57 -05:00
possiblePaths . push ( path . join ( installDir , expDir , ` ${ agentId } .md ` ) ) ;
2025-06-28 01:01:26 -05:00
}
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
for ( const agentPath of possiblePaths ) {
if ( await fileManager . pathExists ( agentPath ) ) {
try {
const agentContent = await fileManager . readFile ( agentPath ) ;
2025-07-13 22:48:19 -05:00
const yamlMatch = agentContent . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
2025-08-16 00:38:44 -03:00
2025-06-28 02:22:57 -05:00
if ( yamlMatch ) {
const yaml = yamlMatch [ 1 ] ;
const titleMatch = yaml . match ( /title:\s*(.+)/ ) ;
if ( titleMatch ) {
return titleMatch [ 1 ] . trim ( ) ;
}
2025-06-28 01:01:26 -05:00
}
2025-06-28 02:22:57 -05:00
} catch ( error ) {
console . warn ( ` Failed to read agent title for ${ agentId } : ${ error . message } ` ) ;
2025-06-28 01:01:26 -05:00
}
}
}
2025-08-16 00:38:44 -03:00
2025-06-28 01:01:26 -05:00
// Fallback to formatted agent ID
2025-08-16 19:08:39 -05:00
return agentId
. split ( '-' )
. map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( ' ' ) ;
2025-06-12 22:38:24 -05:00
}
2025-06-14 15:06:41 -05:00
2025-07-12 21:08:13 -05:00
async getAllTaskIds ( installDir ) {
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
2025-07-12 21:08:13 -05:00
const allTaskIds = [ ] ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check core tasks in .bmad-core or root
2025-08-16 19:08:39 -05:00
let tasksDir = path . join ( installDir , '.bmad-core' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( tasksDir ) ) ) {
2025-08-16 19:08:39 -05:00
tasksDir = path . join ( installDir , 'bmad-core' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( tasksDir ) ) {
2025-08-16 19:08:39 -05:00
const taskFiles = glob . sync ( '*.md' , { cwd : tasksDir } ) ;
allTaskIds . push ( ... taskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check common tasks
2025-08-16 19:08:39 -05:00
const commonTasksDir = path . join ( installDir , 'common' , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( commonTasksDir ) ) {
2025-08-16 19:08:39 -05:00
const commonTaskFiles = glob . sync ( '*.md' , { cwd : commonTasksDir } ) ;
allTaskIds . push ( ... commonTaskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Also check for expansion pack tasks in dot folders
2025-08-16 19:08:39 -05:00
const expansionDirectories = glob . sync ( '.*/tasks' , { cwd : installDir } ) ;
for ( const expDir of expansionDirectories ) {
2025-07-12 21:08:13 -05:00
const fullExpDir = path . join ( installDir , expDir ) ;
2025-08-16 19:08:39 -05:00
const expTaskFiles = glob . sync ( '*.md' , { cwd : fullExpDir } ) ;
allTaskIds . push ( ... expTaskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check expansion-packs folder tasks
2025-08-16 19:08:39 -05:00
const expansionPacksDir = path . join ( installDir , 'expansion-packs' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( expansionPacksDir ) ) {
2025-08-16 19:08:39 -05:00
const expPackDirectories = glob . sync ( '*/tasks' , { cwd : expansionPacksDir } ) ;
for ( const expDir of expPackDirectories ) {
2025-07-12 21:08:13 -05:00
const fullExpDir = path . join ( expansionPacksDir , expDir ) ;
2025-08-16 19:08:39 -05:00
const expTaskFiles = glob . sync ( '*.md' , { cwd : fullExpDir } ) ;
allTaskIds . push ( ... expTaskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ) ;
2025-07-12 21:08:13 -05:00
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Remove duplicates
return [ ... new Set ( allTaskIds ) ] ;
}
async findTaskPath ( taskId , installDir ) {
// Try to find the task file in various locations
const possiblePaths = [
2025-08-16 19:08:39 -05:00
path . join ( installDir , '.bmad-core' , 'tasks' , ` ${ taskId } .md ` ) ,
path . join ( installDir , 'bmad-core' , 'tasks' , ` ${ taskId } .md ` ) ,
path . join ( installDir , 'common' , 'tasks' , ` ${ taskId } .md ` ) ,
2025-07-12 21:08:13 -05:00
] ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Also check expansion pack directories
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check dot folder expansion packs
2025-08-16 19:08:39 -05:00
const expansionDirectories = glob . sync ( '.*/tasks' , { cwd : installDir } ) ;
for ( const expDir of expansionDirectories ) {
2025-07-12 21:08:13 -05:00
possiblePaths . push ( path . join ( installDir , expDir , ` ${ taskId } .md ` ) ) ;
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check expansion-packs folder
2025-08-16 19:08:39 -05:00
const expansionPacksDir = path . join ( installDir , 'expansion-packs' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( expansionPacksDir ) ) {
2025-08-16 19:08:39 -05:00
const expPackDirectories = glob . sync ( '*/tasks' , { cwd : expansionPacksDir } ) ;
for ( const expDir of expPackDirectories ) {
2025-07-12 21:08:13 -05:00
possiblePaths . push ( path . join ( expansionPacksDir , expDir , ` ${ taskId } .md ` ) ) ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
for ( const taskPath of possiblePaths ) {
if ( await fileManager . pathExists ( taskPath ) ) {
return taskPath ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
return null ;
}
async getCoreSlashPrefix ( installDir ) {
try {
2025-08-16 19:08:39 -05:00
const coreConfigPath = path . join ( installDir , '.bmad-core' , 'core-config.yaml' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( coreConfigPath ) ) ) {
// Try bmad-core directory
2025-08-16 19:08:39 -05:00
const altConfigPath = path . join ( installDir , 'bmad-core' , 'core-config.yaml' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( altConfigPath ) ) {
const configContent = await fileManager . readFile ( altConfigPath ) ;
const config = yaml . load ( configContent ) ;
2025-08-16 19:08:39 -05:00
return config . slashPrefix || 'BMad' ;
2025-07-12 21:08:13 -05:00
}
2025-08-16 19:08:39 -05:00
return 'BMad' ; // fallback
2025-07-12 21:08:13 -05:00
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
const configContent = await fileManager . readFile ( coreConfigPath ) ;
const config = yaml . load ( configContent ) ;
2025-08-16 19:08:39 -05:00
return config . slashPrefix || 'BMad' ;
2025-07-12 21:08:13 -05:00
} catch ( error ) {
console . warn ( ` Failed to read core slashPrefix, using default 'BMad': ${ error . message } ` ) ;
2025-08-16 19:08:39 -05:00
return 'BMad' ;
2025-07-12 21:08:13 -05:00
}
}
async getInstalledExpansionPacks ( installDir ) {
const expansionPacks = [ ] ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check for dot-prefixed expansion packs in install directory
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const dotExpansions = glob . sync ( '.bmad-*' , { cwd : installDir } ) ;
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
for ( const dotExpansion of dotExpansions ) {
2025-08-16 19:08:39 -05:00
if ( dotExpansion !== '.bmad-core' ) {
2025-07-12 21:08:13 -05:00
const packPath = path . join ( installDir , dotExpansion ) ;
2025-08-16 19:08:39 -05:00
const packName = dotExpansion . slice ( 1 ) ; // remove the dot
2025-07-12 21:08:13 -05:00
expansionPacks . push ( {
name : packName ,
2025-08-16 19:08:39 -05:00
path : packPath ,
2025-07-12 21:08:13 -05:00
} ) ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
// Check for expansion-packs directory style
2025-08-16 19:08:39 -05:00
const expansionPacksDir = path . join ( installDir , 'expansion-packs' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( expansionPacksDir ) ) {
2025-08-16 19:08:39 -05:00
const packDirectories = glob . sync ( '*' , { cwd : expansionPacksDir } ) ;
2025-08-16 00:38:44 -03:00
2025-08-16 19:08:39 -05:00
for ( const packDir of packDirectories ) {
2025-07-12 21:08:13 -05:00
const packPath = path . join ( expansionPacksDir , packDir ) ;
2025-08-16 19:08:39 -05:00
if (
( await fileManager . pathExists ( packPath ) ) &&
( await fileManager . pathExists ( path . join ( packPath , 'config.yaml' ) ) )
) {
2025-07-12 21:08:13 -05:00
expansionPacks . push ( {
name : packDir ,
2025-08-16 19:08:39 -05:00
path : packPath ,
2025-07-12 21:08:13 -05:00
} ) ;
}
}
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
return expansionPacks ;
}
async getExpansionPackSlashPrefix ( packPath ) {
try {
2025-08-16 19:08:39 -05:00
const configPath = path . join ( packPath , 'config.yaml' ) ;
2025-07-12 21:08:13 -05:00
if ( await fileManager . pathExists ( configPath ) ) {
const configContent = await fileManager . readFile ( configPath ) ;
const config = yaml . load ( configContent ) ;
return config . slashPrefix || path . basename ( packPath ) ;
}
} catch ( error ) {
console . warn ( ` Failed to read expansion pack slashPrefix from ${ packPath } : ${ error . message } ` ) ;
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
return path . basename ( packPath ) ; // fallback to directory name
}
async getExpansionPackAgents ( packPath ) {
2025-08-16 19:08:39 -05:00
const agentsDir = path . join ( packPath , 'agents' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( agentsDir ) ) ) {
return [ ] ;
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
try {
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const agentFiles = glob . sync ( '*.md' , { cwd : agentsDir } ) ;
return agentFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ;
2025-07-12 21:08:13 -05:00
} catch ( error ) {
console . warn ( ` Failed to read expansion pack agents from ${ packPath } : ${ error . message } ` ) ;
return [ ] ;
}
}
async getExpansionPackTasks ( packPath ) {
2025-08-16 19:08:39 -05:00
const tasksDir = path . join ( packPath , 'tasks' ) ;
2025-07-12 21:08:13 -05:00
if ( ! ( await fileManager . pathExists ( tasksDir ) ) ) {
return [ ] ;
}
2025-08-16 00:38:44 -03:00
2025-07-12 21:08:13 -05:00
try {
2025-08-16 19:08:39 -05:00
const glob = require ( 'glob' ) ;
const taskFiles = glob . sync ( '*.md' , { cwd : tasksDir } ) ;
return taskFiles . map ( ( file ) => path . basename ( file , '.md' ) ) ;
2025-07-12 21:08:13 -05:00
} catch ( error ) {
console . warn ( ` Failed to read expansion pack tasks from ${ packPath } : ${ error . message } ` ) ;
return [ ] ;
}
}
2025-06-14 15:06:41 -05:00
async setupRoo ( installDir , selectedAgent ) {
2025-06-19 12:55:16 -05:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-17 17:51:52 +02:00
// Check for existing .roomodes file in project root
2025-08-16 19:08:39 -05:00
const roomodesPath = path . join ( installDir , '.roomodes' ) ;
2025-06-14 15:06:41 -05:00
let existingModes = [ ] ;
2025-08-16 19:08:39 -05:00
let existingContent = '' ;
2025-06-14 15:06:41 -05:00
if ( await fileManager . pathExists ( roomodesPath ) ) {
existingContent = await fileManager . readFile ( roomodesPath ) ;
// Parse existing modes to avoid duplicates
const modeMatches = existingContent . matchAll ( /- slug: ([\w-]+)/g ) ;
for ( const match of modeMatches ) {
existingModes . push ( match [ 1 ] ) ;
}
2025-06-19 12:55:16 -05:00
console . log ( chalk . yellow ( ` Found existing .roomodes file with ${ existingModes . length } modes ` ) ) ;
2025-06-14 15:06:41 -05:00
}
// Create new modes content
2025-08-16 19:08:39 -05:00
let newModesContent = '' ;
2025-06-14 15:06:41 -05:00
2025-06-28 02:22:57 -05:00
// Load dynamic agent permissions from configuration
const config = await this . loadIdeAgentConfig ( ) ;
const agentPermissions = config [ 'roo-permissions' ] || { } ;
2025-06-14 16:38:37 -05:00
2025-06-14 15:06:41 -05:00
for ( const agentId of agents ) {
// Skip if already exists
2025-07-16 23:36:24 -05:00
// Check both with and without bmad- prefix to handle both cases
const checkSlug = agentId . startsWith ( 'bmad-' ) ? agentId : ` bmad- ${ agentId } ` ;
if ( existingModes . includes ( checkSlug ) ) {
2025-06-19 12:55:16 -05:00
console . log ( chalk . dim ( ` Skipping ${ agentId } - already exists in .roomodes ` ) ) ;
2025-06-14 15:06:41 -05:00
continue ;
}
// Read agent file to extract all information
2025-06-28 02:22:57 -05:00
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
2025-06-14 15:06:41 -05:00
2025-06-28 02:22:57 -05:00
if ( agentPath ) {
2025-06-14 15:06:41 -05:00
const agentContent = await fileManager . readFile ( agentPath ) ;
// Extract YAML content
2025-07-14 06:44:40 +03:00
const yamlMatch = agentContent . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
2025-06-14 15:06:41 -05:00
if ( yamlMatch ) {
const yaml = yamlMatch [ 1 ] ;
// Extract agent info from YAML
const titleMatch = yaml . match ( /title:\s*(.+)/ ) ;
const iconMatch = yaml . match ( /icon:\s*(.+)/ ) ;
const whenToUseMatch = yaml . match ( /whenToUse:\s*"(.+)"/ ) ;
2025-06-14 16:38:37 -05:00
const roleDefinitionMatch = yaml . match ( /roleDefinition:\s*"(.+)"/ ) ;
2025-06-14 15:06:41 -05:00
2025-08-16 19:08:39 -05:00
const title = titleMatch
? titleMatch [ 1 ] . trim ( )
: await this . getAgentTitle ( agentId , installDir ) ;
const icon = iconMatch ? iconMatch [ 1 ] . trim ( ) : '🤖' ;
2025-06-19 12:55:16 -05:00
const whenToUse = whenToUseMatch ? whenToUseMatch [ 1 ] . trim ( ) : ` Use for ${ title } tasks ` ;
2025-06-14 16:38:37 -05:00
const roleDefinition = roleDefinitionMatch
? roleDefinitionMatch [ 1 ] . trim ( )
: ` You are a ${ title } specializing in ${ title . toLowerCase ( ) } tasks and responsibilities. ` ;
2025-08-03 15:49:39 +01:00
// Add permissions based on agent type
const permissions = agentPermissions [ agentId ] ;
2025-06-14 18:11:16 -05:00
// Build mode entry with proper formatting (matching exact indentation)
2025-07-16 23:36:24 -05:00
// Avoid double "bmad-" prefix for agents that already have it
const slug = agentId . startsWith ( 'bmad-' ) ? agentId : ` bmad- ${ agentId } ` ;
newModesContent += ` - slug: ${ slug } \n ` ;
2025-06-14 16:38:37 -05:00
newModesContent += ` name: ' ${ icon } ${ title } ' \n ` ;
2025-08-03 15:49:39 +01:00
if ( permissions ) {
2025-08-16 19:08:39 -05:00
newModesContent += ` description: ' ${ permissions . description } ' \n ` ;
2025-08-03 15:49:39 +01:00
}
2025-06-14 16:38:37 -05:00
newModesContent += ` roleDefinition: ${ roleDefinition } \n ` ;
newModesContent += ` whenToUse: ${ whenToUse } \n ` ;
2025-06-28 02:22:57 -05:00
// Get relative path from installDir to agent file
2025-08-16 19:08:39 -05:00
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-07-04 11:53:57 -05:00
newModesContent += ` customInstructions: CRITICAL 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 ` ;
2025-06-14 16:38:37 -05:00
newModesContent += ` groups: \n ` ;
newModesContent += ` - read \n ` ;
2025-06-17 17:51:52 +02:00
2025-06-14 16:38:37 -05:00
if ( permissions ) {
newModesContent += ` - - edit \n ` ;
newModesContent += ` - fileRegex: ${ permissions . fileRegex } \n ` ;
newModesContent += ` description: ${ permissions . description } \n ` ;
} else {
newModesContent += ` - edit \n ` ;
}
2025-06-14 15:06:41 -05:00
2025-06-19 12:55:16 -05:00
console . log ( chalk . green ( ` ✓ Added mode: bmad- ${ agentId } ( ${ icon } ${ title } ) ` ) ) ;
2025-06-14 15:06:41 -05:00
}
}
}
// Build final roomodes content
2025-08-16 19:08:39 -05:00
let roomodesContent = '' ;
2025-06-14 15:06:41 -05:00
if ( existingContent ) {
// If there's existing content, append new modes to it
2025-08-16 19:08:39 -05:00
roomodesContent = existingContent . trim ( ) + '\n' + newModesContent ;
2025-06-14 15:06:41 -05:00
} else {
// Create new .roomodes file with proper YAML structure
2025-08-16 19:08:39 -05:00
roomodesContent = 'customModes:\n' + newModesContent ;
2025-06-14 15:06:41 -05:00
}
// Write .roomodes file
await fileManager . writeFile ( roomodesPath , roomodesContent ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ Created .roomodes file in project root' ) ) ;
2025-06-14 15:06:41 -05:00
console . log ( chalk . green ( ` \n ✓ Roo Code setup complete! ` ) ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . dim ( 'Custom modes will be available when you open this project in Roo Code' ) ) ;
2025-06-14 15:06:41 -05:00
return true ;
}
2025-08-16 00:38:44 -03:00
2025-08-03 15:49:39 +01:00
async setupKilocode ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const filePath = path . join ( installDir , '.kilocodemodes' ) ;
2025-08-03 15:49:39 +01:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-08-16 19:08:39 -05:00
let existingModes = [ ] ,
existingContent = '' ;
2025-08-03 15:49:39 +01:00
if ( await fileManager . pathExists ( filePath ) ) {
existingContent = await fileManager . readFile ( filePath ) ;
for ( const match of existingContent . matchAll ( /- slug: ([\w-]+)/g ) ) {
existingModes . push ( match [ 1 ] ) ;
}
2025-08-16 19:08:39 -05:00
console . log (
chalk . yellow ( ` Found existing .kilocodemodes file with ${ existingModes . length } modes ` ) ,
) ;
2025-08-03 15:49:39 +01:00
}
const config = await this . loadIdeAgentConfig ( ) ;
const permissions = config [ 'roo-permissions' ] || { } ; // reuse same roo permissions block (Kilo Code understands same mode schema)
2025-06-24 04:47:21 +02:00
2025-08-16 19:08:39 -05:00
let newContent = '' ;
2025-08-03 15:49:39 +01:00
for ( const agentId of agents ) {
const slug = agentId . startsWith ( 'bmad-' ) ? agentId : ` bmad- ${ agentId } ` ;
if ( existingModes . includes ( slug ) ) {
console . log ( chalk . dim ( ` Skipping ${ agentId } - already exists in .kilocodemodes ` ) ) ;
continue ;
}
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( ! agentPath ) {
console . log ( chalk . red ( ` ✗ Could not find agent file for ${ agentId } ` ) ) ;
continue ;
}
const agentContent = await fileManager . readFile ( agentPath ) ;
const yamlMatch = agentContent . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
if ( ! yamlMatch ) {
console . log ( chalk . red ( ` ✗ Could not extract YAML block for ${ agentId } ` ) ) ;
continue ;
}
const yaml = yamlMatch [ 1 ] ;
// Robust fallback for title and icon
2025-08-16 19:08:39 -05:00
const title =
yaml . match ( /title:\s*(.+)/ ) ? . [ 1 ] ? . trim ( ) || ( await this . getAgentTitle ( agentId , installDir ) ) ;
const icon = yaml . match ( /icon:\s*(.+)/ ) ? . [ 1 ] ? . trim ( ) || '🤖' ;
const whenToUse = yaml . match ( /whenToUse:\s*"(.+)"/ ) ? . [ 1 ] ? . trim ( ) || ` Use for ${ title } tasks ` ;
const roleDefinition =
yaml . match ( /roleDefinition:\s*"(.+)"/ ) ? . [ 1 ] ? . trim ( ) ||
2025-08-03 15:49:39 +01:00
` You are a ${ title } specializing in ${ title . toLowerCase ( ) } tasks and responsibilities. ` ;
2025-08-16 19:08:39 -05:00
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-08-03 15:49:39 +01:00
const customInstructions = ` CRITICAL 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 ` ;
// Add permissions from config if they exist
const agentPermission = permissions [ agentId ] ;
// Begin .kilocodemodes block
newContent += ` - slug: ${ slug } \n ` ;
newContent += ` name: ' ${ icon } ${ title } ' \n ` ;
if ( agentPermission ) {
2025-08-16 19:08:39 -05:00
newContent += ` description: ' ${ agentPermission . description } ' \n ` ;
2025-08-03 15:49:39 +01:00
}
newContent += ` roleDefinition: ${ roleDefinition } \n ` ;
newContent += ` whenToUse: ${ whenToUse } \n ` ;
newContent += ` customInstructions: ${ customInstructions } \n ` ;
newContent += ` groups: \n ` ;
newContent += ` - read \n ` ;
if ( agentPermission ) {
newContent += ` - - edit \n ` ;
newContent += ` - fileRegex: ${ agentPermission . fileRegex } \n ` ;
newContent += ` description: ${ agentPermission . description } \n ` ;
} else {
// Fallback to generic edit
newContent += ` - edit \n ` ;
}
console . log ( chalk . green ( ` ✓ Added Kilo mode: ${ slug } ( ${ icon } ${ title } ) ` ) ) ;
}
const finalContent = existingContent
2025-08-16 19:08:39 -05:00
? existingContent . trim ( ) + '\n' + newContent
: 'customModes:\n' + newContent ;
2025-08-03 15:49:39 +01:00
await fileManager . writeFile ( filePath , finalContent ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ Created .kilocodemodes file in project root' ) ) ;
2025-08-03 15:49:39 +01:00
console . log ( chalk . green ( ` ✓ KiloCode setup complete! ` ) ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . dim ( 'Custom modes will be available when you open this project in KiloCode' ) ) ;
2025-08-03 15:49:39 +01:00
return true ;
}
2025-08-16 00:38:44 -03:00
2025-06-24 04:47:21 +02:00
async setupCline ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const clineRulesDir = path . join ( installDir , '.clinerules' ) ;
2025-06-24 04:47:21 +02:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
await fileManager . ensureDirectory ( clineRulesDir ) ;
2025-06-28 02:22:57 -05:00
// Load dynamic agent ordering from configuration
const config = await this . loadIdeAgentConfig ( ) ;
const agentOrder = config [ 'cline-order' ] || { } ;
2025-06-24 04:47:21 +02:00
for ( const agentId of agents ) {
2025-06-28 02:22:57 -05:00
// Find the agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
2025-06-24 04:47:21 +02:00
2025-06-28 02:22:57 -05:00
if ( agentPath ) {
2025-06-24 04:47:21 +02:00
const agentContent = await fileManager . readFile ( agentPath ) ;
// Get numeric prefix for ordering
const order = agentOrder [ agentId ] || 99 ;
const prefix = order . toString ( ) . padStart ( 2 , '0' ) ;
const mdPath = path . join ( clineRulesDir , ` ${ prefix } - ${ agentId } .md ` ) ;
// Create MD content for Cline (focused on project standards and role)
2025-06-28 01:01:26 -05:00
let mdContent = ` # ${ await this . getAgentTitle ( agentId , installDir ) } Agent \n \n ` ;
mdContent += ` This rule defines the ${ await this . getAgentTitle ( agentId , installDir ) } persona and project standards. \n \n ` ;
2025-08-16 19:08:39 -05:00
mdContent += '## Role Definition\n\n' ;
2025-06-24 04:47:21 +02:00
mdContent +=
2025-08-16 19:08:39 -05:00
'When the user types `@' +
agentId +
'`, adopt this persona and follow these guidelines:\n\n' ;
mdContent += '```yaml\n' ;
2025-06-24 04:47:21 +02:00
// Extract just the YAML content from the agent file
2025-07-02 21:08:43 -05:00
const yamlContent = extractYamlFromAgent ( agentContent ) ;
if ( yamlContent ) {
mdContent += yamlContent ;
2025-06-24 04:47:21 +02:00
} else {
// If no YAML found, include the whole content minus the header
2025-08-16 19:08:39 -05:00
mdContent += agentContent . replace ( /^#.*$/m , '' ) . trim ( ) ;
2025-06-24 04:47:21 +02:00
}
2025-08-16 19:08:39 -05:00
mdContent += '\n```\n\n' ;
mdContent += '## Project Standards\n\n' ;
2025-06-24 04:47:21 +02:00
mdContent += ` - Always maintain consistency with project documentation in .bmad-core/ \n ` ;
mdContent += ` - Follow the agent's specific guidelines and constraints \n ` ;
mdContent += ` - Update relevant project files when making changes \n ` ;
2025-08-16 19:08:39 -05:00
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-06-28 02:22:57 -05:00
mdContent += ` - Reference the complete agent definition in [ ${ relativePath } ]( ${ relativePath } ) \n \n ` ;
2025-08-16 19:08:39 -05:00
mdContent += '## Usage\n\n' ;
2025-06-28 01:01:26 -05:00
mdContent += ` Type \` @ ${ agentId } \` to activate this ${ await this . getAgentTitle ( agentId , installDir ) } persona. \n ` ;
2025-06-24 04:47:21 +02:00
await fileManager . writeFile ( mdPath , mdContent ) ;
console . log ( chalk . green ( ` ✓ Created rule: ${ prefix } - ${ agentId } .md ` ) ) ;
}
}
console . log ( chalk . green ( ` \n ✓ Created Cline rules in ${ clineRulesDir } ` ) ) ;
return true ;
}
2025-06-26 09:33:58 +07:00
2025-09-06 21:19:47 +03:00
async setupGeminiCli ( installDir , selectedAgent ) {
const ideConfig = await configLoader . getIdeConfiguration ( 'gemini' ) ;
const bmadCommandsDir = path . join ( installDir , ideConfig [ 'rule-dir' ] ) ;
2025-07-12 16:55:12 +02:00
2025-09-06 21:19:47 +03:00
const agentCommandsDir = path . join ( bmadCommandsDir , 'agents' ) ;
const taskCommandsDir = path . join ( bmadCommandsDir , 'tasks' ) ;
await fileManager . ensureDirectory ( agentCommandsDir ) ;
await fileManager . ensureDirectory ( taskCommandsDir ) ;
2025-08-16 00:38:44 -03:00
2025-09-06 21:19:47 +03:00
// Process Agents
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
for ( const agentId of agents ) {
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( ! agentPath ) {
console . log ( chalk . yellow ( ` ✗ Agent file not found for ${ agentId } , skipping. ` ) ) ;
continue ;
2025-07-12 16:55:12 +02:00
}
2025-09-06 21:19:47 +03:00
const agentTitle = await this . getAgentTitle ( agentId , installDir ) ;
const commandPath = path . join ( agentCommandsDir , ` ${ agentId } .toml ` ) ;
2025-06-26 09:33:58 +07:00
2025-09-06 21:19:47 +03:00
// Get relative path from installDir to agent file for @{file} reference
const relativeAgentPath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-06-26 09:33:58 +07:00
2025-09-06 21:19:47 +03:00
const tomlContent = ` description = "Activates the ${ agentTitle } agent from the BMad Method."
prompt = "" "
CRITICAL : You are now the BMad '${agentTitle}' agent . Adopt its persona , follow its instructions , and use its capabilities . The full agent definition is below .
2025-06-26 09:33:58 +07:00
2025-09-06 21:19:47 +03:00
@ { $ { relativeAgentPath } }
"" " ` ;
2025-08-16 00:38:44 -03:00
2025-09-06 21:19:47 +03:00
await fileManager . writeFile ( commandPath , tomlContent ) ;
console . log ( chalk . green ( ` ✓ Created agent command: /bmad:agents: ${ agentId } ` ) ) ;
}
2025-08-16 00:38:44 -03:00
2025-09-06 21:19:47 +03:00
// Process Tasks
const tasks = await this . getAllTaskIds ( installDir ) ;
for ( const taskId of tasks ) {
const taskPath = await this . findTaskPath ( taskId , installDir ) ;
if ( ! taskPath ) {
console . log ( chalk . yellow ( ` ✗ Task file not found for ${ taskId } , skipping. ` ) ) ;
continue ;
2025-06-26 09:33:58 +07:00
}
2025-09-06 21:19:47 +03:00
const taskTitle = taskId
. split ( '-' )
. map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( ' ' ) ;
const commandPath = path . join ( taskCommandsDir , ` ${ taskId } .toml ` ) ;
// Get relative path from installDir to task file for @{file} reference
const relativeTaskPath = path . relative ( installDir , taskPath ) . replaceAll ( '\\' , '/' ) ;
const tomlContent = ` description = "Executes the BMad Task: ${ taskTitle } "
prompt = "" "
CRITICAL : You are to execute the BMad Task defined below .
@ { $ { relativeTaskPath } }
"" " ` ;
await fileManager . writeFile ( commandPath , tomlContent ) ;
console . log ( chalk . green ( ` ✓ Created task command: /bmad:tasks: ${ taskId } ` ) ) ;
2025-06-26 09:33:58 +07:00
}
2025-09-06 21:19:47 +03:00
console . log (
chalk . green ( `
✓ Created Gemini CLI extension in $ { bmadCommandsDir } ` ),
) ;
console . log (
chalk . dim ( 'You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.' ) ,
) ;
2025-06-26 09:33:58 +07:00
return true ;
}
2025-07-01 08:54:13 -04:00
2025-08-03 21:24:09 -04:00
async setupQwenCode ( installDir , selectedAgent ) {
2025-08-16 19:08:39 -05:00
const qwenDir = path . join ( installDir , '.qwen' ) ;
const bmadMethodDir = path . join ( qwenDir , 'bmad-method' ) ;
2025-08-03 21:24:09 -04:00
await fileManager . ensureDirectory ( bmadMethodDir ) ;
// Update logic for existing settings.json
2025-08-16 19:08:39 -05:00
const settingsPath = path . join ( qwenDir , 'settings.json' ) ;
2025-08-03 21:24:09 -04:00
if ( await fileManager . pathExists ( settingsPath ) ) {
try {
const settingsContent = await fileManager . readFile ( settingsPath ) ;
const settings = JSON . parse ( settingsContent ) ;
let updated = false ;
2025-08-16 00:38:44 -03:00
2025-08-03 21:24:09 -04:00
// Handle contextFileName property
if ( settings . contextFileName && Array . isArray ( settings . contextFileName ) ) {
const originalLength = settings . contextFileName . length ;
settings . contextFileName = settings . contextFileName . filter (
2025-08-16 19:08:39 -05:00
( fileName ) => ! fileName . startsWith ( 'agents/' ) ,
2025-08-03 21:24:09 -04:00
) ;
if ( settings . contextFileName . length !== originalLength ) {
updated = true ;
}
}
2025-08-16 00:38:44 -03:00
2025-08-03 21:24:09 -04:00
if ( updated ) {
2025-08-16 19:08:39 -05:00
await fileManager . writeFile ( settingsPath , JSON . stringify ( settings , null , 2 ) ) ;
console . log ( chalk . green ( '✓ Updated .qwen/settings.json - removed agent file references' ) ) ;
2025-08-03 21:24:09 -04:00
}
} catch ( error ) {
2025-08-16 19:08:39 -05:00
console . warn ( chalk . yellow ( 'Could not update .qwen/settings.json' ) , error ) ;
2025-08-03 21:24:09 -04:00
}
}
// Remove old agents directory
2025-08-16 19:08:39 -05:00
const agentsDir = path . join ( qwenDir , 'agents' ) ;
2025-08-03 21:24:09 -04:00
if ( await fileManager . pathExists ( agentsDir ) ) {
await fileManager . removeDirectory ( agentsDir ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ Removed old .qwen/agents directory' ) ) ;
2025-08-03 21:24:09 -04:00
}
// Get all available agents
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-08-16 19:08:39 -05:00
let concatenatedContent = '' ;
2025-08-03 21:24:09 -04:00
for ( const agentId of agents ) {
// Find the source agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( agentPath ) {
const agentContent = await fileManager . readFile ( agentPath ) ;
2025-08-16 00:38:44 -03:00
2025-08-03 21:24:09 -04:00
// Create properly formatted agent rule content (similar to gemini)
let agentRuleContent = ` # ${ agentId . toUpperCase ( ) } Agent Rule \n \n ` ;
agentRuleContent += ` This rule is triggered when the user types \` * ${ agentId } \` and activates the ${ await this . getAgentTitle (
agentId ,
2025-08-16 19:08:39 -05:00
installDir ,
2025-08-03 21:24:09 -04:00
) } agent persona . \ n \ n ` ;
2025-08-16 19:08:39 -05:00
agentRuleContent += '## Agent Activation\n\n' ;
2025-08-03 21:24:09 -04:00
agentRuleContent +=
2025-08-16 19:08:39 -05:00
'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n' ;
agentRuleContent += '```yaml\n' ;
2025-08-03 21:24:09 -04:00
// Extract just the YAML content from the agent file
const yamlContent = extractYamlFromAgent ( agentContent ) ;
if ( yamlContent ) {
agentRuleContent += yamlContent ;
2025-08-16 19:08:39 -05:00
} else {
2025-08-03 21:24:09 -04:00
// If no YAML found, include the whole content minus the header
2025-08-16 19:08:39 -05:00
agentRuleContent += agentContent . replace ( /^#.*$/m , '' ) . trim ( ) ;
2025-08-03 21:24:09 -04:00
}
2025-08-16 19:08:39 -05:00
agentRuleContent += '\n```\n\n' ;
agentRuleContent += '## File Reference\n\n' ;
const relativePath = path . relative ( installDir , agentPath ) . replaceAll ( '\\' , '/' ) ;
2025-08-03 21:24:09 -04:00
agentRuleContent += ` The complete agent definition is available in [ ${ relativePath } ]( ${ relativePath } ). \n \n ` ;
2025-08-16 19:08:39 -05:00
agentRuleContent += '## Usage\n\n' ;
2025-08-03 21:24:09 -04:00
agentRuleContent += ` When the user types \` * ${ agentId } \` , activate this ${ await this . getAgentTitle (
agentId ,
2025-08-16 19:08:39 -05:00
installDir ,
2025-08-03 21:24:09 -04:00
) } persona and follow all instructions defined in the YAML configuration above . \ n ` ;
2025-08-16 00:38:44 -03:00
2025-08-03 21:24:09 -04:00
// Add to concatenated content with separator
2025-08-16 19:08:39 -05:00
concatenatedContent += agentRuleContent + '\n\n---\n\n' ;
2025-08-03 21:24:09 -04:00
console . log ( chalk . green ( ` ✓ Added context for * ${ agentId } ` ) ) ;
}
}
// Write the concatenated content to QWEN.md
2025-08-16 19:08:39 -05:00
const qwenMdPath = path . join ( bmadMethodDir , 'QWEN.md' ) ;
2025-08-03 21:24:09 -04:00
await fileManager . writeFile ( qwenMdPath , concatenatedContent ) ;
console . log ( chalk . green ( ` \n ✓ Created QWEN.md in ${ bmadMethodDir } ` ) ) ;
return true ;
}
2025-08-16 19:08:39 -05:00
async setupGitHubCopilot (
installDir ,
selectedAgent ,
spinner = null ,
preConfiguredSettings = null ,
) {
2025-07-01 08:54:13 -04:00
// Configure VS Code workspace settings first to avoid UI conflicts with loading spinners
2025-07-07 20:46:55 -05:00
await this . configureVsCodeSettings ( installDir , spinner , preConfiguredSettings ) ;
2025-08-16 00:38:44 -03:00
2025-08-16 19:08:39 -05:00
const chatmodesDir = path . join ( installDir , '.github' , 'chatmodes' ) ;
2025-07-01 08:54:13 -04:00
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
await fileManager . ensureDirectory ( chatmodesDir ) ;
for ( const agentId of agents ) {
// Find the agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
const chatmodePath = path . join ( chatmodesDir , ` ${ agentId } .chatmode.md ` ) ;
if ( agentPath ) {
// Create chat mode file with agent content
const agentContent = await fileManager . readFile ( agentPath ) ;
const agentTitle = await this . getAgentTitle ( agentId , installDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
// Extract whenToUse for the description
2025-07-13 22:48:19 -05:00
const yamlMatch = agentContent . match ( /```ya?ml\r?\n([\s\S]*?)```/ ) ;
2025-07-01 08:54:13 -04:00
let description = ` Activates the ${ agentTitle } agent persona. ` ;
if ( yamlMatch ) {
const whenToUseMatch = yamlMatch [ 1 ] . match ( /whenToUse:\s*"(.*?)"/ ) ;
if ( whenToUseMatch && whenToUseMatch [ 1 ] ) {
description = whenToUseMatch [ 1 ] ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
let chatmodeContent = ` ---
2025-08-16 19:08:39 -05:00
description : "${description.replaceAll('" ' , String . raw ` \" ` ) } "
2025-07-18 03:10:14 +02:00
tools : [ 'changes' , 'codebase' , 'fetch' , 'findTestFiles' , 'githubRepo' , 'problems' , 'usages' , 'editFiles' , 'runCommands' , 'runTasks' , 'runTests' , 'search' , 'searchResults' , 'terminalLastCommand' , 'terminalSelection' , 'testFailure' ]
2025-07-01 08:54:13 -04:00
-- -
` ;
chatmodeContent += agentContent ;
await fileManager . writeFile ( chatmodePath , chatmodeContent ) ;
console . log ( chalk . green ( ` ✓ Created chat mode: ${ agentId } .chatmode.md ` ) ) ;
}
}
2025-07-04 08:04:27 -05:00
console . log ( chalk . green ( ` \n ✓ Github Copilot setup complete! ` ) ) ;
2025-07-04 07:47:57 -05:00
console . log ( chalk . dim ( ` You can now find the BMad agents in the Chat view's mode selector. ` ) ) ;
2025-07-01 08:54:13 -04:00
return true ;
}
2025-07-07 20:46:55 -05:00
async configureVsCodeSettings ( installDir , spinner , preConfiguredSettings = null ) {
2025-08-16 19:08:39 -05:00
const vscodeDir = path . join ( installDir , '.vscode' ) ;
const settingsPath = path . join ( vscodeDir , 'settings.json' ) ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
await fileManager . ensureDirectory ( vscodeDir ) ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
// Read existing settings if they exist
let existingSettings = { } ;
if ( await fileManager . pathExists ( settingsPath ) ) {
try {
const existingContent = await fileManager . readFile ( settingsPath ) ;
existingSettings = JSON . parse ( existingContent ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . yellow ( 'Found existing .vscode/settings.json. Merging BMad settings...' ) ) ;
} catch {
console . warn ( chalk . yellow ( 'Could not parse existing settings.json. Creating new one.' ) ) ;
2025-07-01 08:54:13 -04:00
existingSettings = { } ;
}
}
2025-08-16 00:38:44 -03:00
2025-07-07 20:46:55 -05:00
// Use pre-configured settings if provided, otherwise prompt
let configChoice ;
if ( preConfiguredSettings && preConfiguredSettings . configChoice ) {
configChoice = preConfiguredSettings . configChoice ;
console . log ( chalk . dim ( ` Using pre-configured GitHub Copilot settings: ${ configChoice } ` ) ) ;
} else {
// Clear any previous output and add spacing to avoid conflicts with loaders
console . log ( '\n' . repeat ( 2 ) ) ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . blue ( '🔧 Github Copilot Agent Settings Configuration' ) ) ;
console . log (
chalk . dim ( 'BMad works best with specific VS Code settings for optimal agent experience.' ) ,
) ;
2025-07-07 20:46:55 -05:00
console . log ( '' ) ; // Add extra spacing
2025-08-16 00:38:44 -03:00
2025-07-07 20:46:55 -05:00
const response = await inquirer . prompt ( [
{
type : 'list' ,
name : 'configChoice' ,
message : chalk . yellow ( 'How would you like to configure GitHub Copilot settings?' ) ,
choices : [
{
name : 'Use recommended defaults (fastest setup)' ,
2025-08-16 19:08:39 -05:00
value : 'defaults' ,
2025-07-07 20:46:55 -05:00
} ,
{
name : 'Configure each setting manually (customize to your preferences)' ,
2025-08-16 19:08:39 -05:00
value : 'manual' ,
2025-07-07 20:46:55 -05:00
} ,
{
2025-08-16 19:08:39 -05:00
name : "Skip settings configuration (I'll configure manually later)" ,
value : 'skip' ,
} ,
2025-07-07 20:46:55 -05:00
] ,
2025-08-16 19:08:39 -05:00
default : 'defaults' ,
} ,
2025-07-07 20:46:55 -05:00
] ) ;
configChoice = response . configChoice ;
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
let bmadSettings = { } ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
if ( configChoice === 'skip' ) {
2025-08-16 19:08:39 -05:00
console . log ( chalk . yellow ( '⚠️ Skipping VS Code settings configuration.' ) ) ;
console . log ( chalk . dim ( 'You can manually configure these settings in .vscode/settings.json:' ) ) ;
console . log ( chalk . dim ( ' • chat.agent.enabled: true' ) ) ;
console . log ( chalk . dim ( ' • chat.agent.maxRequests: 15' ) ) ;
console . log ( chalk . dim ( ' • github.copilot.chat.agent.runTasks: true' ) ) ;
console . log ( chalk . dim ( ' • chat.mcp.discovery.enabled: true' ) ) ;
console . log ( chalk . dim ( ' • github.copilot.chat.agent.autoFix: true' ) ) ;
console . log ( chalk . dim ( ' • chat.tools.autoApprove: false' ) ) ;
2025-07-01 08:54:13 -04:00
return true ;
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
if ( configChoice === 'defaults' ) {
// Use recommended defaults
bmadSettings = {
2025-08-16 19:08:39 -05:00
'chat.agent.enabled' : true ,
'chat.agent.maxRequests' : 15 ,
'github.copilot.chat.agent.runTasks' : true ,
'chat.mcp.discovery.enabled' : true ,
'github.copilot.chat.agent.autoFix' : true ,
'chat.tools.autoApprove' : false ,
2025-07-01 08:54:13 -04:00
} ;
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ Using recommended BMad defaults for Github Copilot settings' ) ) ;
2025-07-01 08:54:13 -04:00
} else {
// Manual configuration
console . log ( chalk . blue ( "\n📋 Let's configure each setting for your preferences:" ) ) ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
// Pause spinner during manual configuration prompts
let spinnerWasActive = false ;
if ( spinner && spinner . isSpinning ) {
spinner . stop ( ) ;
spinnerWasActive = true ;
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
const manualSettings = await inquirer . prompt ( [
{
type : 'input' ,
name : 'maxRequests' ,
message : 'Maximum requests per agent session (recommended: 15)?' ,
default : '15' ,
validate : ( input ) => {
2025-08-16 19:08:39 -05:00
const number _ = Number . parseInt ( input ) ;
if ( isNaN ( number _ ) || number _ < 1 || number _ > 50 ) {
2025-07-01 08:54:13 -04:00
return 'Please enter a number between 1 and 50' ;
}
return true ;
2025-08-16 19:08:39 -05:00
} ,
2025-07-01 08:54:13 -04:00
} ,
{
type : 'confirm' ,
name : 'runTasks' ,
message : 'Allow agents to run workspace tasks (package.json scripts, etc.)?' ,
2025-08-16 19:08:39 -05:00
default : true ,
2025-07-01 08:54:13 -04:00
} ,
{
type : 'confirm' ,
name : 'mcpDiscovery' ,
message : 'Enable MCP (Model Context Protocol) server discovery?' ,
2025-08-16 19:08:39 -05:00
default : true ,
2025-07-01 08:54:13 -04:00
} ,
{
type : 'confirm' ,
name : 'autoFix' ,
message : 'Enable automatic error detection and fixing in generated code?' ,
2025-08-16 19:08:39 -05:00
default : true ,
2025-07-01 08:54:13 -04:00
} ,
{
type : 'confirm' ,
name : 'autoApprove' ,
message : 'Auto-approve ALL tools without confirmation? (⚠️ EXPERIMENTAL - less secure)' ,
2025-08-16 19:08:39 -05:00
default : false ,
} ,
2025-07-01 08:54:13 -04:00
] ) ;
// Restart spinner if it was active before prompts
if ( spinner && spinnerWasActive ) {
spinner . start ( ) ;
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
bmadSettings = {
2025-08-16 19:08:39 -05:00
'chat.agent.enabled' : true , // Always enabled - required for BMad agents
'chat.agent.maxRequests' : Number . parseInt ( manualSettings . maxRequests ) ,
'github.copilot.chat.agent.runTasks' : manualSettings . runTasks ,
'chat.mcp.discovery.enabled' : manualSettings . mcpDiscovery ,
'github.copilot.chat.agent.autoFix' : manualSettings . autoFix ,
'chat.tools.autoApprove' : manualSettings . autoApprove ,
2025-07-01 08:54:13 -04:00
} ;
2025-08-16 00:38:44 -03:00
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ Custom settings configured' ) ) ;
2025-07-01 08:54:13 -04:00
}
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
// Merge settings (existing settings take precedence to avoid overriding user preferences)
const mergedSettings = { ... bmadSettings , ... existingSettings } ;
2025-08-16 00:38:44 -03:00
2025-07-01 08:54:13 -04:00
// Write the updated settings
await fileManager . writeFile ( settingsPath , JSON . stringify ( mergedSettings , null , 2 ) ) ;
2025-08-16 00:38:44 -03:00
2025-08-16 19:08:39 -05:00
console . log ( chalk . green ( '✓ VS Code workspace settings configured successfully' ) ) ;
console . log ( chalk . dim ( ' Settings written to .vscode/settings.json:' ) ) ;
for ( const [ key , value ] of Object . entries ( bmadSettings ) ) {
2025-07-01 08:54:13 -04:00
console . log ( chalk . dim ( ` • ${ key } : ${ value } ` ) ) ;
2025-08-16 19:08:39 -05:00
}
console . log ( chalk . dim ( '' ) ) ;
console . log ( chalk . dim ( 'You can modify these settings anytime in .vscode/settings.json' ) ) ;
2025-07-01 08:54:13 -04:00
}
2025-09-03 00:16:26 +07:00
async setupAuggieCLI ( installDir , selectedAgent , spinner = null , preConfiguredSettings = null ) {
const os = require ( 'node:os' ) ;
const inquirer = require ( 'inquirer' ) ;
const agents = selectedAgent ? [ selectedAgent ] : await this . getAllAgentIds ( installDir ) ;
// Get the IDE configuration to access location options
const ideConfig = await configLoader . getIdeConfiguration ( 'auggie-cli' ) ;
const locations = ideConfig . locations ;
// Use pre-configured settings if provided, otherwise prompt
let selectedLocations ;
if ( preConfiguredSettings && preConfiguredSettings . selectedLocations ) {
selectedLocations = preConfiguredSettings . selectedLocations ;
console . log (
chalk . dim (
` Using pre-configured Auggie CLI (Augment Code) locations: ${ selectedLocations . join ( ', ' ) } ` ,
) ,
) ;
} else {
// Pause spinner during location selection to avoid UI conflicts
let spinnerWasActive = false ;
if ( spinner && spinner . isSpinning ) {
spinner . stop ( ) ;
spinnerWasActive = true ;
}
// Clear any previous output and add spacing to avoid conflicts with loaders
console . log ( '\n' . repeat ( 2 ) ) ;
console . log ( chalk . blue ( '📍 Auggie CLI Location Configuration' ) ) ;
console . log ( chalk . dim ( 'Choose where to install BMad agents for Auggie CLI access.' ) ) ;
console . log ( '' ) ; // Add extra spacing
const response = await inquirer . prompt ( [
{
type : 'checkbox' ,
name : 'selectedLocations' ,
message : 'Select Auggie CLI command locations:' ,
choices : Object . entries ( locations ) . map ( ( [ key , location ] ) => ( {
name : ` ${ location . name } : ${ location . description } ` ,
value : key ,
} ) ) ,
validate : ( selected ) => {
if ( selected . length === 0 ) {
return 'Please select at least one location' ;
}
return true ;
} ,
} ,
] ) ;
selectedLocations = response . selectedLocations ;
// Restart spinner if it was active before prompts
if ( spinner && spinnerWasActive ) {
spinner . start ( ) ;
}
}
// Install to each selected location
for ( const locationKey of selectedLocations ) {
const location = locations [ locationKey ] ;
let commandsDir = location [ 'rule-dir' ] ;
// Handle tilde expansion for user directory
if ( commandsDir . startsWith ( '~/' ) ) {
commandsDir = path . join ( os . homedir ( ) , commandsDir . slice ( 2 ) ) ;
} else if ( commandsDir . startsWith ( './' ) ) {
commandsDir = path . join ( installDir , commandsDir . slice ( 2 ) ) ;
}
await fileManager . ensureDirectory ( commandsDir ) ;
for ( const agentId of agents ) {
// Find the agent file
const agentPath = await this . findAgentPath ( agentId , installDir ) ;
if ( agentPath ) {
const agentContent = await fileManager . readFile ( agentPath ) ;
const mdPath = path . join ( commandsDir , ` ${ agentId } .md ` ) ;
await fileManager . writeFile ( mdPath , agentContent ) ;
console . log ( chalk . green ( ` ✓ Created command: ${ agentId } .md in ${ location . name } ` ) ) ;
}
}
console . log ( chalk . green ( ` \n ✓ Created Auggie CLI commands in ${ commandsDir } ` ) ) ;
console . log ( chalk . dim ( ` Location: ${ location . name } - ${ location . description } ` ) ) ;
}
return true ;
}
2025-06-12 22:38:24 -05:00
}
2025-06-14 15:06:41 -05:00
module . exports = new IdeSetup ( ) ;