From e72b82ed3101358479a5c319c881901b7adcb561 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Fri, 20 Feb 2026 19:31:45 -0700 Subject: [PATCH] fix(installer): add custom Rovo Dev installer with prompts.yml generation (#1701) * 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 * 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. * refactor(installer): address review findings in Rovo Dev installer - Hoist toDashPath import to module top level - Extract _collectPromptEntries helper replacing 3 duplicated loops - Remove unused detectionPaths (detect() is overridden) - Guard generatePromptsYml when writtenFiles is empty - Align cleanup() with detect() predicate (remove any bmad-*, not just .md) - Use BaseIdeSetup abstractions (this.pathExists/readFile/writeFile) in cleanup() - Update loadHandlers() JSDoc to include rovodev.js --------- Co-authored-by: Brian --- tools/cli/installers/lib/ide/manager.js | 6 +- .../installers/lib/ide/platform-codes.yaml | 4 +- tools/cli/installers/lib/ide/rovodev.js | 257 ++++++++++++++++++ tools/platform-codes.yaml | 6 - 4 files changed, 261 insertions(+), 12 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..9b8df1597 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 { @@ -44,7 +44,7 @@ class IdeManager { /** * Dynamically load all IDE handlers - * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js) + * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js) * 2. Load config-driven handlers from platform-codes.yaml */ async loadHandlers() { @@ -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 f51add17b..b9db95733 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..da3c4809d --- /dev/null +++ b/tools/cli/installers/lib/ide/rovodev.js @@ -0,0 +1,257 @@ +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'); +const { toDashPath } = require('./shared/path-utils'); + +/** + * 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'; + } + + /** + * 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); + this._collectPromptEntries(writtenFiles, agentArtifacts, ['agent-launcher'], 'agent'); + + // 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); + this._collectPromptEntries(writtenFiles, workflowArtifacts, ['workflow-command'], 'workflow'); + + // 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; + this._collectPromptEntries(writtenFiles, taskToolArtifacts, ['task', 'tool']); + + // Generate prompts.yml manifest (only if we have entries to write) + if (writtenFiles.length > 0) { + 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, + }, + }; + } + + /** + * Collect prompt entries from artifacts into writtenFiles array + * @param {Array} writtenFiles - Target array to push entries into + * @param {Array} artifacts - Artifacts from a generator's collect method + * @param {string[]} acceptedTypes - Artifact types to include (e.g., ['agent-launcher']) + * @param {string} [fallbackSuffix] - Suffix for fallback description; defaults to artifact.type + */ + _collectPromptEntries(writtenFiles, artifacts, acceptedTypes, fallbackSuffix) { + for (const artifact of artifacts) { + if (!acceptedTypes.includes(artifact.type)) continue; + const flatName = toDashPath(artifact.relativePath); + writtenFiles.push({ + name: path.basename(flatName, '.md'), + description: artifact.description || `${artifact.name} ${fallbackSuffix || artifact.type}`, + contentFile: `${this.workflowsDir}/${flatName}`, + }); + } + } + + /** + * 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 (prefix description with name so /prompts list is scannable) + const bmadEntries = writtenFiles.map((file) => ({ + name: file.name, + description: `${file.name} - ${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 all bmad-* entries from workflows dir (aligned with detect() predicate) + if (await this.pathExists(workflowsPath)) { + const entries = await fs.readdir(workflowsPath); + for (const entry of entries) { + if (entry.startsWith('bmad-')) { + 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 this.pathExists(promptsPath)) { + try { + const content = await this.readFile(promptsPath); + 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 this.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 this.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 this.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 9948dfd9f..bacdbc894 100644 --- a/tools/platform-codes.yaml +++ b/tools/platform-codes.yaml @@ -49,12 +49,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