diff --git a/tools/installer/bin/bmad.js b/tools/installer/bin/bmad.js index 152e1634..a8568bd8 100755 --- a/tools/installer/bin/bmad.js +++ b/tools/installer/bin/bmad.js @@ -484,29 +484,10 @@ async function promptInstallation() { console.log(chalk.cyan('\n⚙️ OpenCode (SST) Configuration')); console.log( chalk.dim( - 'Select which agents you want in opencode.json(c) and choose optional key prefixes (defaults: no prefixes).\n', + 'OpenCode will include agents and tasks from the packages you selected above; choose optional key prefixes (defaults: no prefixes).\n', ), ); - // Load available agents from installer - const availableAgents = (await installer.getAvailableAgents()) || []; - const agentChoices = availableAgents.map((a) => ({ name: a.id, value: a.id })); - - const { selectedOpenCodeAgents } = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedOpenCodeAgents', - message: 'Select agents to add to OpenCode:', - choices: agentChoices, - validate: (selected) => { - if (selected.length === 0) { - return 'Please select at least one agent for OpenCode or deselect OpenCode from IDEs.'; - } - return true; - }, - }, - ]); - const { useAgentPrefix, useCommandPrefix } = await inquirer.prompt([ { type: 'confirm', @@ -527,8 +508,11 @@ async function promptInstallation() { useAgentPrefix, useCommandPrefix, }, - // pass selected agents so IDE setup only applies those - selectedAgents: selectedOpenCodeAgents, + // pass previously selected packages so IDE setup only applies those + selectedPackages: { + includeCore: selectedItems.includes('bmad-core'), + packs: answers.expansionPacks || [], + }, }; } diff --git a/tools/installer/lib/ide-setup.js b/tools/installer/lib/ide-setup.js index a9f2a821..be81ba64 100644 --- a/tools/installer/lib/ide-setup.js +++ b/tools/installer/lib/ide-setup.js @@ -153,11 +153,18 @@ class IdeSetup extends BaseIdeSetup { } const ensureInstructionRef = (obj) => { - const ref = '.bmad-core/core-config.yaml'; + 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]; - const hasRef = obj.instructions.some((it) => typeof it === 'string' && it === ref); - if (!hasRef) obj.instructions.push(ref); + // 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; }; @@ -165,6 +172,8 @@ class IdeSetup extends BaseIdeSetup { // 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 = { @@ -178,16 +187,78 @@ class IdeSetup extends BaseIdeSetup { commandsSkipped: 0, }; - // Agents: use core agent ids by default - // If pre-config provided selected agents for opencode, respect that list - const preSelected = preConfiguredSettings?.selectedAgents; - const agentIds = - preSelected && Array.isArray(preSelected) && preSelected.length > 0 - ? preSelected - : selectedAgent - ? [selectedAgent] - : await this.getCoreAgentIds(installDir); - for (const agentId of agentIds) { + // 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); + + // 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-') @@ -196,8 +267,8 @@ class IdeSetup extends BaseIdeSetup { : baseKey; const existing = configObj.agent[key]; const agentDef = { - prompt: `{file:./.bmad-core/agents/${agentId}.md}`, - mode: 'subagent', + prompt: fileRef, + mode: isOrchestratorAgent(agentId) ? 'primary' : 'subagent', }; if (!existing) { configObj.agent[key] = agentDef; @@ -206,9 +277,8 @@ class IdeSetup extends BaseIdeSetup { existing && typeof existing === 'object' && typeof existing.prompt === 'string' && - existing.prompt.includes(`./.bmad-core/agents/${agentId}.md`) + existing.prompt.includes(rel) ) { - // Update only BMAD-managed entries detected by prompt path existing.prompt = agentDef.prompt; existing.mode = agentDef.mode; configObj.agent[key] = existing; @@ -218,14 +288,47 @@ class IdeSetup extends BaseIdeSetup { } } - // Commands: expose core tasks as commands - const taskIds = await this.getAllTaskIds(installDir); - for (const taskId of taskIds) { + // 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 agentDef = { + prompt: fileRef, + mode: isOrchestratorAgent(agentId) ? 'primary' : 'subagent', + }; + 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; + configObj.agent[prefixedKey] = existing; + summary.agentsUpdated++; + } else { + summary.agentsSkipped++; + } + } + } + + // 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 cmdDef = { - template: `{file:./.bmad-core/tasks/${taskId}.md}`, - }; + const cmdDef = { template: fileRef }; if (!existing) { configObj.command[key] = cmdDef; summary.commandsAdded++; @@ -233,9 +336,8 @@ class IdeSetup extends BaseIdeSetup { existing && typeof existing === 'object' && typeof existing.template === 'string' && - existing.template.includes(`./.bmad-core/tasks/${taskId}.md`) + existing.template.includes(rel) ) { - // Update only BMAD-managed entries detected by template path existing.template = cmdDef.template; configObj.command[key] = existing; summary.commandsUpdated++; @@ -244,6 +346,34 @@ class IdeSetup extends BaseIdeSetup { } } + // 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 cmdDef = { template: fileRef }; + 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; + configObj.command[prefixedKey] = existing; + summary.commandsUpdated++; + } else { + summary.commandsSkipped++; + } + } + } + return { configObj, summary }; }; @@ -279,7 +409,7 @@ class IdeSetup extends BaseIdeSetup { // Create minimal opencode.jsonc const minimal = { $schema: 'https://opencode.ai/config.json', - instructions: ['./.bmad-core/core-config.yaml'], + instructions: ['.bmad-core/core-config.yaml'], agent: {}, command: {}, };