feat(installer/opencode): implement setupOpenCode with minimal instructions merge and BMAD-managed agents/commands

This commit is contained in:
Javier Gomez 2025-09-09 19:34:06 +02:00
parent 5fb8df11f2
commit 0c3e3be692
1 changed files with 122 additions and 0 deletions

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const chalk = require('chalk'); const chalk = require('chalk');
const inquirer = require('inquirer'); const inquirer = require('inquirer');
const cjson = require('comment-json');
const fileManager = require('./file-manager'); const fileManager = require('./file-manager');
const configLoader = require('./config-loader'); const configLoader = require('./config-loader');
const { extractYamlFromAgent } = require('../../lib/yaml-utils'); const { extractYamlFromAgent } = require('../../lib/yaml-utils');
@ -44,6 +45,9 @@ class IdeSetup extends BaseIdeSetup {
case 'cursor': { case 'cursor': {
return this.setupCursor(installDir, selectedAgent); return this.setupCursor(installDir, selectedAgent);
} }
case 'opencode': {
return this.setupOpenCode(installDir, selectedAgent);
}
case 'claude-code': { case 'claude-code': {
return this.setupClaudeCode(installDir, selectedAgent); return this.setupClaudeCode(installDir, selectedAgent);
} }
@ -93,6 +97,124 @@ class IdeSetup extends BaseIdeSetup {
} }
} }
async setupOpenCode(installDir, selectedAgent) {
// 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);
const ensureInstructionRef = (obj) => {
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
: it && typeof it.file === 'string'
? it.file === ref
: false,
);
if (!hasRef) obj.instructions.push({ file: ref });
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 = {};
// Agents: use core agent ids by default
const agentIds = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir);
for (const agentId of agentIds) {
const key = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`;
const existing = configObj.agent[key];
const agentDef = {
instructions: [{ file: `./.bmad-core/agents/${agentId}.md` }],
tools: ['write', 'edit', 'bash'],
mode: 'subagent',
bmadManaged: true,
};
if (!existing) {
configObj.agent[key] = agentDef;
} else if (existing && existing.bmadManaged) {
// Update to latest shape without clobbering non-BMAD entries
existing.instructions = agentDef.instructions;
existing.tools = agentDef.tools;
existing.mode = agentDef.mode;
existing.bmadManaged = true;
configObj.agent[key] = existing;
}
}
// Commands: expose core tasks as commands
const taskIds = await this.getAllTaskIds(installDir);
for (const taskId of taskIds) {
const key = `bmad:tasks:${taskId}`;
const existing = configObj.command[key];
const cmdDef = {
instructions: [{ file: `./.bmad-core/tasks/${taskId}.md` }],
bmadManaged: true,
};
if (!existing) {
configObj.command[key] = cmdDef;
} else if (existing && existing.bmadManaged) {
existing.instructions = cmdDef.instructions;
existing.bmadManaged = true;
configObj.command[key] = existing;
}
}
return configObj;
};
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);
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',
),
);
} 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: [{ file: './.bmad-core/core-config.yaml' }],
agent: {},
command: {},
};
try {
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'),
);
return true;
} catch (error) {
console.log(chalk.red('✗ Failed to create opencode.jsonc'), error.message);
return false;
}
}
async setupCodex(installDir, selectedAgent, options) { async setupCodex(installDir, selectedAgent, options) {
options = options ?? { webEnabled: false }; options = options ?? { webEnabled: false };
// Codex reads AGENTS.md at the project root as project memory (CLI & Web). // Codex reads AGENTS.md at the project root as project memory (CLI & Web).