Files
BMAD-METHOD/tools/cli/installers/lib/ide/workflow-command-generator.js

230 lines
6.8 KiB
JavaScript

const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const chalk = require('chalk');
/**
* Generates Claude Code command files for each workflow in the manifest
*/
class WorkflowCommandGenerator {
constructor() {
this.templatePath = path.join(__dirname, 'workflow-command-template.md');
}
/**
* Generate workflow commands from the manifest CSV
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
*/
async generateWorkflowCommands(projectDir, bmadDir) {
const workflows = await this.loadWorkflowManifest(bmadDir);
if (!workflows) {
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
return { generated: 0 };
}
// Base commands directory
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0;
// Generate a command file for each workflow, organized by module
for (const workflow of workflows) {
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir);
const commandContent = await this.generateCommandContent(workflow, bmadDir);
const commandPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
await fs.writeFile(commandPath, commandContent);
generatedCount++;
}
// Also create a workflow launcher README in each module
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
*/
async generateCommandContent(workflow, bmadDir) {
// Load the template
const template = await fs.readFile(this.templatePath, 'utf8');
// Convert source path to installed path
// From: /Users/.../src/modules/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/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]}`;
}
}
// Replace template variables
return template
.replaceAll('{{name}}', workflow.name)
.replaceAll('{{module}}', workflow.module)
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('{{interactive}}', workflow.interactive)
.replaceAll('{{author}}', workflow.author || '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}/bmad/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/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,
});
}
}
module.exports = { WorkflowCommandGenerator };