refactor: streamline OpenCode configuration prompts and normalize instruction paths for agents and tasks

This commit is contained in:
Javier Gomez 2025-09-10 12:21:02 +02:00
parent 9de0932e55
commit c4c4331801
2 changed files with 162 additions and 48 deletions

View File

@ -484,29 +484,10 @@ async function promptInstallation() {
console.log(chalk.cyan('\n⚙ OpenCode (SST) Configuration')); console.log(chalk.cyan('\n⚙ OpenCode (SST) Configuration'));
console.log( console.log(
chalk.dim( 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([ const { useAgentPrefix, useCommandPrefix } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
@ -527,8 +508,11 @@ async function promptInstallation() {
useAgentPrefix, useAgentPrefix,
useCommandPrefix, useCommandPrefix,
}, },
// pass selected agents so IDE setup only applies those // pass previously selected packages so IDE setup only applies those
selectedAgents: selectedOpenCodeAgents, selectedPackages: {
includeCore: selectedItems.includes('bmad-core'),
packs: answers.expansionPacks || [],
},
}; };
} }

View File

@ -153,11 +153,18 @@ class IdeSetup extends BaseIdeSetup {
} }
const ensureInstructionRef = (obj) => { 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 (!obj.instructions) obj.instructions = [];
if (!Array.isArray(obj.instructions)) obj.instructions = [obj.instructions]; if (!Array.isArray(obj.instructions)) obj.instructions = [obj.instructions];
const hasRef = obj.instructions.some((it) => typeof it === 'string' && it === ref); // Normalize alternative form (with './') to preferred without './'
if (!hasRef) obj.instructions.push(ref); 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; return obj;
}; };
@ -165,6 +172,8 @@ class IdeSetup extends BaseIdeSetup {
// Ensure objects exist // Ensure objects exist
if (!configObj.agent || typeof configObj.agent !== 'object') configObj.agent = {}; if (!configObj.agent || typeof configObj.agent !== 'object') configObj.agent = {};
if (!configObj.command || typeof configObj.command !== 'object') configObj.command = {}; 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 // Track a concise summary of changes
const summary = { const summary = {
@ -178,16 +187,78 @@ class IdeSetup extends BaseIdeSetup {
commandsSkipped: 0, commandsSkipped: 0,
}; };
// Agents: use core agent ids by default // Determine package scope: previously SELECTED packages in installer UI
// If pre-config provided selected agents for opencode, respect that list const selectedPackages = preConfiguredSettings?.selectedPackages || {
const preSelected = preConfiguredSettings?.selectedAgents; includeCore: true,
const agentIds = packs: [],
preSelected && Array.isArray(preSelected) && preSelected.length > 0 };
? preSelected
: selectedAgent // Helper: ensure an instruction path is present without './' prefix, de-duplicating './' variants
? [selectedAgent] const ensureInstructionPath = (pathNoDot) => {
: await this.getCoreAgentIds(installDir); const withDot = `./${pathNoDot}`;
for (const agentId of agentIds) { // 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 baseKey = agentId;
const key = useAgentPrefix const key = useAgentPrefix
? baseKey.startsWith('bmad-') ? baseKey.startsWith('bmad-')
@ -196,8 +267,8 @@ class IdeSetup extends BaseIdeSetup {
: baseKey; : baseKey;
const existing = configObj.agent[key]; const existing = configObj.agent[key];
const agentDef = { const agentDef = {
prompt: `{file:./.bmad-core/agents/${agentId}.md}`, prompt: fileRef,
mode: 'subagent', mode: isOrchestratorAgent(agentId) ? 'primary' : 'subagent',
}; };
if (!existing) { if (!existing) {
configObj.agent[key] = agentDef; configObj.agent[key] = agentDef;
@ -206,9 +277,8 @@ class IdeSetup extends BaseIdeSetup {
existing && existing &&
typeof existing === 'object' && typeof existing === 'object' &&
typeof existing.prompt === 'string' && 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.prompt = agentDef.prompt;
existing.mode = agentDef.mode; existing.mode = agentDef.mode;
configObj.agent[key] = existing; configObj.agent[key] = existing;
@ -218,14 +288,47 @@ class IdeSetup extends BaseIdeSetup {
} }
} }
// Commands: expose core tasks as commands // Generate agents - expansion packs (forced pack-specific prefix)
const taskIds = await this.getAllTaskIds(installDir); for (const pack of packsInfo) {
for (const taskId of taskIds) { 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 key = useCommandPrefix ? `bmad:tasks:${taskId}` : `${taskId}`;
const existing = configObj.command[key]; const existing = configObj.command[key];
const cmdDef = { const cmdDef = { template: fileRef };
template: `{file:./.bmad-core/tasks/${taskId}.md}`,
};
if (!existing) { if (!existing) {
configObj.command[key] = cmdDef; configObj.command[key] = cmdDef;
summary.commandsAdded++; summary.commandsAdded++;
@ -233,9 +336,8 @@ class IdeSetup extends BaseIdeSetup {
existing && existing &&
typeof existing === 'object' && typeof existing === 'object' &&
typeof existing.template === 'string' && 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; existing.template = cmdDef.template;
configObj.command[key] = existing; configObj.command[key] = existing;
summary.commandsUpdated++; 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 }; return { configObj, summary };
}; };
@ -279,7 +409,7 @@ class IdeSetup extends BaseIdeSetup {
// Create minimal opencode.jsonc // Create minimal opencode.jsonc
const minimal = { const minimal = {
$schema: 'https://opencode.ai/config.json', $schema: 'https://opencode.ai/config.json',
instructions: ['./.bmad-core/core-config.yaml'], instructions: ['.bmad-core/core-config.yaml'],
agent: {}, agent: {},
command: {}, command: {},
}; };