const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); /** * Generates command files for each workflow in the manifest */ class WorkflowCommandGenerator { constructor(bmadFolderName = 'bmad') { this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.bmadFolderName = bmadFolderName; } /** * REMOVED: Old generateWorkflowCommands method that created nested structure. * This was hardcoded to .claude/commands/bmad and caused bugs. * Use collectWorkflowArtifacts() + writeColonArtifacts/writeDashArtifacts() instead. */ async collectWorkflowArtifacts(bmadDir) { const workflows = await this.loadWorkflowManifest(bmadDir); if (!workflows) { return { artifacts: [], counts: { commands: 0, launchers: 0 } }; } // ALL workflows now generate commands - no standalone filtering const allWorkflows = workflows; const artifacts = []; for (const workflow of allWorkflows) { 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(allWorkflows); 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: allWorkflows.length, launchers: Object.keys(groupedWorkflows).length, }, }; } /** * Generate command content for a workflow */ async generateCommandContent(workflow, bmadDir) { // Determine template based on workflow file type const isMarkdownWorkflow = workflow.path.endsWith('workflow.md'); const templateName = isMarkdownWorkflow ? 'workflow-commander.md' : 'workflow-command-template.md'; const templatePath = path.join(path.dirname(this.templatePath), templateName); // Load the appropriate template const template = await fs.readFile(templatePath, 'utf8'); // Convert source path to installed path // From: /Users/.../src/bmm/workflows/.../workflow.yaml // To: {project-root}/_bmad/bmm/workflows/.../workflow.yaml let workflowPath = workflow.path; // Extract the relative path from source if (workflowPath.includes('/src/bmm/')) { // bmm is directly under src/ const match = workflowPath.match(/\/src\/bmm\/(.+)/); if (match) { workflowPath = `${this.bmadFolderName}/bmm/${match[1]}`; } } else if (workflowPath.includes('/src/core/')) { const match = workflowPath.match(/\/src\/core\/(.+)/); if (match) { workflowPath = `${this.bmadFolderName}/core/${match[1]}`; } } // Replace template variables return template .replaceAll('{{name}}', workflow.name) .replaceAll('{{module}}', workflow.module) .replaceAll('{{description}}', workflow.description) .replaceAll('{{workflow_path}}', workflowPath) .replaceAll('_bmad', this.bmadFolderName) .replaceAll('_bmad', '_bmad'); } /** * Create workflow launcher files for each 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] = []; } workflowsByModule[workflow.module].push({ ...workflow, displayPath: this.transformWorkflowPath(workflow.path), }); } 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`; } content += ` ## Execution When running any workflow: 1. LOAD {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml 2. Pass the workflow path as 'workflow-config' parameter 3. Follow workflow.xml instructions EXACTLY 4. Save outputs after EACH section ## Modes - Normal: Full interaction - #yolo: Skip optional steps `; return content; } transformWorkflowPath(workflowPath) { let transformed = workflowPath; if (workflowPath.includes('/src/bmm/')) { const match = workflowPath.match(/\/src\/bmm\/(.+)/); if (match) { transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`; } else if (workflowPath.includes('/src/core/')) { const match = workflowPath.match(/\/src\/core\/(.+)/); if (match) { transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`; } } return transformed; } } async loadWorkflowManifest(bmadDir) { const manifestPath = path.join(bmadDir, '_config', '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, }); } /** * Write workflow command artifacts using underscore format (Windows-compatible) * Creates flat files like: bmad_bmm_correct-course.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Workflow artifacts * @returns {number} Count of commands written */ async writeColonArtifacts(baseCommandsDir, artifacts) { let writtenCount = 0; for (const artifact of artifacts) { if (artifact.type === 'workflow-command') { // Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md const flatName = toColonPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, artifact.content); writtenCount++; } } return writtenCount; } /** * Write workflow command artifacts using underscore format (Windows-compatible) * Creates flat files like: bmad_bmm_correct-course.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Workflow artifacts * @returns {number} Count of commands written */ async writeDashArtifacts(baseCommandsDir, artifacts) { let writtenCount = 0; for (const artifact of artifacts) { if (artifact.type === 'workflow-command') { // Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md const flatName = toDashPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, artifact.content); writtenCount++; } } return writtenCount; } } module.exports = { WorkflowCommandGenerator };