Compare commits
3 Commits
fc4c711113
...
411219e2ee
| Author | SHA1 | Date |
|---|---|---|
|
|
411219e2ee | |
|
|
d8746d5b6c | |
|
|
da227bdafb |
|
|
@ -54,6 +54,7 @@ _bmad-output
|
||||||
.opencode
|
.opencode
|
||||||
.qwen
|
.qwen
|
||||||
.rovodev
|
.rovodev
|
||||||
|
.bob
|
||||||
.kilocodemodes
|
.kilocodemodes
|
||||||
.claude/commands
|
.claude/commands
|
||||||
.codex
|
.codex
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export default [
|
||||||
'website/**',
|
'website/**',
|
||||||
// Gitignored patterns
|
// Gitignored patterns
|
||||||
'z*/**', // z-samples, z1, z2, etc.
|
'z*/**', // z-samples, z1, z2, etc.
|
||||||
|
'.bob/**',
|
||||||
'.claude/**',
|
'.claude/**',
|
||||||
'.codex/**',
|
'.codex/**',
|
||||||
'.github/chatmodes/**',
|
'.github/chatmodes/**',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
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 .bob/custom_modes.yaml file
|
||||||
|
*/
|
||||||
|
class BobSetup extends BaseIdeSetup {
|
||||||
|
constructor() {
|
||||||
|
super('bob', 'IBM Bob');
|
||||||
|
this.configFile = '.bob/custom_modes.yaml';
|
||||||
|
this.detectionPaths = ['.bob'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 .bob/custom_modes.yaml, 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 .bob/custom_modes.yaml file with proper YAML structure
|
||||||
|
const finalContent = yaml.stringify(config, { lineWidth: 0 });
|
||||||
|
const workflowsDir = path.join(projectDir, '.bob', 'workflows');
|
||||||
|
|
||||||
|
let workflowCount = 0;
|
||||||
|
let taskCount = 0;
|
||||||
|
let toolCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
await this.ensureDir(workflowsDir);
|
||||||
|
|
||||||
|
// Clear old BMAD workflows before writing new ones
|
||||||
|
await this.clearBmadWorkflows(workflowsDir);
|
||||||
|
|
||||||
|
// Write workflow files
|
||||||
|
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);
|
||||||
|
taskCount = taskToolCounts.tasks || 0;
|
||||||
|
toolCount = taskToolCounts.tools || 0;
|
||||||
|
} catch (error) {
|
||||||
|
// Roll back partial writes to avoid inconsistent state
|
||||||
|
try {
|
||||||
|
await fs.remove(bobModesPath);
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
await this.clearBmadWorkflows(workflowsDir);
|
||||||
|
throw new Error(`Failed to write Bob configuration: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 title and icon from the compiled agent file's <agent> XML tag
|
||||||
|
// artifact.content is the launcher template which does NOT contain these attributes
|
||||||
|
let title = this.formatTitle(artifact.name);
|
||||||
|
let icon = '🤖';
|
||||||
|
|
||||||
|
if (artifact.sourcePath && (await this.pathExists(artifact.sourcePath))) {
|
||||||
|
const agentContent = await this.readFile(artifact.sourcePath);
|
||||||
|
const titleMatch = agentContent.match(/<agent[^>]*\stitle="([^"]+)"/);
|
||||||
|
if (titleMatch) title = titleMatch[1];
|
||||||
|
const iconMatch = agentContent.match(/<agent[^>]*\sicon="([^"]+)"/);
|
||||||
|
if (iconMatch) icon = iconMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const whenToUse = `Use for ${title} tasks`;
|
||||||
|
|
||||||
|
// Get the activation header from central template (trim to avoid YAML formatting issues)
|
||||||
|
const activationHeader = (await this.getAgentCommandHeader()).trim();
|
||||||
|
|
||||||
|
const roleDefinition = `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
|
||||||
|
|
||||||
|
// Get relative path (fall back to artifact name if sourcePath unavailable)
|
||||||
|
const relativePath = artifact.sourcePath
|
||||||
|
? path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/')
|
||||||
|
: `${this.bmadFolderName}/agents/${artifact.name}.md`;
|
||||||
|
|
||||||
|
// 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 agent definition from ${relativePath}. Start activation to assume this persona. Follow the startup section instructions. Stay in this mode until told to exit.\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) {
|
||||||
|
if (!(await this.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 bobModesPath = path.join(projectDir, this.configFile);
|
||||||
|
|
||||||
|
if (await this.pathExists(bobModesPath)) {
|
||||||
|
const content = await this.readFile(bobModesPath);
|
||||||
|
|
||||||
|
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 this.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 }));
|
||||||
|
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bob/custom_modes.yaml`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, leave file as-is
|
||||||
|
if (!options.silent) await prompts.log.warn('Warning: Could not parse .bob/custom_modes.yaml 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 .bob/custom_modes.yaml 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
|
||||||
|
const title = metadata?.title || `BMAD Custom: ${agentName}`;
|
||||||
|
const icon = metadata?.icon || '🤖';
|
||||||
|
const activationHeader = (await this.getAgentCommandHeader()).trim();
|
||||||
|
config.customModes.push({
|
||||||
|
slug: slug,
|
||||||
|
name: `${icon} ${title}`,
|
||||||
|
roleDefinition: `You are a custom BMAD agent "${agentName}". Follow the persona and instructions from the agent file.`,
|
||||||
|
whenToUse: `Use for custom BMAD agent "${agentName}" tasks`,
|
||||||
|
customInstructions: `${activationHeader} Read the full agent definition from ${agentPath}. Start activation to assume this persona. Follow the startup section instructions. Stay in this mode until told to exit.\n`,
|
||||||
|
groups: ['read', 'edit', 'browser', 'command', 'mcp'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write .bob/custom_modes.yaml 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
|
||||||
|
|
@ -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, rovodev.js) - for platforms with unique installation logic
|
* 1. Custom installer files (bob.js, 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, rovodev.js)
|
* 1. Load custom installer files first (bob.js, 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', 'rovodev.js'];
|
const customFiles = ['bob.js', '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);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ platforms:
|
||||||
target_dir: .augment/commands
|
target_dir: .augment/commands
|
||||||
template_type: default
|
template_type: default
|
||||||
|
|
||||||
|
bob:
|
||||||
|
name: "IBM Bob"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "IBM's agentic IDE for AI-powered development"
|
||||||
|
# No installer config - uses custom bob.js (creates .bob/custom_modes.yaml)
|
||||||
|
|
||||||
claude-code:
|
claude-code:
|
||||||
name: "Claude Code"
|
name: "Claude Code"
|
||||||
preferred: true
|
preferred: true
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ platforms:
|
||||||
category: cli
|
category: cli
|
||||||
description: "AI development tool"
|
description: "AI development tool"
|
||||||
|
|
||||||
|
bob:
|
||||||
|
name: "IBM Bob"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "IBM's agentic IDE for AI-powered development"
|
||||||
|
|
||||||
roo:
|
roo:
|
||||||
name: "Roo Cline"
|
name: "Roo Cline"
|
||||||
preferred: false
|
preferred: false
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue