From db3456afb62f9debf1fb4cab153409382a732c91 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Wed, 18 Feb 2026 07:15:03 -0700 Subject: [PATCH 1/2] fix(installer): add custom Rovo Dev installer with prompts.yml generation Rovo Dev CLI requires a .rovodev/prompts.yml manifest to register prompts for /prompts access. The config-driven installer was writing .md files but never generating this manifest, so /prompts showed nothing. - Create custom rovodev.js installer extending BaseIdeSetup - Generate prompts.yml indexing all written workflow files - Merge with existing user entries (only touch bmad- prefixed entries) - Remove stale rovo entry from tools/platform-codes.yaml Closes #1466 --- tools/cli/installers/lib/ide/manager.js | 4 +- .../installers/lib/ide/platform-codes.yaml | 4 +- tools/cli/installers/lib/ide/rovodev.js | 272 ++++++++++++++++++ tools/platform-codes.yaml | 6 - 4 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 tools/cli/installers/lib/ide/rovodev.js diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index f83db4592..271d25e99 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts'); * Dynamically discovers and loads IDE handlers * * Loading strategy: - * 1. Custom installer files (codex.js, github-copilot.js, kilo.js) - for platforms with unique installation logic + * 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { @@ -61,7 +61,7 @@ class IdeManager { */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js']; + const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 7c2dde2cb..3d80f10bb 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -153,9 +153,7 @@ platforms: preferred: false category: ide description: "Atlassian's Rovo development environment" - installer: - target_dir: .rovodev/workflows - template_type: rovodev + # No installer config - uses custom rovodev.js (generates prompts.yml manifest) trae: name: "Trae" diff --git a/tools/cli/installers/lib/ide/rovodev.js b/tools/cli/installers/lib/ide/rovodev.js new file mode 100644 index 000000000..14d48a69b --- /dev/null +++ b/tools/cli/installers/lib/ide/rovodev.js @@ -0,0 +1,272 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('yaml'); +const { BaseIdeSetup } = require('./_base-ide'); +const prompts = require('../../../lib/prompts'); +const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); + +/** + * Rovo Dev IDE setup handler + * + * Custom installer that writes .md workflow files to .rovodev/workflows/ + * and generates .rovodev/prompts.yml to register them with Rovo Dev's /prompts feature. + * + * prompts.yml format (per Rovo Dev docs): + * prompts: + * - name: bmad-bmm-create-prd + * description: "PRD workflow..." + * content_file: workflows/bmad-bmm-create-prd.md + */ +class RovoDevSetup extends BaseIdeSetup { + constructor() { + super('rovo-dev', 'Rovo Dev', false); + this.rovoDir = '.rovodev'; + this.workflowsDir = 'workflows'; + this.promptsFile = 'prompts.yml'; + this.detectionPaths = ['.rovodev/workflows', '.rovodev/prompts.yml']; + } + + /** + * Setup Rovo Dev configuration + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + * @returns {Promise} Setup result with { success, results: { agents, workflows, tasks, tools } } + */ + async setup(projectDir, bmadDir, options = {}) { + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); + + // Clean up any old BMAD installation first + await this.cleanup(projectDir, options); + + const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir); + await this.ensureDir(workflowsPath); + + const selectedModules = options.selectedModules || []; + const writtenFiles = []; + + // Generate and write agent launchers + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); + const agentCount = await agentGen.writeDashArtifacts(workflowsPath, agentArtifacts); + + // Track written agent files for prompts.yml + for (const artifact of agentArtifacts) { + if (artifact.type === 'agent-launcher') { + const { toDashPath } = require('./shared/path-utils'); + const flatName = toDashPath(artifact.relativePath); + writtenFiles.push({ + name: path.basename(flatName, '.md'), + description: artifact.description || `${artifact.name} agent`, + contentFile: `${this.workflowsDir}/${flatName}`, + }); + } + } + + // Generate and write workflow commands + const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); + const workflowCount = await workflowGen.writeDashArtifacts(workflowsPath, workflowArtifacts); + + // Track written workflow files for prompts.yml + for (const artifact of workflowArtifacts) { + if (artifact.type === 'workflow-command') { + const { toDashPath } = require('./shared/path-utils'); + const flatName = toDashPath(artifact.relativePath); + writtenFiles.push({ + name: path.basename(flatName, '.md'), + description: artifact.description || `${artifact.name} workflow`, + contentFile: `${this.workflowsDir}/${flatName}`, + }); + } + } + + // Generate and write task/tool commands + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + await taskToolGen.writeDashArtifacts(workflowsPath, taskToolArtifacts); + const taskCount = taskToolCounts.tasks || 0; + const toolCount = taskToolCounts.tools || 0; + + // Track written task/tool files for prompts.yml + for (const artifact of taskToolArtifacts) { + if (artifact.type === 'task' || artifact.type === 'tool') { + const { toDashPath } = require('./shared/path-utils'); + const flatName = toDashPath(artifact.relativePath); + writtenFiles.push({ + name: path.basename(flatName, '.md'), + description: artifact.description || `${artifact.name} ${artifact.type}`, + contentFile: `${this.workflowsDir}/${flatName}`, + }); + } + } + + // Generate prompts.yml manifest + await this.generatePromptsYml(projectDir, writtenFiles); + + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${agentCount} agents, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools`, + ); + } + + return { + success: true, + results: { + agents: agentCount, + workflows: workflowCount, + tasks: taskCount, + tools: toolCount, + }, + }; + } + + /** + * Generate .rovodev/prompts.yml manifest + * Merges with existing user entries -- strips entries with names starting 'bmad-', + * appends new BMAD entries, and writes back. + * + * @param {string} projectDir - Project directory + * @param {Array} writtenFiles - Array of { name, description, contentFile } + */ + async generatePromptsYml(projectDir, writtenFiles) { + const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile); + let existingPrompts = []; + + // Read existing prompts.yml and preserve non-BMAD entries + if (await this.pathExists(promptsPath)) { + try { + const content = await this.readFile(promptsPath); + const parsed = yaml.parse(content); + if (parsed && Array.isArray(parsed.prompts)) { + // Keep only non-BMAD entries (entries whose name does NOT start with bmad-) + existingPrompts = parsed.prompts.filter((entry) => !entry.name || !entry.name.startsWith('bmad-')); + } + } catch { + // If parsing fails, start fresh but preserve file safety + existingPrompts = []; + } + } + + // Build new BMAD entries + const bmadEntries = writtenFiles.map((file) => ({ + name: file.name, + description: file.description, + content_file: file.contentFile, + })); + + // Merge: user entries first, then BMAD entries + const allPrompts = [...existingPrompts, ...bmadEntries]; + + const config = { prompts: allPrompts }; + const yamlContent = yaml.stringify(config, { lineWidth: 0 }); + await this.writeFile(promptsPath, yamlContent); + } + + /** + * Cleanup Rovo Dev configuration + * Removes bmad-* files from .rovodev/workflows/ and strips BMAD entries from prompts.yml + * @param {string} projectDir - Project directory + * @param {Object} options - Cleanup options + */ + async cleanup(projectDir, options = {}) { + const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir); + + // Remove bmad-* workflow files + if (await fs.pathExists(workflowsPath)) { + const entries = await fs.readdir(workflowsPath); + for (const entry of entries) { + if (entry.startsWith('bmad-') && entry.endsWith('.md')) { + await fs.remove(path.join(workflowsPath, entry)); + } + } + } + + // Clean BMAD entries from prompts.yml (preserve user entries) + const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile); + if (await fs.pathExists(promptsPath)) { + try { + const content = await fs.readFile(promptsPath, 'utf8'); + const parsed = yaml.parse(content) || {}; + + if (Array.isArray(parsed.prompts)) { + const originalCount = parsed.prompts.length; + parsed.prompts = parsed.prompts.filter((entry) => !entry.name || !entry.name.startsWith('bmad-')); + const removedCount = originalCount - parsed.prompts.length; + + if (removedCount > 0) { + if (parsed.prompts.length === 0) { + // If no entries remain, remove the file entirely + await fs.remove(promptsPath); + } else { + await fs.writeFile(promptsPath, yaml.stringify(parsed, { lineWidth: 0 })); + } + if (!options.silent) { + await prompts.log.message(`Removed ${removedCount} BMAD entries from ${this.promptsFile}`); + } + } + } + } catch { + // If parsing fails, leave file as-is + if (!options.silent) { + await prompts.log.warn(`Warning: Could not parse ${this.promptsFile} for cleanup`); + } + } + } + + // Remove empty .rovodev directories + if (await fs.pathExists(workflowsPath)) { + const remaining = await fs.readdir(workflowsPath); + if (remaining.length === 0) { + await fs.remove(workflowsPath); + } + } + + const rovoDirPath = path.join(projectDir, this.rovoDir); + if (await fs.pathExists(rovoDirPath)) { + const remaining = await fs.readdir(rovoDirPath); + if (remaining.length === 0) { + await fs.remove(rovoDirPath); + } + } + } + + /** + * Detect whether Rovo Dev configuration exists in the project + * Checks for .rovodev/ dir with bmad files or bmad entries in prompts.yml + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir); + + // Check for bmad files in workflows dir + if (await fs.pathExists(workflowsPath)) { + const entries = await fs.readdir(workflowsPath); + if (entries.some((entry) => entry.startsWith('bmad-'))) { + return true; + } + } + + // Check for bmad entries in prompts.yml + const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile); + if (await fs.pathExists(promptsPath)) { + try { + const content = await fs.readFile(promptsPath, 'utf8'); + const parsed = yaml.parse(content); + if (parsed && Array.isArray(parsed.prompts)) { + return parsed.prompts.some((entry) => entry.name && entry.name.startsWith('bmad-')); + } + } catch { + // If parsing fails, check raw content + return false; + } + } + + return false; + } +} + +module.exports = { RovoDevSetup }; diff --git a/tools/platform-codes.yaml b/tools/platform-codes.yaml index d75e31fee..a747a7910 100644 --- a/tools/platform-codes.yaml +++ b/tools/platform-codes.yaml @@ -55,12 +55,6 @@ platforms: category: ide description: "Enhanced Cline fork" - rovo: - name: "Rovo" - preferred: false - category: ide - description: "Atlassian's AI coding assistant" - rovo-dev: name: "Rovo Dev" preferred: false From 3e09eecfc398f19c73399482020ef6a7575e5122 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Wed, 18 Feb 2026 08:01:57 -0700 Subject: [PATCH 2/2] fix(installer): prefix prompts.yml descriptions with entry name The /prompts list in Rovo Dev only shows descriptions, making it hard to identify entries. Prefix each description with the bmad entry name so users see e.g. "bmad-bmm-create-prd - PRD workflow..." instead of just the description text. --- tools/cli/installers/lib/ide/rovodev.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cli/installers/lib/ide/rovodev.js b/tools/cli/installers/lib/ide/rovodev.js index 14d48a69b..79b6dde48 100644 --- a/tools/cli/installers/lib/ide/rovodev.js +++ b/tools/cli/installers/lib/ide/rovodev.js @@ -150,10 +150,10 @@ class RovoDevSetup extends BaseIdeSetup { } } - // Build new BMAD entries + // Build new BMAD entries (prefix description with name so /prompts list is scannable) const bmadEntries = writtenFiles.map((file) => ({ name: file.name, - description: file.description, + description: `${file.name} - ${file.description}`, content_file: file.contentFile, }));