diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfd939f..9bd0ce08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Codex Installer + +- Codex installer uses custom prompts in `.codex/prompts/`, instead of `AGENTS.md` + ## [6.0.0-alpha.0] **Release: September 28, 2025** diff --git a/docs/ide-info/claude-code.md b/docs/ide-info/claude-code.md index 17a1fd77..74981b6e 100644 --- a/docs/ide-info/claude-code.md +++ b/docs/ide-info/claude-code.md @@ -13,13 +13,13 @@ BMAD agents are installed as slash commands in `.claude/commands/bmad/`. ### Examples ``` -/bmad-dev - Activate development agent -/bmad-architect - Activate architect agent -/bmad-task-setup - Execute setup task +/bmad:bmm:agents:dev - Activate development agent +/bmad:bmm:agents:architect - Activate architect agent +/bmad:bmm:workflows:dev-story - Execute dev-story workflow ``` ### Notes - Commands are autocompleted when you type `/` - Agent remains active for the conversation -- Start new conversation to switch agents +- Start a new conversation to switch agents diff --git a/docs/ide-info/codex.md b/docs/ide-info/codex.md index 88b3a642..5e1c05d4 100644 --- a/docs/ide-info/codex.md +++ b/docs/ide-info/codex.md @@ -2,31 +2,20 @@ ## Activating Agents -BMAD agents are documented in `AGENTS.md` file in project root. - -### CLI Mode - -1. **Reference Agent**: Type `@{agent-name}` in prompt -2. **Execute Task**: Type `@task-{task-name}` -3. **Active Session**: Agent remains active for conversation - -### Web Mode - -1. **Navigate**: Go to Agents section in web interface -2. **Select Agent**: Click to activate agent persona -3. **Session**: Agent active for browser session +BMAD agents, tasks and workflows are installed as custom prompts in +`$CODEX_HOME/prompts/bmad-*.md` files. If `CODEX_HOME` is not set, it +defaults to `$HOME/.codex/`. ### Examples ``` -@dev - Activate development agent -@architect - Activate architect agent -@task-setup - Execute setup task +/bmad-bmm-agents-dev - Activate development agent +/bmad-bmm-agents-architect - Activate architect agent +/bmad-bmm-workflows-dev-story - Execute dev-story workflow ``` ### Notes -- All agents documented in AGENTS.md -- CLI: Reference with @ syntax -- Web: Use interface to select -- One agent active at a time +Prompts are autocompleted when you type / +Agent remains active for the conversation +Start a new conversation to switch agents diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js index 3d2a7b65..eb05d634 100644 --- a/tools/cli/installers/lib/ide/claude-code.js +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -3,6 +3,13 @@ const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { WorkflowCommandGenerator } = require('./workflow-command-generator'); +const { + loadModuleInjectionConfig, + shouldApplyInjection, + filterAgentInstructions, + resolveSubagentFiles, +} = require('./shared/module-injections'); +const { getAgentsFromBmad, getTasksFromBmad, getAgentsFromDir, getTasksFromDir } = require('./shared/bmad-artifacts'); /** * Claude Code IDE setup handler @@ -94,8 +101,8 @@ class ClaudeCodeSetup extends BaseIdeSetup { // Get agents and tasks from INSTALLED bmad/ directory // Base installer has already built .md files from .agent.yaml sources - const agents = await this.getAgentsFromBmad(bmadDir, options.selectedModules || []); - const tasks = await this.getTasksFromBmad(bmadDir, options.selectedModules || []); + const agents = await getAgentsFromBmad(bmadDir, options.selectedModules || []); + const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); // Create directories for each module const modules = new Set(); @@ -186,58 +193,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { return super.processContent(content, metadata, this.projectDir); } - /** - * Get agents from installed bmad/ directory - */ - async getAgentsFromBmad(bmadDir, selectedModules) { - const fs = require('fs-extra'); - const agents = []; - - // Add core agents - if (await fs.pathExists(path.join(bmadDir, 'core', 'agents'))) { - const coreAgents = await this.getAgentsFromDir(path.join(bmadDir, 'core', 'agents'), 'core'); - agents.push(...coreAgents); - } - - // Add module agents - for (const moduleName of selectedModules) { - const agentsPath = path.join(bmadDir, moduleName, 'agents'); - - if (await fs.pathExists(agentsPath)) { - const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); - agents.push(...moduleAgents); - } - } - - return agents; - } - - /** - * Get tasks from installed bmad/ directory - */ - async getTasksFromBmad(bmadDir, selectedModules) { - const fs = require('fs-extra'); - const tasks = []; - - // Add core tasks - if (await fs.pathExists(path.join(bmadDir, 'core', 'tasks'))) { - const coreTasks = await this.getTasksFromDir(path.join(bmadDir, 'core', 'tasks'), 'core'); - tasks.push(...coreTasks); - } - - // Add module tasks - for (const moduleName of selectedModules) { - const tasksPath = path.join(bmadDir, moduleName, 'tasks'); - - if (await fs.pathExists(tasksPath)) { - const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); - tasks.push(...moduleTasks); - } - } - - return tasks; - } - /** * Get agents from source modules (not installed location) */ @@ -248,7 +203,7 @@ class ClaudeCodeSetup extends BaseIdeSetup { // Add core agents const corePath = getModulePath('core'); if (await fs.pathExists(path.join(corePath, 'agents'))) { - const coreAgents = await this.getAgentsFromDir(path.join(corePath, 'agents'), 'core'); + const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core'); agents.push(...coreAgents); } @@ -258,7 +213,7 @@ class ClaudeCodeSetup extends BaseIdeSetup { const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { - const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); + const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); agents.push(...moduleAgents); } } @@ -266,142 +221,23 @@ class ClaudeCodeSetup extends BaseIdeSetup { return agents; } - /** - * Get tasks from source modules (not installed location) - */ - async getTasksFromSource(sourceDir, selectedModules) { - const fs = require('fs-extra'); - const tasks = []; - - // Add core tasks - const corePath = getModulePath('core'); - if (await fs.pathExists(path.join(corePath, 'tasks'))) { - const coreTasks = await this.getTasksFromDir(path.join(corePath, 'tasks'), 'core'); - tasks.push(...coreTasks); - } - - // Add module tasks - for (const moduleName of selectedModules) { - const modulePath = path.join(sourceDir, moduleName); - const tasksPath = path.join(modulePath, 'tasks'); - - if (await fs.pathExists(tasksPath)) { - const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); - tasks.push(...moduleTasks); - } - } - - return tasks; - } - - /** - * Get agents from a specific directory - * When reading from bmad/, this returns built .md files - */ - async getAgentsFromDir(dirPath, moduleName) { - const fs = require('fs-extra'); - const agents = []; - - const files = await fs.readdir(dirPath); - - for (const file of files) { - // Only process .md files (base installer has already built .agent.yaml to .md) - if (file.endsWith('.md')) { - // Skip customize templates - if (file.includes('.customize.')) { - continue; - } - - const baseName = file.replace('.md', ''); - const filePath = path.join(dirPath, file); - const content = await fs.readFile(filePath, 'utf8'); - - // Skip web-only agents - if (content.includes('localskip="true"')) { - continue; - } - - agents.push({ - path: filePath, - name: baseName, - module: moduleName, - }); - } - } - - return agents; - } - - /** - * Get tasks from a specific directory - */ - async getTasksFromDir(dirPath, moduleName) { - const fs = require('fs-extra'); - const tasks = []; - - const files = await fs.readdir(dirPath); - for (const file of files) { - if (file.endsWith('.md')) { - tasks.push({ - path: path.join(dirPath, file), - name: file.replace('.md', ''), - module: moduleName, - }); - } - } - - return tasks; - } - /** * Process module injections with pre-collected configuration */ async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { - const fs = require('fs-extra'); - const yaml = require('js-yaml'); - // Get list of installed modules const modules = options.selectedModules || []; const { subagentChoices, installLocation } = preCollectedConfig; // Get the actual source directory (not the installation directory) - const sourceModulesPath = getSourcePath('modules'); - - for (const moduleName of modules) { - // Check for Claude Code sub-module injection config in SOURCE directory - const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml'); - - if (await this.exists(injectionConfigPath)) { - try { - // Load injection configuration - const configContent = await fs.readFile(injectionConfigPath, 'utf8'); - const config = yaml.load(configContent); - - // Process content injections based on user choices - if (config.injections && subagentChoices && subagentChoices.install !== 'none') { - for (const injection of config.injections) { - // Check if this injection is related to a selected subagent - if (this.shouldInject(injection, subagentChoices)) { - await this.injectContent(projectDir, injection, subagentChoices); - } - } - } - - // Copy selected subagents - if (config.subagents && subagentChoices && subagentChoices.install !== 'none') { - await this.copySelectedSubagents( - projectDir, - path.dirname(injectionConfigPath), - config.subagents, - subagentChoices, - installLocation, - ); - } - } catch (error) { - console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`)); - } - } - } + await this.processModuleInjectionsInternal({ + projectDir, + modules, + handler: 'claude-code', + subagentChoices, + installLocation, + interactive: false, + }); } /** @@ -409,77 +245,81 @@ class ClaudeCodeSetup extends BaseIdeSetup { * Looks for injections.yaml in each module's claude-code sub-module */ async processModuleInjections(projectDir, bmadDir, options) { - const fs = require('fs-extra'); - const yaml = require('js-yaml'); - const inquirer = require('inquirer'); - // Get list of installed modules const modules = options.selectedModules || []; let subagentChoices = null; let installLocation = null; // Get the actual source directory (not the installation directory) - const sourceModulesPath = getSourcePath('modules'); + const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ + projectDir, + modules, + handler: 'claude-code', + subagentChoices, + installLocation, + interactive: true, + }); + + if (updatedChoices) { + subagentChoices = updatedChoices; + } + if (updatedLocation) { + installLocation = updatedLocation; + } + } + + async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { + let choices = subagentChoices; + let location = installLocation; for (const moduleName of modules) { - // Check for Claude Code sub-module injection config in SOURCE directory - const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml'); + const configData = await loadModuleInjectionConfig(handler, moduleName); - if (await this.exists(injectionConfigPath)) { - console.log(chalk.cyan(`\nConfiguring ${moduleName} Claude Code features...`)); + if (!configData) { + continue; + } - try { - // Load injection configuration - const configContent = await fs.readFile(injectionConfigPath, 'utf8'); - const config = yaml.load(configContent); + const { config, handlerBaseDir } = configData; - // Ask about subagents if they exist and we haven't asked yet - if (config.subagents && !subagentChoices) { - subagentChoices = await this.promptSubagentInstallation(config.subagents); + if (interactive) { + console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`)); + } - if (subagentChoices.install !== 'none') { - // Ask for installation location - const locationAnswer = await inquirer.prompt([ - { - type: 'list', - name: 'location', - message: 'Where would you like to install Claude Code subagents?', - choices: [ - { name: 'Project level (.claude/agents/)', value: 'project' }, - { name: 'User level (~/.claude/agents/)', value: 'user' }, - ], - default: 'project', - }, - ]); - installLocation = locationAnswer.location; - } - } + if (interactive && config.subagents && !choices) { + choices = await this.promptSubagentInstallation(config.subagents); - // Process content injections based on user choices - if (config.injections && subagentChoices && subagentChoices.install !== 'none') { - for (const injection of config.injections) { - // Check if this injection is related to a selected subagent - if (this.shouldInject(injection, subagentChoices)) { - await this.injectContent(projectDir, injection, subagentChoices); - } - } - } - - // Copy selected subagents - if (config.subagents && subagentChoices && subagentChoices.install !== 'none') { - await this.copySelectedSubagents( - projectDir, - path.dirname(injectionConfigPath), - config.subagents, - subagentChoices, - installLocation, - ); - } - } catch (error) { - console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`)); + if (choices.install !== 'none') { + const inquirer = require('inquirer'); + const locationAnswer = await inquirer.prompt([ + { + type: 'list', + name: 'location', + message: 'Where would you like to install Claude Code subagents?', + choices: [ + { name: 'Project level (.claude/agents/)', value: 'project' }, + { name: 'User level (~/.claude/agents/)', value: 'user' }, + ], + default: 'project', + }, + ]); + location = locationAnswer.location; } } + + if (config.injections && choices && choices.install !== 'none') { + for (const injection of config.injections) { + if (shouldApplyInjection(injection, choices)) { + await this.injectContent(projectDir, injection, choices); + } + } + } + + if (config.subagents && choices && choices.install !== 'none') { + await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); + } } + + return { subagentChoices: choices, installLocation: location }; } /** @@ -532,45 +372,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { return { install }; } - /** - * Check if an injection should be applied based on user choices - */ - shouldInject(injection, subagentChoices) { - // If user chose no subagents, no injections - if (subagentChoices.install === 'none') { - return false; - } - - // If user chose all subagents, all injections apply - if (subagentChoices.install === 'all') { - return true; - } - - // For selective installation, check the 'requires' field - if (subagentChoices.install === 'selective') { - // If injection requires 'any' subagent and user selected at least one - if (injection.requires === 'any' && subagentChoices.selected.length > 0) { - return true; - } - - // Check if the required subagent was selected - if (injection.requires) { - const requiredAgent = injection.requires + '.md'; - return subagentChoices.selected.includes(requiredAgent); - } - - // Fallback: check if injection mentions a selected agent - const selectedAgentNames = subagentChoices.selected.map((f) => f.replace('.md', '')); - for (const agentName of selectedAgentNames) { - if (injection.point && injection.point.includes(agentName)) { - return true; - } - } - } - - return false; - } - /** * Inject content at specified point in file */ @@ -587,7 +388,7 @@ class ClaudeCodeSetup extends BaseIdeSetup { // Filter content if selective subagents chosen if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { - injectionContent = this.filterAgentInstructions(injection.content, subagentChoices.selected); + injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); } content = content.replace(marker, injectionContent); @@ -597,59 +398,17 @@ class ClaudeCodeSetup extends BaseIdeSetup { } } - /** - * Filter agent instructions to only include selected subagents - */ - filterAgentInstructions(content, selectedFiles) { - const selectedAgents = selectedFiles.map((f) => f.replace('.md', '')); - const lines = content.split('\n'); - const filteredLines = []; - - let includeNextLine = true; - for (const line of lines) { - // Always include structural lines - if (line.includes('')) { - filteredLines.push(line); - includeNextLine = true; - } - // Check if line mentions a subagent - else if (line.includes('subagent')) { - let shouldInclude = false; - for (const agent of selectedAgents) { - if (line.includes(agent)) { - shouldInclude = true; - break; - } - } - if (shouldInclude) { - filteredLines.push(line); - } - } - // Include general instructions - else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) { - filteredLines.push(line); - } - } - - // Only return content if we have actual instructions - if (filteredLines.length > 2) { - // More than just llm tags - return filteredLines.join('\n'); - } - return ''; // Return empty if no relevant content - } - /** * Copy selected subagents to appropriate Claude agents directory */ - async copySelectedSubagents(projectDir, moduleClaudeDir, subagentConfig, choices, location) { + async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { const fs = require('fs-extra'); - const sourceDir = path.join(moduleClaudeDir, subagentConfig.source); + const os = require('node:os'); // Determine target directory based on user choice let targetDir; if (location === 'user') { - targetDir = path.join(require('node:os').homedir(), '.claude', 'agents'); + targetDir = path.join(os.homedir(), '.claude', 'agents'); console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`)); } else { targetDir = path.join(projectDir, '.claude', 'agents'); @@ -659,51 +418,28 @@ class ClaudeCodeSetup extends BaseIdeSetup { // Ensure target directory exists await this.ensureDir(targetDir); - // Determine which files to copy - let filesToCopy = []; - if (choices.install === 'all') { - filesToCopy = subagentConfig.files; - } else if (choices.install === 'selective') { - filesToCopy = choices.selected; - } + const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); - // Recursively find all matching files in source directory - const findFileInSource = async (filename) => { - const { glob } = require('glob'); - const pattern = path.join(sourceDir, '**', filename); - const files = await glob(pattern); - return files[0]; // Return first match - }; - - // Copy selected subagent files let copiedCount = 0; - for (const file of filesToCopy) { + for (const resolved of resolvedFiles) { try { - const sourcePath = await findFileInSource(file); + const sourcePath = resolved.absolutePath; - if (sourcePath && (await this.exists(sourcePath))) { - // Extract subfolder name if file is in a subfolder - const relPath = path.relative(sourceDir, sourcePath); - const subFolder = path.dirname(relPath); - - // Create corresponding subfolder in target if needed - let targetPath; - if (subFolder && subFolder !== '.') { - const targetSubDir = path.join(targetDir, subFolder); - await this.ensureDir(targetSubDir); - targetPath = path.join(targetSubDir, file); - } else { - targetPath = path.join(targetDir, file); - } - - await fs.copyFile(sourcePath, targetPath); - console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : subFolder + '/'}${file.replace('.md', '')}`)); - copiedCount++; + const subFolder = path.dirname(resolved.relativePath); + let targetPath; + if (subFolder && subFolder !== '.') { + const targetSubDir = path.join(targetDir, subFolder); + await this.ensureDir(targetSubDir); + targetPath = path.join(targetSubDir, path.basename(resolved.file)); } else { - console.log(chalk.yellow(` ⚠ Not found: ${file}`)); + targetPath = path.join(targetDir, path.basename(resolved.file)); } + + await fs.copyFile(sourcePath, targetPath); + console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); + copiedCount++; } catch (error) { - console.log(chalk.yellow(` ⚠ Error copying ${file}: ${error.message}`)); + console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); } } diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index b9c99a3a..03f17818 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -1,16 +1,18 @@ const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); +const fs = require('fs-extra'); +const os = require('node:os'); const chalk = require('chalk'); const inquirer = require('inquirer'); +const { BaseIdeSetup } = require('./_base-ide'); +const { WorkflowCommandGenerator } = require('./workflow-command-generator'); +const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); /** * Codex setup handler (supports both CLI and Web) - * Creates comprehensive AGENTS.md file in project root */ class CodexSetup extends BaseIdeSetup { constructor() { super('codex', 'Codex', true); // preferred IDE - this.agentsFile = 'AGENTS.md'; } /** @@ -44,223 +46,158 @@ class CodexSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Use pre-collected configuration if available const config = options.preCollectedConfig || {}; const mode = config.codexMode || options.codexMode || 'cli'; - // Get agents and tasks - const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options); - // Create AGENTS.md content - const content = this.createAgentsDocument(agents, tasks, mode); - - // Write AGENTS.md file - const agentsPath = path.join(projectDir, this.agentsFile); - await this.writeFile(agentsPath, content); - - // Handle mode-specific setup - if (mode === 'web') { - await this.setupWebMode(projectDir); - } else { - await this.setupCliMode(projectDir); - } + const destDir = this.getCodexPromptDir(); + await fs.ensureDir(destDir); + await this.clearOldBmadFiles(destDir); + const written = await this.flattenAndWriteArtifacts(artifacts, destDir); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - Mode: ${mode === 'web' ? 'Web' : 'CLI'}`)); - console.log(chalk.dim(` - ${agents.length} agents documented`)); - console.log(chalk.dim(` - ${tasks.length} tasks documented`)); - console.log(chalk.dim(` - Agents file: ${this.agentsFile}`)); + console.log(chalk.dim(` - ${counts.agents} agents exported`)); + console.log(chalk.dim(` - ${counts.tasks} tasks exported`)); + console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`)); + if (counts.workflowLaunchers > 0) { + console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`)); + } + if (counts.subagents > 0) { + console.log(chalk.dim(` - ${counts.subagents} subagents exported`)); + } + console.log(chalk.dim(` - ${written} Codex prompt files written`)); + console.log(chalk.dim(` - Destination: ${destDir}`)); return { success: true, mode, - agents: agents.length, - tasks: tasks.length, + artifacts, + counts, + destination: destDir, + written, }; } /** - * Select Codex mode (CLI or Web) + * Collect Claude-style artifacts for Codex export. + * Returns the normalized artifact list for further processing. */ - async selectMode() { - const response = await inquirer.prompt([ - { - type: 'list', - name: 'mode', - message: 'Select Codex deployment mode:', - choices: [ - { name: 'CLI (Command-line interface)', value: 'cli' }, - { name: 'Web (Browser-based interface)', value: 'web' }, - ], - default: 'cli', - }, - ]); + async collectClaudeArtifacts(projectDir, bmadDir, options = {}) { + const selectedModules = options.selectedModules || []; + const artifacts = []; - return response.mode; - } - - /** - * Create comprehensive agents document - */ - createAgentsDocument(agents, tasks, mode) { - let content = `# BMAD Method - Agent Directory - -This document contains all available BMAD agents and tasks for use with Codex ${mode === 'web' ? 'Web' : 'CLI'}. - -## Quick Start - -${ - mode === 'web' - ? `Access agents through the web interface: -1. Navigate to the Agents section -2. Select an agent to activate -3. The agent persona will be active for your session` - : `Activate agents in CLI: -1. Reference agents using \`@{agent-name}\` -2. Execute tasks using \`@task-{task-name}\` -3. Agents remain active for the conversation` -} - ---- - -## Available Agents - -`; - - // Group agents by module - const agentsByModule = {}; + const agents = await getAgentsFromBmad(bmadDir, selectedModules); for (const agent of agents) { - if (!agentsByModule[agent.module]) { - agentsByModule[agent.module] = []; - } - agentsByModule[agent.module].push(agent); + const content = await this.readAndProcessWithProject( + agent.path, + { + module: agent.module, + name: agent.name, + }, + projectDir, + ); + + artifacts.push({ + type: 'agent', + module: agent.module, + sourcePath: agent.path, + relativePath: path.join(agent.module, 'agents', `${agent.name}.md`), + content, + }); } - // Document each module's agents - for (const [module, moduleAgents] of Object.entries(agentsByModule)) { - content += `### ${module.toUpperCase()} Module\n\n`; - - for (const agent of moduleAgents) { - const agentContent = this.readFileSync(agent.path); - const titleMatch = agentContent.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - - const iconMatch = agentContent.match(/icon="([^"]+)"/); - const icon = iconMatch ? iconMatch[1] : '🤖'; - - const whenToUseMatch = agentContent.match(/whenToUse="([^"]+)"/); - const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - - content += `#### ${icon} ${title} (\`@${agent.name}\`)\n\n`; - content += `**When to use:** ${whenToUse}\n\n`; - content += `**Activation:** Type \`@${agent.name}\` to activate this agent.\n\n`; - } - } - - content += `--- - -## Available Tasks - -`; - - // Group tasks by module - const tasksByModule = {}; + const tasks = await getTasksFromBmad(bmadDir, selectedModules); for (const task of tasks) { - if (!tasksByModule[task.module]) { - tasksByModule[task.module] = []; - } - tasksByModule[task.module].push(task); + const content = await this.readAndProcessWithProject( + task.path, + { + module: task.module, + name: task.name, + }, + projectDir, + ); + + artifacts.push({ + type: 'task', + module: task.module, + sourcePath: task.path, + relativePath: path.join(task.module, 'tasks', `${task.name}.md`), + content, + }); } - // Document each module's tasks - for (const [module, moduleTasks] of Object.entries(tasksByModule)) { - content += `### ${module.toUpperCase()} Module Tasks\n\n`; + const workflowGenerator = new WorkflowCommandGenerator(); + const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + artifacts.push(...workflowArtifacts); - for (const task of moduleTasks) { - const taskContent = this.readFileSync(task.path); - const nameMatch = taskContent.match(/([^<]+)<\/name>/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - content += `- **${taskName}** (\`@task-${task.name}\`)\n`; - } - content += '\n'; - } - - content += `--- - -## Usage Guidelines - -1. **One agent at a time**: Activate a single agent for focused assistance -2. **Task execution**: Tasks are one-time workflows, not persistent personas -3. **Module organization**: Agents and tasks are grouped by their source module -4. **Context preservation**: ${mode === 'web' ? 'Sessions maintain agent context' : 'Conversations maintain agent context'} - ---- - -*Generated by BMAD Method installer for Codex ${mode === 'web' ? 'Web' : 'CLI'}* -`; - - return content; + return { + artifacts, + counts: { + agents: agents.length, + tasks: tasks.length, + workflows: workflowCounts.commands, + workflowLaunchers: workflowCounts.launchers, + }, + }; } - /** - * Read file synchronously (for document generation) - */ - readFileSync(filePath) { - const fs = require('node:fs'); - try { - return fs.readFileSync(filePath, 'utf8'); - } catch { - return ''; - } + getCodexPromptDir() { + return path.join(os.homedir(), '.codex', 'prompts'); } - /** - * Setup for CLI mode - */ - async setupCliMode(projectDir) { - // CLI mode - ensure .gitignore includes AGENTS.md if needed - const fs = require('fs-extra'); - const gitignorePath = path.join(projectDir, '.gitignore'); + flattenFilename(relativePath) { + const sanitized = relativePath.replaceAll(/[\\/]/g, '-'); + return `bmad-${sanitized}`; + } - if (await fs.pathExists(gitignorePath)) { - const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); - if (!gitignoreContent.includes('AGENTS.md')) { - // User can decide whether to track this file - console.log(chalk.dim(' Note: Consider adding AGENTS.md to .gitignore if desired')); + async flattenAndWriteArtifacts(artifacts, destDir) { + let written = 0; + + for (const artifact of artifacts) { + const flattenedName = this.flattenFilename(artifact.relativePath); + const targetPath = path.join(destDir, flattenedName); + await fs.writeFile(targetPath, artifact.content); + written++; + } + + return written; + } + + async clearOldBmadFiles(destDir) { + if (!(await fs.pathExists(destDir))) { + return; + } + + const entries = await fs.readdir(destDir); + + for (const entry of entries) { + if (!entry.startsWith('bmad-')) { + continue; + } + + const entryPath = path.join(destDir, entry); + const stat = await fs.stat(entryPath); + if (stat.isFile()) { + await fs.remove(entryPath); + } else if (stat.isDirectory()) { + await fs.remove(entryPath); } } } - /** - * Setup for Web mode - */ - async setupWebMode(projectDir) { - // Web mode - add to .gitignore to avoid committing - const fs = require('fs-extra'); - const gitignorePath = path.join(projectDir, '.gitignore'); - - if (await fs.pathExists(gitignorePath)) { - const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); - if (!gitignoreContent.includes('AGENTS.md')) { - await fs.appendFile(gitignorePath, '\n# Codex Web agents file\nAGENTS.md\n'); - console.log(chalk.dim(' Added AGENTS.md to .gitignore for web deployment')); - } - } + async readAndProcessWithProject(filePath, metadata, projectDir) { + const content = await fs.readFile(filePath, 'utf8'); + return super.processContent(content, metadata, projectDir); } /** - * Cleanup Codex configuration + * Cleanup Codex configuration (no-op until export destination is finalized) */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const agentsPath = path.join(projectDir, this.agentsFile); - - if (await fs.pathExists(agentsPath)) { - await fs.remove(agentsPath); - console.log(chalk.dim('Removed AGENTS.md file')); - } + async cleanup() { + const destDir = this.getCodexPromptDir(); + await this.clearOldBmadFiles(destDir); } } diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 7712a4a8..a7677d70 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,6 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); const chalk = require('chalk'); +const os = require('node:os'); /** * IDE Manager - handles IDE-specific setup @@ -191,9 +192,13 @@ class IdeManager { } } - // Check for AGENTS.md (Codex) - if (await fs.pathExists(path.join(projectDir, 'AGENTS.md'))) { - detected.push('codex'); + // Check Codex prompt directory for BMAD exports + const codexPromptDir = path.join(os.homedir(), '.codex', 'prompts'); + if (await fs.pathExists(codexPromptDir)) { + const codexEntries = await fs.readdir(codexPromptDir); + if (codexEntries.some((file) => file.startsWith('bmad-'))) { + detected.push('codex'); + } } return detected; diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js new file mode 100644 index 00000000..732d7807 --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -0,0 +1,112 @@ +const path = require('node:path'); +const fs = require('fs-extra'); + +/** + * Helpers for gathering BMAD agents/tasks from the installed tree. + * Shared by installers that need Claude-style exports. + */ +async function getAgentsFromBmad(bmadDir, selectedModules = []) { + const agents = []; + + if (await fs.pathExists(path.join(bmadDir, 'core', 'agents'))) { + const coreAgents = await getAgentsFromDir(path.join(bmadDir, 'core', 'agents'), 'core'); + agents.push(...coreAgents); + } + + for (const moduleName of selectedModules) { + const agentsPath = path.join(bmadDir, moduleName, 'agents'); + + if (await fs.pathExists(agentsPath)) { + const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); + agents.push(...moduleAgents); + } + } + + return agents; +} + +async function getTasksFromBmad(bmadDir, selectedModules = []) { + const tasks = []; + + if (await fs.pathExists(path.join(bmadDir, 'core', 'tasks'))) { + const coreTasks = await getTasksFromDir(path.join(bmadDir, 'core', 'tasks'), 'core'); + tasks.push(...coreTasks); + } + + for (const moduleName of selectedModules) { + const tasksPath = path.join(bmadDir, moduleName, 'tasks'); + + if (await fs.pathExists(tasksPath)) { + const moduleTasks = await getTasksFromDir(tasksPath, moduleName); + tasks.push(...moduleTasks); + } + } + + return tasks; +} + +async function getAgentsFromDir(dirPath, moduleName) { + const agents = []; + + if (!(await fs.pathExists(dirPath))) { + return agents; + } + + const files = await fs.readdir(dirPath); + + for (const file of files) { + if (!file.endsWith('.md')) { + continue; + } + + if (file.includes('.customize.')) { + continue; + } + + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, 'utf8'); + + if (content.includes('localskip="true"')) { + continue; + } + + agents.push({ + path: filePath, + name: file.replace('.md', ''), + module: moduleName, + }); + } + + return agents; +} + +async function getTasksFromDir(dirPath, moduleName) { + const tasks = []; + + if (!(await fs.pathExists(dirPath))) { + return tasks; + } + + const files = await fs.readdir(dirPath); + + for (const file of files) { + if (!file.endsWith('.md')) { + continue; + } + + tasks.push({ + path: path.join(dirPath, file), + name: file.replace('.md', ''), + module: moduleName, + }); + } + + return tasks; +} + +module.exports = { + getAgentsFromBmad, + getTasksFromBmad, + getAgentsFromDir, + getTasksFromDir, +}; diff --git a/tools/cli/installers/lib/ide/shared/module-injections.js b/tools/cli/installers/lib/ide/shared/module-injections.js new file mode 100644 index 00000000..28ff64d1 --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/module-injections.js @@ -0,0 +1,133 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('js-yaml'); +const { glob } = require('glob'); +const { getSourcePath } = require('../../../../lib/project-root'); + +async function loadModuleInjectionConfig(handler, moduleName) { + const sourceModulesPath = getSourcePath('modules'); + const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler); + const configPath = path.join(handlerBaseDir, 'injections.yaml'); + + if (!(await fs.pathExists(configPath))) { + return null; + } + + const configContent = await fs.readFile(configPath, 'utf8'); + const config = yaml.load(configContent) || {}; + + return { + config, + handlerBaseDir, + configPath, + }; +} + +function shouldApplyInjection(injection, subagentChoices) { + if (!subagentChoices || subagentChoices.install === 'none') { + return false; + } + + if (subagentChoices.install === 'all') { + return true; + } + + if (subagentChoices.install === 'selective') { + const selected = subagentChoices.selected || []; + + if (injection.requires === 'any' && selected.length > 0) { + return true; + } + + if (injection.requires) { + const required = `${injection.requires}.md`; + return selected.includes(required); + } + + if (injection.point) { + const selectedNames = selected.map((file) => file.replace('.md', '')); + return selectedNames.some((name) => injection.point.includes(name)); + } + } + + return false; +} + +function filterAgentInstructions(content, selectedFiles) { + if (!selectedFiles || selectedFiles.length === 0) { + return ''; + } + + const selectedAgents = selectedFiles.map((file) => file.replace('.md', '')); + const lines = content.split('\n'); + const filteredLines = []; + + for (const line of lines) { + if (line.includes('')) { + filteredLines.push(line); + } else if (line.includes('subagent')) { + let shouldInclude = false; + for (const agent of selectedAgents) { + if (line.includes(agent)) { + shouldInclude = true; + break; + } + } + + if (shouldInclude) { + filteredLines.push(line); + } + } else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) { + filteredLines.push(line); + } + } + + if (filteredLines.length > 2) { + return filteredLines.join('\n'); + } + + return ''; +} + +async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) { + if (!subagentConfig || !subagentConfig.files) { + return []; + } + + if (!subagentChoices || subagentChoices.install === 'none') { + return []; + } + + let filesToCopy = subagentConfig.files; + + if (subagentChoices.install === 'selective') { + filesToCopy = subagentChoices.selected || []; + } + + const sourceDir = path.join(handlerBaseDir, subagentConfig.source || ''); + const resolved = []; + + for (const file of filesToCopy) { + const pattern = path.join(sourceDir, '**', file); + const matches = await glob(pattern); + + if (matches.length > 0) { + const absolutePath = matches[0]; + resolved.push({ + file, + absolutePath, + relativePath: path.relative(sourceDir, absolutePath), + sourceDir, + }); + } + } + + return resolved; +} + +module.exports = { + loadModuleInjectionConfig, + shouldApplyInjection, + filterAgentInstructions, + resolveSubagentFiles, +}; diff --git a/tools/cli/installers/lib/ide/workflow-command-generator.js b/tools/cli/installers/lib/ide/workflow-command-generator.js index a2b6f5b8..aa13624a 100644 --- a/tools/cli/installers/lib/ide/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/workflow-command-generator.js @@ -17,20 +17,13 @@ class WorkflowCommandGenerator { * @param {string} bmadDir - BMAD installation directory */ async generateWorkflowCommands(projectDir, bmadDir) { - const manifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv'); + const workflows = await this.loadWorkflowManifest(bmadDir); - if (!(await fs.pathExists(manifestPath))) { + if (!workflows) { console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.')); return { generated: 0 }; } - // Read and parse the CSV manifest - const csvContent = await fs.readFile(manifestPath, 'utf8'); - const workflows = csv.parse(csvContent, { - columns: true, - skip_empty_lines: true, - }); - // Base commands directory const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad'); @@ -38,11 +31,9 @@ class WorkflowCommandGenerator { // Generate a command file for each workflow, organized by module for (const workflow of workflows) { - // Create module directory structure: commands/bmad/{module}/workflows/ const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows'); await fs.ensureDir(moduleWorkflowsDir); - // Use just the workflow name as filename (no prefix) const commandContent = await this.generateCommandContent(workflow, bmadDir); const commandPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`); @@ -51,11 +42,52 @@ class WorkflowCommandGenerator { } // Also create a workflow launcher README in each module - await this.createModuleWorkflowLaunchers(baseCommandsDir, workflows, bmadDir); + const groupedWorkflows = this.groupWorkflowsByModule(workflows); + await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows); return { generated: generatedCount }; } + async collectWorkflowArtifacts(bmadDir) { + const workflows = await this.loadWorkflowManifest(bmadDir); + + if (!workflows) { + return { artifacts: [], counts: { commands: 0, launchers: 0 } }; + } + + const artifacts = []; + + for (const workflow of workflows) { + const commandContent = await this.generateCommandContent(workflow, bmadDir); + artifacts.push({ + type: 'workflow-command', + module: workflow.module, + relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), + content: commandContent, + sourcePath: workflow.path, + }); + } + + const groupedWorkflows = this.groupWorkflowsByModule(workflows); + for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) { + artifacts.push({ + type: 'workflow-launcher', + module, + relativePath: path.join(module, 'workflows', 'README.md'), + content: launcherContent, + sourcePath: null, + }); + } + + return { + artifacts, + counts: { + commands: workflows.length, + launchers: Object.keys(groupedWorkflows).length, + }, + }; + } + /** * Generate command content for a workflow */ @@ -94,49 +126,57 @@ class WorkflowCommandGenerator { /** * Create workflow launcher files for each module */ - async createModuleWorkflowLaunchers(baseCommandsDir, workflows, bmadDir) { - // Group workflows by module + async createModuleWorkflowLaunchers(baseCommandsDir, workflowsByModule) { + for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) { + const content = this.buildLauncherContent(module, moduleWorkflows); + const moduleWorkflowsDir = path.join(baseCommandsDir, module, 'workflows'); + await fs.ensureDir(moduleWorkflowsDir); + const launcherPath = path.join(moduleWorkflowsDir, 'README.md'); + await fs.writeFile(launcherPath, content); + } + } + + groupWorkflowsByModule(workflows) { const workflowsByModule = {}; + for (const workflow of workflows) { if (!workflowsByModule[workflow.module]) { workflowsByModule[workflow.module] = []; } - // Convert path for display - let workflowPath = workflow.path; - if (workflowPath.includes('/src/modules/')) { - const match = workflowPath.match(/\/src\/modules\/(.+)/); - if (match) { - workflowPath = `{project-root}/bmad/${match[1]}`; - } - } else if (workflowPath.includes('/src/core/')) { - const match = workflowPath.match(/\/src\/core\/(.+)/); - if (match) { - workflowPath = `{project-root}/bmad/core/${match[1]}`; - } - } - workflowsByModule[workflow.module].push({ ...workflow, - displayPath: workflowPath, + displayPath: this.transformWorkflowPath(workflow.path), }); } - // Create a launcher file for each module - for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) { - let content = `# ${module.toUpperCase()} Workflows + return workflowsByModule; + } + + buildModuleWorkflowLaunchers(groupedWorkflows) { + const launchers = {}; + + for (const [module, moduleWorkflows] of Object.entries(groupedWorkflows)) { + launchers[module] = this.buildLauncherContent(module, moduleWorkflows); + } + + return launchers; + } + + buildLauncherContent(module, moduleWorkflows) { + let content = `# ${module.toUpperCase()} Workflows ## Available Workflows in ${module} `; - for (const workflow of moduleWorkflows) { - content += `**${workflow.name}**\n`; - content += `- Path: \`${workflow.displayPath}\`\n`; - content += `- ${workflow.description}\n\n`; - } + for (const workflow of moduleWorkflows) { + content += `**${workflow.name}**\n`; + content += `- Path: \`${workflow.displayPath}\`\n`; + content += `- ${workflow.description}\n\n`; + } - content += ` + content += ` ## Execution When running any workflow: @@ -150,12 +190,39 @@ When running any workflow: - #yolo: Skip optional steps `; - // Write module-specific launcher - const moduleWorkflowsDir = path.join(baseCommandsDir, module, 'workflows'); - await fs.ensureDir(moduleWorkflowsDir); - const launcherPath = path.join(moduleWorkflowsDir, 'README.md'); - await fs.writeFile(launcherPath, content); + return content; + } + + transformWorkflowPath(workflowPath) { + let transformed = workflowPath; + + if (workflowPath.includes('/src/modules/')) { + const match = workflowPath.match(/\/src\/modules\/(.+)/); + if (match) { + transformed = `{project-root}/bmad/${match[1]}`; + } + } else if (workflowPath.includes('/src/core/')) { + const match = workflowPath.match(/\/src\/core\/(.+)/); + if (match) { + transformed = `{project-root}/bmad/core/${match[1]}`; + } } + + return transformed; + } + + async loadWorkflowManifest(bmadDir) { + const manifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv'); + + if (!(await fs.pathExists(manifestPath))) { + return null; + } + + const csvContent = await fs.readFile(manifestPath, 'utf8'); + return csv.parse(csvContent, { + columns: true, + skip_empty_lines: true, + }); } }