From 3bbfc89c27d19fef86f21508f97bbacb163b4f3d Mon Sep 17 00:00:00 2001 From: Tim Graepel Date: Thu, 26 Feb 2026 00:45:01 +0100 Subject: [PATCH] feat: add IBM Bob platform support (#1768) Add IBM Bob (agentic IDE) as a supported IDE platform with custom installer: - Create custom Bob installer (tools/cli/installers/lib/ide/bob.js) - Add .bobmodes to .gitignore - Add platform config to tools/platform-codes.yaml - Register Bob in IDE manager's custom installer list Bob uses a custom installer (like Kilo) that creates a .bobmodes file to register custom modes and writes workflows to .bob/workflows/ Fixes #1768 --- .gitignore | 2 + tools/cli/installers/lib/ide/bob.js | 271 ++++++++++++++++++++++++ tools/cli/installers/lib/ide/manager.js | 2 +- tools/platform-codes.yaml | 6 + 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 tools/cli/installers/lib/ide/bob.js diff --git a/.gitignore b/.gitignore index a1229c93d..dcdf6b7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,8 @@ _bmad-output .opencode .qwen .rovodev +.bobmodes +.bob .kilocodemodes .claude/commands .codex diff --git a/tools/cli/installers/lib/ide/bob.js b/tools/cli/installers/lib/ide/bob.js new file mode 100644 index 000000000..3eabbfbe5 --- /dev/null +++ b/tools/cli/installers/lib/ide/bob.js @@ -0,0 +1,271 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const yaml = require('yaml'); +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'); + +/** + * IBM Bob IDE setup handler + * Creates custom modes in .bobmodes file (similar to Kilo) + */ +class BobSetup extends BaseIdeSetup { + constructor() { + super('bob', 'IBM Bob'); + this.configFile = '.bobmodes'; + } + + /** + * Setup IBM Bob IDE configuration + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + */ + 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); + + // Load existing config (may contain non-BMAD modes and other settings) + const bobModesPath = path.join(projectDir, this.configFile); + let config = {}; + + if (await this.pathExists(bobModesPath)) { + const existingContent = await this.readFile(bobModesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + // If parsing fails, start fresh but warn user + await prompts.log.warn('Warning: Could not parse existing .bobmodes, starting fresh'); + config = {}; + } + } + + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; + } + + // Generate agent launchers + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + + // Create mode objects and add to config + let addedCount = 0; + + for (const artifact of agentArtifacts) { + const modeObject = await this.createModeObject(artifact, projectDir); + config.customModes.push(modeObject); + addedCount++; + } + + // Write .bobmodes file with proper YAML structure + const finalContent = yaml.stringify(config, { lineWidth: 0 }); + await this.writeFile(bobModesPath, finalContent); + + // Generate workflow commands + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + + // Write to .bob/workflows/ directory + const workflowsDir = path.join(projectDir, '.bob', 'workflows'); + await this.ensureDir(workflowsDir); + + // Clear old BMAD workflows before writing new ones + await this.clearBmadWorkflows(workflowsDir); + + // Write workflow files + const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts); + + // Generate task and tool commands + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + + // Write task/tool files to workflows directory (same location as workflows) + await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts); + const taskCount = taskToolCounts.tasks || 0; + const toolCount = taskToolCounts.tools || 0; + + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, + ); + } + + return { + success: true, + modes: addedCount, + workflows: workflowCount, + tasks: taskCount, + tools: toolCount, + }; + } + + /** + * Create a mode object for an agent + * @param {Object} artifact - Agent artifact + * @param {string} projectDir - Project directory + * @returns {Object} Mode object for YAML serialization + */ + async createModeObject(artifact, projectDir) { + // Extract metadata from launcher content + const titleMatch = artifact.content.match(/title="([^"]+)"/); + const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); + + const iconMatch = artifact.content.match(/icon="([^"]+)"/); + const icon = iconMatch ? iconMatch[1] : '🤖'; + + const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); + const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; + + // Get the activation header from central template (trim to avoid YAML formatting issues) + const activationHeader = (await this.getAgentCommandHeader()).trim(); + + const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); + const roleDefinition = roleDefinitionMatch + ? roleDefinitionMatch[1] + : `You are a ${title} specializing in ${title.toLowerCase()} tasks.`; + + // Get relative path + const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); + + // Build mode object (Bob uses same schema as Kilo/Roo) + return { + slug: `bmad-${artifact.module}-${artifact.name}`, + name: `${icon} ${title}`, + roleDefinition: roleDefinition, + whenToUse: whenToUse, + customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`, + groups: ['read', 'edit', 'browser', 'command', 'mcp'], + }; + } + + /** + * Format name as title + */ + formatTitle(name) { + return name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + /** + * Clear old BMAD workflow files from workflows directory + * @param {string} workflowsDir - Workflows directory path + */ + async clearBmadWorkflows(workflowsDir) { + const fs = require('fs-extra'); + if (!(await fs.pathExists(workflowsDir))) return; + + const entries = await fs.readdir(workflowsDir); + for (const entry of entries) { + if (entry.startsWith('bmad-') && entry.endsWith('.md')) { + await fs.remove(path.join(workflowsDir, entry)); + } + } + } + + /** + * Cleanup IBM Bob configuration + */ + async cleanup(projectDir, options = {}) { + const fs = require('fs-extra'); + const bobModesPath = path.join(projectDir, this.configFile); + + if (await fs.pathExists(bobModesPath)) { + const content = await fs.readFile(bobModesPath, 'utf8'); + + try { + const config = yaml.parse(content) || {}; + + if (Array.isArray(config.customModes)) { + const originalCount = config.customModes.length; + // Remove BMAD modes only (keep non-BMAD modes) + config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-')); + const removedCount = originalCount - config.customModes.length; + + if (removedCount > 0) { + await fs.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 })); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bobmodes`); + } + } + } catch { + // If parsing fails, leave file as-is + if (!options.silent) await prompts.log.warn('Warning: Could not parse .bobmodes for cleanup'); + } + } + + // Clean up workflow files + const workflowsDir = path.join(projectDir, '.bob', 'workflows'); + await this.clearBmadWorkflows(workflowsDir); + } + + /** + * Install a custom agent launcher for Bob + * @param {string} projectDir - Project directory + * @param {string} agentName - Agent name (e.g., "fred-commit-poet") + * @param {string} agentPath - Path to compiled agent (relative to project root) + * @param {Object} metadata - Agent metadata + * @returns {Object} Installation result + */ + async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { + const bobmodesPath = path.join(projectDir, this.configFile); + let config = {}; + + // Read existing .bobmodes file + if (await this.pathExists(bobmodesPath)) { + const existingContent = await this.readFile(bobmodesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + config = {}; + } + } + + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; + } + + // Create custom agent mode object + const slug = `bmad-custom-${agentName.toLowerCase()}`; + + // Check if mode already exists + if (config.customModes.some((mode) => mode.slug === slug)) { + return { + ide: 'bob', + path: this.configFile, + command: agentName, + type: 'custom-agent-launcher', + alreadyExists: true, + }; + } + + // Add custom mode object + config.customModes.push({ + slug: slug, + name: `BMAD Custom: ${agentName}`, + description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`, + prompt: `@${agentPath}\n`, + always: false, + permissions: 'all', + }); + + // Write .bobmodes file with proper YAML structure + await this.writeFile(bobmodesPath, yaml.stringify(config, { lineWidth: 0 })); + + return { + ide: 'bob', + path: this.configFile, + command: slug, + type: 'custom-agent-launcher', + }; + } +} + +module.exports = { BobSetup }; + +// Made with Bob diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9e286fdd3..94b74ba9c 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -61,7 +61,7 @@ class IdeManager { */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; + const customFiles = ['bob.js', 'codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); diff --git a/tools/platform-codes.yaml b/tools/platform-codes.yaml index 97846a9bd..da4ad3126 100644 --- a/tools/platform-codes.yaml +++ b/tools/platform-codes.yaml @@ -49,6 +49,12 @@ platforms: category: cli description: "AI development tool" + bob: + name: "IBM Bob" + preferred: false + category: ide + description: "IBM's agentic IDE for AI-powered development" + roo: name: "Roo Cline" preferred: false