const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); const { toColonName, toColonPath, toDashPath } = require('./path-utils'); /** * Generates command files for standalone tasks and tools */ 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') { this.bmadFolderName = bmadFolderName; } /** * Collect task and tool artifacts for IDE installation * @param {string} bmadDir - BMAD installation directory * @returns {Promise} 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 = []; // Collect task artifacts for (const task of standaloneTasks) { let taskPath = (task.path || '').replaceAll('\\', '/'); // Remove _bmad/ prefix if present to get relative path within bmad folder if (taskPath.startsWith('_bmad/')) { taskPath = taskPath.slice(6); // Remove '_bmad/' } 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('\\', '/'); // Remove _bmad/ prefix if present to get relative path within bmad folder if (toolPath.startsWith('_bmad/')) { toolPath = toolPath.slice(6); // Remove '_bmad/' } 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 * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad) */ async generateTaskToolCommands(projectDir, bmadDir, baseCommandsDir = null) { 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) : []; // Base commands directory - use provided or default to Claude Code structure const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad'); let generatedCount = 0; // Generate command files for tasks for (const task of standaloneTasks) { const moduleTasksDir = path.join(commandsDir, task.module, 'tasks'); await fs.ensureDir(moduleTasksDir); const commandContent = this.generateCommandContent(task, 'task'); const commandPath = path.join(moduleTasksDir, `${task.name}.md`); await fs.writeFile(commandPath, commandContent); generatedCount++; } // Generate command files for tools for (const tool of standaloneTools) { const moduleToolsDir = path.join(commandsDir, tool.module, 'tools'); await fs.ensureDir(moduleToolsDir); const commandContent = this.generateCommandContent(tool, 'tool'); const commandPath = path.join(moduleToolsDir, `${tool.name}.md`); await fs.writeFile(commandPath, commandContent); generatedCount++; } return { generated: generatedCount, tasks: standaloneTasks.length, tools: standaloneTools.length, }; } /** * Generate command content for a task or tool */ generateCommandContent(item, type) { const description = item.description || `Execute ${item.displayName || item.name}`; // Convert path to use {project-root} placeholder // Handle undefined/missing path by constructing from module and name let itemPath = item.path; if (!itemPath || typeof itemPath !== 'string') { // 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/... 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/')) { // Relative path starting with _bmad/ itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(6)}`; } 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 `--- description: '${description.replaceAll("'", "''")}' --- # ${item.displayName || item.name} Read the entire ${type} file at: ${itemPath} Follow all instructions in the ${type} file exactly as written. `; } /** * Load task manifest CSV */ async loadTaskManifest(bmadDir) { const manifestPath = path.join(bmadDir, '_config', 'task-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, }); } /** * Load tool manifest CSV */ async loadToolManifest(bmadDir) { const manifestPath = path.join(bmadDir, '_config', 'tool-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, }); } /** * Generate task and tool commands using underscore format (Windows-compatible) * Creates flat files like: bmad_bmm_bmad-help.md * * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} baseCommandsDir - Base commands directory for the IDE * @returns {Object} Generation results */ async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { 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) : []; let generatedCount = 0; // Generate command files for tasks for (const task of standaloneTasks) { const commandContent = this.generateCommandContent(task, 'task'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(task.module, 'tasks', task.name); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); generatedCount++; } // Generate command files for tools for (const tool of standaloneTools) { const commandContent = this.generateCommandContent(tool, 'tool'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(tool.module, 'tools', tool.name); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); generatedCount++; } return { generated: generatedCount, tasks: standaloneTasks.length, tools: standaloneTools.length, }; } /** * Generate task and tool commands using underscore format (Windows-compatible) * Creates flat files like: bmad_bmm_bmad-help.md * * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} baseCommandsDir - Base commands directory for the IDE * @returns {Object} Generation results */ async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { 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) : []; let generatedCount = 0; // Generate command files for tasks for (const task of standaloneTasks) { const commandContent = this.generateCommandContent(task, 'task'); // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); generatedCount++; } // Generate command files for tools for (const tool of standaloneTools) { const commandContent = this.generateCommandContent(tool, 'tool'); // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); generatedCount++; } return { generated: generatedCount, tasks: standaloneTasks.length, tools: standaloneTools.length, }; } /** * Write task/tool artifacts using underscore format (Windows-compatible) * Creates flat files like: bmad_bmm_bmad-help.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Task/tool artifacts with relativePath * @returns {number} Count of commands written */ async writeColonArtifacts(baseCommandsDir, artifacts) { let writtenCount = 0; for (const artifact of artifacts) { if (artifact.type === 'task' || artifact.type === 'tool') { const commandContent = this.generateCommandContent(artifact, artifact.type); // Use underscore format: bmad_module_name.md const flatName = toColonPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); writtenCount++; } } return writtenCount; } /** * Write task/tool artifacts using dash format (NEW STANDARD) * Creates flat files like: bmad-bmm-bmad-help.md * * Note: Tasks/tools do NOT have bmad-agent- prefix - only agents do. * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Task/tool artifacts with relativePath * @returns {number} Count of commands written */ async writeDashArtifacts(baseCommandsDir, artifacts) { let writtenCount = 0; for (const artifact of artifacts) { if (artifact.type === 'task' || artifact.type === 'tool') { const commandContent = this.generateCommandContent(artifact, artifact.type); // Use dash format: bmad-module-name.md const flatName = toDashPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); writtenCount++; } } return writtenCount; } } module.exports = { TaskToolCommandGenerator };