fix: normalize custom bmad workflow paths in command generation

This commit is contained in:
Dicky Moore 2026-02-09 08:15:54 +00:00
parent 9987a25e3e
commit 0ba1167c60
2 changed files with 87 additions and 16 deletions

View File

@ -862,6 +862,56 @@ internal: true
console.log('');
// ============================================================
// Test 22: Custom BMAD Folder Workflow Path Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 22: Custom BMAD Folder Workflow Path Guard${colors.reset}\n`);
try {
const generator = new WorkflowCommandGenerator('mybmad');
generator.loadWorkflowManifest = async () => [
{
name: 'sprint-planning',
description: 'Sprint Planning',
module: 'bmm',
path: '/tmp/project/mybmad/bmm/workflows/4-implementation/sprint-planning/workflow.md',
},
{
name: 'create-story',
description: 'Create Story',
module: 'bmm',
path: 'mybmad/bmm/workflows/4-implementation/create-story/workflow.md',
},
];
generator.generateCommandContent = async () => 'content';
const { artifacts } = await generator.collectWorkflowArtifacts('/tmp');
const sprintPlanning = artifacts.find((artifact) => artifact.name === 'sprint-planning');
const createStory = artifacts.find((artifact) => artifact.name === 'create-story');
assert(
sprintPlanning?.workflowPath === 'bmm/workflows/4-implementation/sprint-planning/workflow.md',
'Custom folder absolute workflow path strips configured BMAD folder prefix',
sprintPlanning?.workflowPath,
);
assert(
createStory?.workflowPath === 'bmm/workflows/4-implementation/create-story/workflow.md',
'Custom folder relative workflow path strips configured BMAD folder prefix',
createStory?.workflowPath,
);
const installedPath = generator.mapSourcePathToInstalled('/tmp/project/mybmad/core/tasks/workflow.md');
assert(
installedPath === 'mybmad/core/tasks/workflow.md',
'Installed workflow path mapping handles absolute paths containing custom BMAD folder',
installedPath,
);
} catch (error) {
assert(false, 'Custom BMAD folder workflow path guard runs', error.message);
}
console.log('');
for (const tmpRoot of tmpRoots) {
await fs.remove(tmpRoot).catch(() => {});
}

View File

@ -4,6 +4,10 @@ const csv = require('csv-parse/sync');
const prompts = require('../../../../lib/prompts');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
function escapeRegex(value) {
return String(value).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
}
/**
* Generates command files for each workflow in the manifest
*/
@ -67,22 +71,7 @@ class WorkflowCommandGenerator {
for (const workflow of allWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir);
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
let workflowRelPath = workflow.path || '';
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.at(-1);
}
} else if (workflowRelPath.includes('/src/') || workflowRelPath.startsWith('src/')) {
const match = workflowRelPath.match(/(?:^|\/)src\/([^/]+)\/(.+)/);
if (match) {
workflowRelPath = `${match[1]}/${match[2]}`;
}
}
const workflowRelPath = this.mapSourcePathToModuleRelative(workflow.path);
artifacts.push({
type: 'workflow-command',
name: workflow.name,
@ -213,6 +202,29 @@ When running any workflow:
return this.mapSourcePathToInstalled(workflowPath, true);
}
mapSourcePathToModuleRelative(sourcePath) {
const mapped = this.mapSourcePathToInstalled(sourcePath, false);
if (!mapped) {
return mapped;
}
const normalized = String(mapped).replaceAll('\\', '/');
// Typical installed path -> strip BMAD root prefix for templates that prepend it.
if (normalized.startsWith(`${this.bmadFolderName}/`)) {
return normalized.slice(`${this.bmadFolderName}/`.length);
}
// Absolute path containing the configured BMAD root folder.
const folderPattern = new RegExp(`(?:^|\\/)${escapeRegex(this.bmadFolderName)}\\/(.+)`);
const folderMatch = normalized.match(folderPattern);
if (folderMatch) {
return folderMatch[1];
}
return normalized;
}
mapSourcePathToInstalled(sourcePath, includeProjectRootPrefix = false) {
if (!sourcePath) {
return sourcePath;
@ -232,6 +244,15 @@ When running any workflow:
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
// Handle absolute paths that already include the configured BMAD folder
// (e.g., /tmp/project/mybmad/bmm/workflows/...).
const folderPattern = new RegExp(`(?:^|\\/)${escapeRegex(this.bmadFolderName)}\\/(.+)`);
const folderMatch = normalized.match(folderPattern);
if (folderMatch) {
const mapped = `${this.bmadFolderName}/${folderMatch[1]}`;
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
if (normalized.startsWith(`${this.bmadFolderName}/`)) {
return includeProjectRootPrefix ? `{project-root}/${normalized}` : normalized;
}