Add Opencode IDE installer (#820)
- Added docs/ide-info/opencode.md - Added tool/cli/installers/lib/ide/opencode.js - Modified tools/installers/lib/ide/core/detector.js to include detection for opencode command dir - Modified tools/cli/platform-codes.yaml to include opencode config - Modified tools/cli/installers/lib/ide/workflow-command-template.md to include frontmatter with description as opencode requires this for commands and adding it to the template by default does not seem to impact other IDEs - Modified src/modules/bmm/workflows/workflow-status/workflow.yaml description so that it properly escapes quotes when interpolated in the teplate
This commit is contained in:
parent
b7e6bfcde5
commit
8220c819e6
|
|
@ -0,0 +1,24 @@
|
||||||
|
# BMAD Method - OpenCode Instructions
|
||||||
|
|
||||||
|
## Activating Agents
|
||||||
|
|
||||||
|
BMAD agents are installed as OpenCode agents in `.opencode/agent/BMAD/{module_name}` and workflow commands in `.opencode/command/BMAD/{module_name}`.
|
||||||
|
|
||||||
|
### How to Use
|
||||||
|
|
||||||
|
1. **Switch Agents**: Press **Tab** to cycle through primary agents or select using the `/agents`
|
||||||
|
2. **Activate Agent**: Once the Agent is selected say `hello` or any prompt to activate that agent persona
|
||||||
|
3. **Execute Commands**: Type `/bmad` to see and execute bmad workflow commands (commands allow for fuzzy matching)
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
/agents - to see a list of agents and switch between them
|
||||||
|
/bmad/bmm/workflows/workflow-init - Activate the workflow-init command
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Press **Tab** to switch between primary agents (Analyst, Architect, Dev, etc.)
|
||||||
|
- Commands are autocompleted when you type `/` and allow for fuzzy matching
|
||||||
|
- Workflow commands execute in current agent context, make sure you have the right agent activated before running a command
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Workflow Status - Master Router and Status Tracker
|
# Workflow Status - Master Router and Status Tracker
|
||||||
name: workflow-status
|
name: workflow-status
|
||||||
description: "Lightweight status checker - answers 'what should I do now?' for any agent. Reads simple key-value status file for instant parsing. Use workflow-init for new projects."
|
description: 'Lightweight status checker - answers "what should I do now?" for any agent. Reads simple key-value status file for instant parsing. Use workflow-init for new projects.'
|
||||||
author: "BMad"
|
author: "BMad"
|
||||||
|
|
||||||
# Critical variables from config
|
# Critical variables from config
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ The installer is a multi-stage system that handles agent compilation, IDE integr
|
||||||
|
|
||||||
### IDE Support
|
### IDE Support
|
||||||
|
|
||||||
The installer supports **14 IDE environments** through a base-derived architecture. Each IDE handler extends `BaseIDE` and implements IDE-specific artifact generation.
|
The installer supports **15 IDE environments** through a base-derived architecture. Each IDE handler extends `BaseIDE` and implements IDE-specific artifact generation.
|
||||||
|
|
||||||
**Supported IDEs** (as of v6-alpha):
|
**Supported IDEs** (as of v6-alpha):
|
||||||
|
|
||||||
|
|
@ -134,6 +134,7 @@ The installer supports **14 IDE environments** through a base-derived architectu
|
||||||
| ---------------- | ----------------- | ---------------------- |
|
| ---------------- | ----------------- | ---------------------- |
|
||||||
| `codex` | Claude Code | `.claude/commands/` |
|
| `codex` | Claude Code | `.claude/commands/` |
|
||||||
| `claude-code` | Claude Code (alt) | `.claude/commands/` |
|
| `claude-code` | Claude Code (alt) | `.claude/commands/` |
|
||||||
|
| `opencode` | OpenCode | `.opencode` |
|
||||||
| `windsurf` | Windsurf | `.windsurf/workflows/` |
|
| `windsurf` | Windsurf | `.windsurf/workflows/` |
|
||||||
| `cursor` | Cursor | `.cursor/rules/` |
|
| `cursor` | Cursor | `.cursor/rules/` |
|
||||||
| `cline` | Cline | `.clinerules/` |
|
| `cline` | Cline | `.clinerules/` |
|
||||||
|
|
|
||||||
|
|
@ -211,10 +211,11 @@ class Detector {
|
||||||
|
|
||||||
// Check inside various IDE command folders for legacy bmad folders
|
// Check inside various IDE command folders for legacy bmad folders
|
||||||
// List of IDE config folders that might have commands directories
|
// List of IDE config folders that might have commands directories
|
||||||
const ideConfigFolders = ['.claude', '.crush', '.continue', '.cursor', '.windsurf', '.cline', '.roo-cline'];
|
const ideConfigFolders = ['.opencode', '.claude', '.crush', '.continue', '.cursor', '.windsurf', '.cline', '.roo-cline'];
|
||||||
|
|
||||||
for (const ideFolder of ideConfigFolders) {
|
for (const ideFolder of ideConfigFolders) {
|
||||||
const commandsPath = path.join(projectDir, ideFolder, 'commands');
|
const commandsDirName = ideFolder === '.opencode' ? 'command' : 'commands';
|
||||||
|
const commandsPath = path.join(projectDir, ideFolder, commandsDirName);
|
||||||
if (await fs.pathExists(commandsPath)) {
|
if (await fs.pathExists(commandsPath)) {
|
||||||
try {
|
try {
|
||||||
const commandEntries = await fs.readdir(commandsPath, { withFileTypes: true });
|
const commandEntries = await fs.readdir(commandsPath, { withFileTypes: true });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const os = require('node:os');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
|
const { WorkflowCommandGenerator } = require('./workflow-command-generator');
|
||||||
|
|
||||||
|
const { getAgentsFromBmad } = require('./shared/bmad-artifacts');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenCode IDE setup handler
|
||||||
|
*/
|
||||||
|
class OpenCodeSetup extends BaseIdeSetup {
|
||||||
|
constructor() {
|
||||||
|
super('opencode', 'OpenCode', false);
|
||||||
|
this.configDir = '.opencode';
|
||||||
|
this.commandsDir = 'command';
|
||||||
|
this.agentsDir = 'agent';
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
|
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||||
|
|
||||||
|
const baseDir = path.join(projectDir, this.configDir);
|
||||||
|
const agentsBaseDir = path.join(baseDir, this.agentsDir, 'bmad');
|
||||||
|
const commandsBaseDir = path.join(baseDir, this.commandsDir, 'bmad');
|
||||||
|
|
||||||
|
await this.ensureDir(agentsBaseDir);
|
||||||
|
await this.ensureDir(commandsBaseDir);
|
||||||
|
|
||||||
|
// Install primary agents
|
||||||
|
const agents = await getAgentsFromBmad(bmadDir, options.selectedModules || []);
|
||||||
|
const modules = new Set(agents.map((agent) => agent.module));
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
await this.ensureDir(path.join(agentsBaseDir, module));
|
||||||
|
}
|
||||||
|
|
||||||
|
let agentCount = 0;
|
||||||
|
for (const agent of agents) {
|
||||||
|
const processed = await this.readAndProcess(agent.path, {
|
||||||
|
module: agent.module,
|
||||||
|
name: agent.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agentContent = this.createAgentContent(processed, agent);
|
||||||
|
const targetPath = path.join(agentsBaseDir, agent.module, `${agent.name}.md`);
|
||||||
|
await this.writeFile(targetPath, agentContent);
|
||||||
|
agentCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install workflow commands
|
||||||
|
const workflowGenerator = new WorkflowCommandGenerator();
|
||||||
|
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||||
|
|
||||||
|
let workflowCommandCount = 0;
|
||||||
|
for (const artifact of workflowArtifacts) {
|
||||||
|
// Workflow artifacts already have proper frontmatter from the template
|
||||||
|
// No need to wrap with additional frontmatter
|
||||||
|
const commandContent = artifact.content;
|
||||||
|
const targetPath = path.join(commandsBaseDir, artifact.relativePath);
|
||||||
|
await this.writeFile(targetPath, commandContent);
|
||||||
|
workflowCommandCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||||
|
console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/bmad/`));
|
||||||
|
if (workflowCommandCount > 0) {
|
||||||
|
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated to .opencode/command/bmad/`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
agents: agentCount,
|
||||||
|
workflows: workflowCommandCount,
|
||||||
|
workflowCounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAndProcess(filePath, metadata) {
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
return this.processContent(content, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAgentContent(content, metadata) {
|
||||||
|
const { frontmatter = {}, body } = this.parseFrontmatter(content);
|
||||||
|
|
||||||
|
frontmatter.description =
|
||||||
|
frontmatter.description && String(frontmatter.description).trim().length > 0
|
||||||
|
? frontmatter.description
|
||||||
|
: `BMAD ${metadata.module} agent: ${metadata.name}`;
|
||||||
|
|
||||||
|
// OpenCode agents use: 'primary' mode for main agents
|
||||||
|
frontmatter.mode = 'primary';
|
||||||
|
|
||||||
|
const frontmatterString = this.stringifyFrontmatter(frontmatter);
|
||||||
|
|
||||||
|
return `${frontmatterString}\n${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFrontmatter(content) {
|
||||||
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
||||||
|
if (!match) {
|
||||||
|
return { data: {}, body: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = content.slice(match[0].length);
|
||||||
|
|
||||||
|
let frontmatter = {};
|
||||||
|
try {
|
||||||
|
frontmatter = yaml.load(match[1]) || {};
|
||||||
|
} catch {
|
||||||
|
frontmatter = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frontmatter, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
stringifyFrontmatter(frontmatter) {
|
||||||
|
const yamlText = yaml
|
||||||
|
.dump(frontmatter, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: false,
|
||||||
|
})
|
||||||
|
.trimEnd();
|
||||||
|
|
||||||
|
return `---\n${yamlText}\n---`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { OpenCodeSetup };
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
# {{name}}
|
# {{name}}
|
||||||
|
|
||||||
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
|
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "AI coding assistant"
|
description: "AI coding assistant"
|
||||||
|
|
||||||
|
opencode:
|
||||||
|
name: "OpenCode"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "OpenCode terminal coding assistant"
|
||||||
|
|
||||||
auggie:
|
auggie:
|
||||||
name: "Auggie"
|
name: "Auggie"
|
||||||
preferred: false
|
preferred: false
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue