Compare commits

...

16 Commits

Author SHA1 Message Date
Davor Racic 2edf03c63d
Merge 5d470b2de3 into 323cd75efd 2026-02-02 11:16:31 +01:00
Davor Racić 5d470b2de3 fix: skip internal tools in manifest generation and improve Windows path handling in command generator 2026-02-02 11:16:20 +01:00
Davor Racić 6f99092be1 fix: skip empty artifact_types targets and remove unused vscode_settings target 2026-02-02 10:39:45 +01:00
Davor Racić 12d4e1ff6e fix: skip internal tasks in manifest generation and IDE command discovery 2026-02-02 09:08:45 +01:00
Davor Racić ba00d43216 fix: support .xml task files in bmad-artifacts task discovery 2026-02-02 07:49:49 +01:00
Davor Racic e5bd51528a
Merge branch 'main' into fix/gemini 2026-02-01 21:07:29 +01:00
Davor Racić a69810afe5 fix: convert absolute paths to relative in task-tool-command-generator 2026-02-01 13:39:09 +01:00
Davor Racić 1d505b17b4 fix: add safety checks for setBmadFolderName method calls in IdeManager 2026-02-01 13:23:19 +01:00
Davor Racić a6a960f88c fix: Replace the rest of BMAD_FOLDER magic values 2026-02-01 12:42:10 +01:00
Davor Racić 8ed36d9f0d refactor: centralize BMAD_FOLDER_NAME constant in path-utils 2026-02-01 12:24:09 +01:00
Alex Verkhovsky 2ef410c726
Merge branch 'main' into fix/gemini 2026-02-01 01:12:55 -07:00
Davor Racić 74b9f22f20 fix: change default BMAD folder name from 'bmad' to '_bmad' across all IDE components 2026-02-01 00:06:03 +01:00
Davor Racić f334f66164 fix: correct path handling and variable reference in task/tool command generator 2026-01-31 23:33:58 +01:00
Davor Racić 9e50f015a3 fix: double extension issue in wrapper filename generation 2026-01-31 22:25:37 +01:00
Davor Racić c0f51350c2 fix: preserve file extensions in IDE task/tool paths and update BMAD branding 2026-01-31 22:25:37 +01:00
Davor Racić 91b2d84ff8 fix: support CRLF line endings and add task/tool templates for all IDEs 2026-01-31 22:25:37 +01:00
20 changed files with 307 additions and 41 deletions

View File

@ -1,4 +1,4 @@
<task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false"> <task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false" internal="true">
<objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective> <objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective>
<llm critical="true"> <llm critical="true">

View File

@ -146,7 +146,7 @@ class DependencyResolver {
const content = await fs.readFile(file.path, 'utf8'); const content = await fs.readFile(file.path, 'utf8');
// Parse YAML frontmatter for explicit dependencies // Parse YAML frontmatter for explicit dependencies
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
// Pre-process to handle backticks in YAML values // Pre-process to handle backticks in YAML values

View File

@ -17,9 +17,7 @@ const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager'); const { IdeConfigManager } = require('./ide-config-manager');
const { CustomHandler } = require('../custom/handler'); const { CustomHandler } = require('../custom/handler');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
// BMAD installation folder name - this is constant and should never change
const BMAD_FOLDER_NAME = '_bmad';
class Installer { class Installer {
constructor() { constructor() {

View File

@ -161,7 +161,7 @@ class ManifestGenerator {
workflow = yaml.parse(content); workflow = yaml.parse(content);
} else { } else {
// Parse MD workflow with YAML frontmatter // Parse MD workflow with YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch) { if (!frontmatterMatch) {
if (debug) { if (debug) {
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
@ -385,6 +385,11 @@ class ManifestGenerator {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
// Skip internal/engine files (not user-facing tasks)
if (content.includes('internal="true"')) {
continue;
}
let name = file.replace(/\.(xml|md)$/, ''); let name = file.replace(/\.(xml|md)$/, '');
let displayName = name; let displayName = name;
let description = ''; let description = '';
@ -392,7 +397,7 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tasks // Parse YAML frontmatter for .md tasks
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
@ -474,6 +479,11 @@ class ManifestGenerator {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
// Skip internal tools (same as tasks)
if (content.includes('internal="true"')) {
continue;
}
let name = file.replace(/\.(xml|md)$/, ''); let name = file.replace(/\.(xml|md)$/, '');
let displayName = name; let displayName = name;
let description = ''; let description = '';
@ -481,7 +491,7 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tools // Parse YAML frontmatter for .md tools
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler'); const { XmlHandler } = require('../../../lib/xml-handler');
const { getSourcePath } = require('../../../lib/project-root'); const { getSourcePath } = require('../../../lib/project-root');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
/** /**
* Base class for IDE-specific setup * Base class for IDE-specific setup
@ -18,7 +19,7 @@ class BaseIdeSetup {
this.configFile = null; // Override in subclasses when detection is file-based this.configFile = null; // Override in subclasses when detection is file-based
this.detectionPaths = []; // Additional paths that indicate the IDE is configured this.detectionPaths = []; // Additional paths that indicate the IDE is configured
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
} }
/** /**
@ -57,7 +58,7 @@ class BaseIdeSetup {
if (this.configDir) { if (this.configDir) {
const configPath = path.join(projectDir, this.configDir); const configPath = path.join(projectDir, this.configDir);
if (await fs.pathExists(configPath)) { if (await fs.pathExists(configPath)) {
const bmadRulesPath = path.join(configPath, 'bmad'); const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
if (await fs.pathExists(bmadRulesPath)) { if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath); await fs.remove(bmadRulesPath);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
@ -445,6 +446,11 @@ class BaseIdeSetup {
try { try {
const content = await fs.readFile(fullPath, 'utf8'); const content = await fs.readFile(fullPath, 'utf8');
// Skip internal/engine files (not user-facing tasks/tools)
if (content.includes('internal="true"')) {
continue;
}
// Check for standalone="true" in XML files // Check for standalone="true" in XML files
if (entry.name.endsWith('.xml')) { if (entry.name.endsWith('.xml')) {
// Look for standalone="true" in the opening tag (task or tool) // Look for standalone="true" in the opening tag (task or tool)

View File

@ -66,6 +66,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/ */
async installToTarget(projectDir, bmadDir, config, options) { async installToTarget(projectDir, bmadDir, config, options) {
const { target_dir, template_type, artifact_types } = config; const { target_dir, template_type, artifact_types } = config;
// Skip targets with explicitly empty artifact_types array
// This prevents creating empty directories when no artifacts will be written
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
}
const targetPath = path.join(projectDir, target_dir); const targetPath = path.join(projectDir, target_dir);
await this.ensureDir(targetPath); await this.ensureDir(targetPath);
@ -86,10 +93,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
} }
// Install tasks and tools // Install tasks and tools using template system (supports TOML for Gemini, MD for others)
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
const taskToolGen = new TaskToolCommandGenerator(); const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
results.tasks = taskToolResult.tasks || 0; results.tasks = taskToolResult.tasks || 0;
results.tools = taskToolResult.tools || 0; results.tools = taskToolResult.tools || 0;
} }
@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
return count; return count;
} }
/**
* Write task/tool artifacts to target directory using templates
* @param {string} targetPath - Target directory path
* @param {Array} artifacts - Task/tool artifacts
* @param {string} templateType - Template type to use
* @param {Object} config - Installation configuration
* @returns {Promise<Object>} Counts of tasks and tools written
*/
async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) {
let taskCount = 0;
let toolCount = 0;
// Pre-load templates to avoid repeated file I/O in the loop
const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task');
const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool');
const { artifact_types } = config;
for (const artifact of artifacts) {
if (artifact.type !== 'task' && artifact.type !== 'tool') {
continue;
}
// Skip if the specific artifact type is not requested in config
if (artifact_types) {
if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue;
if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue;
}
// Use pre-loaded template based on artifact type
const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate;
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, artifact.type, extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
if (artifact.type === 'task') {
taskCount++;
} else {
toolCount++;
}
}
return { tasks: taskCount, tools: toolCount };
}
/** /**
* Load template based on type and configuration * Load template based on type and configuration
* @param {string} templateType - Template type (claude, windsurf, etc.) * @param {string} templateType - Template type (claude, windsurf, etc.)
@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
renderTemplate(template, artifact) { renderTemplate(template, artifact) {
// Use the appropriate path property based on artifact type // Use the appropriate path property based on artifact type
let pathToUse = artifact.relativePath || ''; let pathToUse = artifact.relativePath || '';
if (artifact.type === 'agent-launcher') { switch (artifact.type) {
pathToUse = artifact.agentPath || artifact.relativePath || ''; case 'agent-launcher': {
} else if (artifact.type === 'workflow-command') { pathToUse = artifact.agentPath || artifact.relativePath || '';
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'workflow-command': {
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'task':
case 'tool': {
pathToUse = artifact.path || artifact.relativePath || '';
break;
}
// No default
} }
let rendered = template let rendered = template
@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
// Reuse central logic to ensure consistent naming conventions // Reuse central logic to ensure consistent naming conventions
const standardName = toDashPath(artifact.relativePath); const standardName = toDashPath(artifact.relativePath);
// Clean up potential double extensions from source files (e.g. .yaml.md -> .md) // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md'); // This handles any extensions that might slip through toDashPath()
const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md');
// If using default markdown, preserve the bmad-agent- prefix for agents // If using default markdown, preserve the bmad-agent- prefix for agents
if (extension === '.md') { if (extension === '.md') {

View File

@ -104,7 +104,10 @@ class CodexSetup extends BaseIdeSetup {
); );
taskArtifacts.push({ taskArtifacts.push({
type: 'task', type: 'task',
name: task.name,
displayName: task.name,
module: task.module, module: task.module,
path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content, content,
@ -116,7 +119,7 @@ class CodexSetup extends BaseIdeSetup {
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts); const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
// Also write tasks using underscore format // Also write tasks using underscore format
const ttGen = new TaskToolCommandGenerator(); const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts); const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
const written = agentCount + workflowCount + tasksWritten; const written = agentCount + workflowCount + tasksWritten;
@ -214,7 +217,10 @@ class CodexSetup extends BaseIdeSetup {
artifacts.push({ artifacts.push({
type: 'task', type: 'task',
name: task.name,
displayName: task.name,
module: task.module, module: task.module,
path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content, content,

View File

@ -1,6 +1,7 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const chalk = require('chalk'); const chalk = require('chalk');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
/** /**
* IDE Manager - handles IDE-specific setup * IDE Manager - handles IDE-specific setup
@ -14,7 +15,7 @@ class IdeManager {
constructor() { constructor() {
this.handlers = new Map(); this.handlers = new Map();
this._initialized = false; this._initialized = false;
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
} }
/** /**
@ -73,6 +74,9 @@ class IdeManager {
if (HandlerClass) { if (HandlerClass) {
const instance = new HandlerClass(); const instance = new HandlerClass();
if (instance.name && typeof instance.name === 'string') { if (instance.name && typeof instance.name === 'string') {
if (typeof instance.setBmadFolderName === 'function') {
instance.setBmadFolderName(this.bmadFolderName);
}
this.handlers.set(instance.name, instance); this.handlers.set(instance.name, instance);
} }
} }
@ -100,7 +104,9 @@ class IdeManager {
if (!platformInfo.installer) continue; if (!platformInfo.installer) continue;
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
handler.setBmadFolderName(this.bmadFolderName); if (typeof handler.setBmadFolderName === 'function') {
handler.setBmadFolderName(this.bmadFolderName);
}
this.handlers.set(platformCode, handler); this.handlers.set(platformCode, handler);
} }
} }

View File

@ -94,9 +94,6 @@ platforms:
- target_dir: .github/agents - target_dir: .github/agents
template_type: copilot_agents template_type: copilot_agents
artifact_types: [agents] artifact_types: [agents]
- target_dir: .vscode
template_type: vscode_settings
artifact_types: []
iflow: iflow:
name: "iFlow" name: "iFlow"

View File

@ -1,14 +1,14 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates launcher command files for each agent * Generates launcher command files for each agent
* Similar to WorkflowCommandGenerator but for agents * Similar to WorkflowCommandGenerator but for agents
*/ */
class AgentCommandGenerator { class AgentCommandGenerator {
constructor(bmadFolderName = 'bmad') { constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md'); this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
this.bmadFolderName = bmadFolderName; this.bmadFolderName = bmadFolderName;
} }

View File

@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
for (const file of files) { for (const file of files) {
if (!file.endsWith('.md')) { // Include both .md and .xml task files
if (!file.endsWith('.md') && !file.endsWith('.xml')) {
continue; continue;
} }
const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8');
// Skip internal/engine files (not user-facing tasks)
if (content.includes('internal="true"')) {
continue;
}
// Remove extension to get task name
const ext = file.endsWith('.xml') ? '.xml' : '.md';
tasks.push({ tasks.push({
path: path.join(dirPath, file), path: filePath,
name: file.replace('.md', ''), name: file.replace(ext, ''),
module: moduleName, module: moduleName,
}); });
} }

View File

@ -18,6 +18,9 @@
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents'; const AGENT_SEGMENT = 'agents';
// BMAD installation folder name - centralized constant for all installers
const BMAD_FOLDER_NAME = '_bmad';
/** /**
* Convert hierarchical path to flat dash-separated name (NEW STANDARD) * Convert hierarchical path to flat dash-separated name (NEW STANDARD)
* Converts: 'bmm', 'agents', 'pm' 'bmad-agent-bmm-pm.md' * Converts: 'bmm', 'agents', 'pm' 'bmad-agent-bmm-pm.md'
@ -59,7 +62,9 @@ function toDashPath(relativePath) {
return 'bmad-unknown.md'; return 'bmad-unknown.md';
} }
const withoutExt = relativePath.replace('.md', ''); // Strip common file extensions to avoid double extensions in generated filenames
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow'
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);
const module = parts[0]; const module = parts[0];
@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) {
* @deprecated Use toDashPath instead * @deprecated Use toDashPath instead
*/ */
function toUnderscorePath(relativePath) { function toUnderscorePath(relativePath) {
const withoutExt = relativePath.replace('.md', ''); // Strip common file extensions (same as toDashPath for consistency)
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);
const module = parts[0]; const module = parts[0];
@ -289,4 +295,5 @@ module.exports = {
TYPE_SEGMENTS, TYPE_SEGMENTS,
AGENT_SEGMENT, AGENT_SEGMENT,
BMAD_FOLDER_NAME,
}; };

View File

@ -2,12 +2,98 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonName, toColonPath, toDashPath } = require('./path-utils'); const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates command files for standalone tasks and tools * Generates command files for standalone tasks and tools
*/ */
class TaskToolCommandGenerator { class TaskToolCommandGenerator {
/**
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad')
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
* used for template placeholder rendering ({{bmadFolderName}}).
*/
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.bmadFolderName = bmadFolderName;
}
/**
* Collect task and tool artifacts for IDE installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Promise<Object>} Artifacts array with metadata
*/
async collectTaskToolArtifacts(bmadDir) {
const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir);
// Filter to only standalone items
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const artifacts = [];
const bmadPrefix = `${BMAD_FOLDER_NAME}/`;
// Collect task artifacts
for (const task of standaloneTasks) {
let taskPath = (task.path || '').replaceAll('\\', '/');
// Convert absolute paths to relative paths
if (path.isAbsolute(taskPath)) {
taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/');
}
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (taskPath.startsWith(bmadPrefix)) {
taskPath = taskPath.slice(bmadPrefix.length);
}
const taskExt = path.extname(taskPath) || '.md';
artifacts.push({
type: 'task',
name: task.name,
displayName: task.displayName || task.name,
description: task.description || `Execute ${task.displayName || task.name}`,
module: task.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
path: taskPath,
});
}
// Collect tool artifacts
for (const tool of standaloneTools) {
let toolPath = (tool.path || '').replaceAll('\\', '/');
// Convert absolute paths to relative paths
if (path.isAbsolute(toolPath)) {
toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/');
}
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (toolPath.startsWith(bmadPrefix)) {
toolPath = toolPath.slice(bmadPrefix.length);
}
const toolExt = path.extname(toolPath) || '.md';
artifacts.push({
type: 'tool',
name: tool.name,
displayName: tool.displayName || tool.name,
description: tool.description || `Execute ${tool.displayName || tool.name}`,
module: tool.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
path: toolPath,
});
}
return {
artifacts,
counts: {
tasks: standaloneTasks.length,
tools: standaloneTools.length,
},
};
}
/** /**
* Generate task and tool commands from manifest CSVs * Generate task and tool commands from manifest CSVs
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
@ -65,9 +151,35 @@ class TaskToolCommandGenerator {
const description = item.description || `Execute ${item.displayName || item.name}`; const description = item.description || `Execute ${item.displayName || item.name}`;
// Convert path to use {project-root} placeholder // Convert path to use {project-root} placeholder
// Handle undefined/missing path by constructing from module and name
let itemPath = item.path; let itemPath = item.path;
if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { if (!itemPath || typeof itemPath !== 'string') {
itemPath = `{project-root}/${itemPath}`; // Fallback: construct path from module and name if path is missing
const typePlural = type === 'task' ? 'tasks' : 'tools';
itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`;
} else {
// Normalize path separators to forward slashes
itemPath = itemPath.replaceAll('\\', '/');
// Extract relative path from absolute paths (Windows or Unix)
// Look for _bmad/ or bmad/ in the path and extract everything after it
// Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/...
// Use [/\\] to handle both Unix forward slashes and Windows backslashes,
// and also paths without a leading separator (e.g., C:/_bmad/...)
const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/);
if (bmadMatch) {
// Found /_bmad/ or /bmad/ - use relative path after it
itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`;
} else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) {
// Relative path starting with _bmad/
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`;
} else if (itemPath.startsWith('bmad/')) {
// Relative path starting with bmad/
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`;
} else if (!itemPath.startsWith('{project-root}')) {
// For other relative paths, prefix with project root and bmad folder
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`;
}
} }
return `--- return `---
@ -187,7 +299,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md // Use dash format: bmad-bmm-name.md
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
@ -198,7 +310,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools // Generate command files for tools
for (const tool of standaloneTools) { for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool'); const commandContent = this.generateCommandContent(tool, 'tool');
// Use underscore format: bmad_bmm_name.md // Use dash format: bmad-bmm-name.md
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));

View File

@ -2,13 +2,13 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates command files for each workflow in the manifest * Generates command files for each workflow in the manifest
*/ */
class WorkflowCommandGenerator { class WorkflowCommandGenerator {
constructor(bmadFolderName = 'bmad') { constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
this.bmadFolderName = bmadFolderName; this.bmadFolderName = bmadFolderName;
} }

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the task file exactly as written.

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the tool file exactly as written.

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} task from the BMAD Method."
prompt = """
Execute the BMAD '{{name}}' task.
TASK INSTRUCTIONS:
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} tool from the BMAD Method."
prompt = """
Execute the BMAD '{{name}}' tool.
TOOL INSTRUCTIONS:
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""

View File

@ -7,6 +7,7 @@ const { XmlHandler } = require('../../../lib/xml-handler');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { filterCustomizationData } = require('../../../lib/agent/compiler');
const { ExternalModuleManager } = require('./external-manager'); const { ExternalModuleManager } = require('./external-manager');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
/** /**
* Manages the installation, updating, and removal of BMAD modules. * Manages the installation, updating, and removal of BMAD modules.
@ -27,7 +28,7 @@ const { ExternalModuleManager } = require('./external-manager');
class ModuleManager { class ModuleManager {
constructor(options = {}) { constructor(options = {}) {
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
this.customModulePaths = new Map(); // Initialize custom module paths this.customModulePaths = new Map(); // Initialize custom module paths
this.externalModuleManager = new ExternalModuleManager(); // For external official modules this.externalModuleManager = new ExternalModuleManager(); // For external official modules
} }

View File

@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) {
* @returns {string} Resolved path * @returns {string} Resolved path
*/ */
function resolvePath(pathStr, context) { function resolvePath(pathStr, context) {
return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder); return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder);
} }
/** /**