180 lines
5.5 KiB
JavaScript
180 lines
5.5 KiB
JavaScript
const path = require('node:path');
|
|
const fs = require('fs-extra');
|
|
const csv = require('csv-parse/sync');
|
|
const { BMAD_FOLDER_NAME } = require('./path-utils');
|
|
|
|
/**
|
|
* Generates command files for each workflow in the manifest
|
|
*/
|
|
class WorkflowCommandGenerator {
|
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
|
this.bmadFolderName = bmadFolderName;
|
|
}
|
|
|
|
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) {
|
|
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
|
|
let workflowRelPath = workflow.path || '';
|
|
// Normalize path separators for cross-platform compatibility
|
|
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
|
// Remove _bmad/ prefix if present to get relative path from project root
|
|
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
|
if (workflowRelPath.includes('_bmad/')) {
|
|
const parts = workflowRelPath.split(/_bmad\//);
|
|
if (parts.length > 1) {
|
|
workflowRelPath = parts.slice(1).join('/');
|
|
}
|
|
} else if (workflowRelPath.includes('/src/')) {
|
|
// Normalize source paths (e.g. .../src/bmm/...) to relative module path (e.g. bmm/...)
|
|
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
|
|
if (match) {
|
|
workflowRelPath = `${match[1]}/${match[2]}`;
|
|
}
|
|
}
|
|
artifacts.push({
|
|
type: 'workflow-command',
|
|
name: workflow.name,
|
|
description: workflow.description || `${workflow.name} workflow`,
|
|
module: workflow.module,
|
|
canonicalId: workflow.canonicalId || '',
|
|
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
|
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 the workflow.md file at the path shown above
|
|
2. READ its entire contents and follow its directions exactly
|
|
3. 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = { WorkflowCommandGenerator };
|