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 <bmadcode@gmail.com>
This commit is contained in:
parent
d0ecfc0dd9
commit
e72b82ed31
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
||||||
* Dynamically discovers and loads IDE handlers
|
* Dynamically discovers and loads IDE handlers
|
||||||
*
|
*
|
||||||
* Loading strategy:
|
* 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
|
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||||
*/
|
*/
|
||||||
class IdeManager {
|
class IdeManager {
|
||||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically load all IDE handlers
|
* 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
|
* 2. Load config-driven handlers from platform-codes.yaml
|
||||||
*/
|
*/
|
||||||
async loadHandlers() {
|
async loadHandlers() {
|
||||||
|
|
@ -61,7 +61,7 @@ class IdeManager {
|
||||||
*/
|
*/
|
||||||
async loadCustomInstallerFiles() {
|
async loadCustomInstallerFiles() {
|
||||||
const ideDir = __dirname;
|
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) {
|
for (const file of customFiles) {
|
||||||
const filePath = path.join(ideDir, file);
|
const filePath = path.join(ideDir, file);
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,7 @@ platforms:
|
||||||
preferred: false
|
preferred: false
|
||||||
category: ide
|
category: ide
|
||||||
description: "Atlassian's Rovo development environment"
|
description: "Atlassian's Rovo development environment"
|
||||||
installer:
|
# No installer config - uses custom rovodev.js (generates prompts.yml manifest)
|
||||||
target_dir: .rovodev/workflows
|
|
||||||
template_type: rovodev
|
|
||||||
|
|
||||||
trae:
|
trae:
|
||||||
name: "Trae"
|
name: "Trae"
|
||||||
|
|
|
||||||
|
|
@ -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<Object>} 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<Object>} 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 };
|
||||||
|
|
@ -49,12 +49,6 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "Enhanced Cline fork"
|
description: "Enhanced Cline fork"
|
||||||
|
|
||||||
rovo:
|
|
||||||
name: "Rovo"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Atlassian's AI coding assistant"
|
|
||||||
|
|
||||||
rovo-dev:
|
rovo-dev:
|
||||||
name: "Rovo Dev"
|
name: "Rovo Dev"
|
||||||
preferred: false
|
preferred: false
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue