feat(installer): enhance OpenCode setup with agent selection and prefix options

This commit is contained in:
Javier Gomez 2025-09-10 11:46:43 +02:00
parent 79442df3f0
commit 9970d711e7
3 changed files with 167 additions and 13 deletions

View File

@ -479,6 +479,59 @@ async function promptInstallation() {
answers.githubCopilotConfig = { configChoice };
}
// Configure OpenCode (SST) immediately if selected
if (ides.includes('opencode')) {
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',
),
);
// 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',
name: 'useAgentPrefix',
message: "Prefix agent keys with 'bmad-'? (e.g., 'bmad-dev')",
default: false,
},
{
type: 'confirm',
name: 'useCommandPrefix',
message: "Prefix command keys with 'bmad:tasks:'? (e.g., 'bmad:tasks:create-doc')",
default: false,
},
]);
answers.openCodeConfig = {
opencode: {
useAgentPrefix,
useCommandPrefix,
},
// pass selected agents so IDE setup only applies those
selectedAgents: selectedOpenCodeAgents,
};
}
// Configure Auggie CLI (Augment Code) immediately if selected
if (ides.includes('auggie-cli')) {
console.log(chalk.cyan('\n📍 Auggie CLI Location Configuration'));

View File

@ -46,7 +46,7 @@ class IdeSetup extends BaseIdeSetup {
return this.setupCursor(installDir, selectedAgent);
}
case 'opencode': {
return this.setupOpenCode(installDir, selectedAgent);
return this.setupOpenCode(installDir, selectedAgent, spinner, preConfiguredSettings);
}
case 'claude-code': {
return this.setupClaudeCode(installDir, selectedAgent);
@ -97,7 +97,7 @@ class IdeSetup extends BaseIdeSetup {
}
}
async setupOpenCode(installDir, selectedAgent) {
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
@ -107,8 +107,53 @@ class IdeSetup extends BaseIdeSetup {
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: false,
},
{
type: 'confirm',
name: 'useCommandPrefix',
message:
"Prefix command keys with 'bmad:tasks:'? (Recommended, e.g., 'bmad:tasks:create-doc')",
default: false,
},
]);
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 ref = './.bmad-core/core-config.yaml';
const ref = '.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);
@ -121,10 +166,34 @@ class IdeSetup extends BaseIdeSetup {
if (!configObj.agent || typeof configObj.agent !== 'object') configObj.agent = {};
if (!configObj.command || typeof configObj.command !== 'object') configObj.command = {};
// Track a concise summary of changes
const summary = {
target: null,
created: false,
agentsAdded: 0,
agentsUpdated: 0,
agentsSkipped: 0,
commandsAdded: 0,
commandsUpdated: 0,
commandsSkipped: 0,
};
// Agents: use core agent ids by default
const agentIds = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir);
// 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) {
const key = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`;
const baseKey = agentId;
const key = useAgentPrefix
? baseKey.startsWith('bmad-')
? baseKey
: `bmad-${baseKey}`
: baseKey;
const existing = configObj.agent[key];
const agentDef = {
prompt: `{file:./.bmad-core/agents/${agentId}.md}`,
@ -132,6 +201,7 @@ class IdeSetup extends BaseIdeSetup {
};
if (!existing) {
configObj.agent[key] = agentDef;
summary.agentsAdded++;
} else if (
existing &&
typeof existing === 'object' &&
@ -142,19 +212,23 @@ class IdeSetup extends BaseIdeSetup {
existing.prompt = agentDef.prompt;
existing.mode = agentDef.mode;
configObj.agent[key] = existing;
summary.agentsUpdated++;
} else {
summary.agentsSkipped++;
}
}
// Commands: expose core tasks as commands
const taskIds = await this.getAllTaskIds(installDir);
for (const taskId of taskIds) {
const key = `bmad:tasks:${taskId}`;
const key = useCommandPrefix ? `bmad:tasks:${taskId}` : `${taskId}`;
const existing = configObj.command[key];
const cmdDef = {
template: `{file:./.bmad-core/tasks/${taskId}.md}`,
};
if (!existing) {
configObj.command[key] = cmdDef;
summary.commandsAdded++;
} else if (
existing &&
typeof existing === 'object' &&
@ -164,10 +238,13 @@ class IdeSetup extends BaseIdeSetup {
// Update only BMAD-managed entries detected by template path
existing.template = cmdDef.template;
configObj.command[key] = existing;
summary.commandsUpdated++;
} else {
summary.commandsSkipped++;
}
}
return configObj;
return { configObj, summary };
};
if (hasJson || hasJsonc) {
@ -178,7 +255,7 @@ class IdeSetup extends BaseIdeSetup {
// Use comment-json for both .json and .jsonc for resilience
const parsed = cjson.parse(raw, undefined, true);
ensureInstructionRef(parsed);
await mergeBmadAgentsAndCommands(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(
@ -186,6 +263,12 @@ class IdeSetup extends BaseIdeSetup {
'✓ 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}`,
),
);
} catch (error) {
console.log(chalk.red('✗ Failed to update existing OpenCode config'), error.message);
return false;
@ -201,12 +284,17 @@ class IdeSetup extends BaseIdeSetup {
command: {},
};
try {
await mergeBmadAgentsAndCommands(minimal);
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}`,
),
);
return true;
} catch (error) {
console.log(chalk.red('✗ Failed to create opencode.jsonc'), error.message);

View File

@ -409,10 +409,23 @@ class Installer {
for (const ide of ides) {
spinner.text = `Setting up ${ide} integration...`;
let preConfiguredSettings = null;
if (ide === 'github-copilot') {
preConfiguredSettings = config.githubCopilotConfig;
} else if (ide === 'auggie-cli') {
preConfiguredSettings = config.augmentCodeConfig;
switch (ide) {
case 'github-copilot': {
preConfiguredSettings = config.githubCopilotConfig;
break;
}
case 'auggie-cli': {
preConfiguredSettings = config.augmentCodeConfig;
break;
}
case 'opencode': {
preConfiguredSettings = config.openCodeConfig;
break;
}
default: {
// no pre-configured settings
break;
}
}
await ideSetup.setup(ide, installDir, config.agent, spinner, preConfiguredSettings);
}