163 lines
5.2 KiB
JavaScript
163 lines
5.2 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 manifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv');
|
|
|
|
if (!(await fs.pathExists(manifestPath))) {
|
|
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');
|
|
|
|
let generatedCount = 0;
|
|
|
|
// 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`);
|
|
|
|
await fs.writeFile(commandPath, commandContent);
|
|
generatedCount++;
|
|
}
|
|
|
|
// Also create a workflow launcher README in each module
|
|
await this.createModuleWorkflowLaunchers(baseCommandsDir, workflows, bmadDir);
|
|
|
|
return { generated: generatedCount };
|
|
}
|
|
|
|
/**
|
|
* 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, workflows, bmadDir) {
|
|
// Group workflows by module
|
|
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,
|
|
});
|
|
}
|
|
|
|
// Create a launcher file for each module
|
|
for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) {
|
|
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
|
|
`;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { WorkflowCommandGenerator };
|