diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 0b977884f..05029e1e0 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -49,34 +49,37 @@ function assert(condition, testName, errorMessage = '') { } async function createTestBmadFixture() { - const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-')); + const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-')); + const fixtureDir = path.join(fixtureRoot, '_bmad'); + await fs.ensureDir(fixtureDir); - // Minimal workflow manifest (generators check for this) + // Skill manifest CSV — the sole source of truth for IDE skill installation await fs.ensureDir(path.join(fixtureDir, '_config')); - await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), ''); + await fs.writeFile( + path.join(fixtureDir, '_config', 'skill-manifest.csv'), + [ + 'canonicalId,name,description,module,path,install_to_bmad', + '"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"', + '', + ].join('\n'), + ); - // Minimal compiled agent for core/agents (contains ', - 'Test persona', - '', - ].join('\n'); - - await fs.ensureDir(path.join(fixtureDir, 'core', 'agents')); - await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent); - // Skill manifest so the installer uses 'bmad-master' as the canonical skill name - await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n'); - - // Minimal compiled agent for bmm module (tests use selectedModules: ['bmm']) - await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents')); - await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent); + // Minimal SKILL.md for the skill entry + const skillDir = path.join(fixtureDir, 'core', 'bmad-master'); + await fs.ensureDir(skillDir); + await fs.writeFile( + path.join(skillDir, 'SKILL.md'), + [ + '---', + 'name: bmad-master', + 'description: Minimal test agent fixture', + '---', + '', + '', + 'You are a test agent.', + ].join('\n'), + ); + await fs.writeFile(path.join(skillDir, 'bmad-skill-manifest.yaml'), 'SKILL.md:\n type: skill\n'); return fixtureDir; } @@ -1837,18 +1840,12 @@ async function runTests() { }); assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names'); - assert(result.detail === '2 agents', 'Installer detail reports agents separately from skills'); - assert(result.handlerResult.results.skillDirectories === 2, 'Result exposes unique skill directory count'); - assert(result.handlerResult.results.agents === 2, 'Result retains generated agent write count'); - assert(result.handlerResult.results.workflows === 1, 'Result retains generated workflow count'); + assert(result.detail === '1 skills', 'Installer detail reports skill count'); + assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count'); assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count'); - assert( - await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-agent-bmad-master', 'SKILL.md')), - 'Agent skill directory is created', - ); assert( await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-help', 'SKILL.md')), - 'Overlapping skill directory is created once', + 'Skill directory is created from skill-manifest', ); } catch (error) { assert(false, 'Skill-format unique count test succeeds', error.message); diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index e94cb9edb..99a92e528 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -4,9 +4,6 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const { BaseIdeSetup } = require('./_base-ide'); const prompts = require('../../../lib/prompts'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const csv = require('csv-parse/sync'); /** @@ -115,53 +112,20 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Installation result */ async installToTarget(projectDir, bmadDir, config, options) { - const { target_dir, template_type, artifact_types } = config; + const { target_dir } = config; - // Skip targets with explicitly empty artifact_types and no verbatim skills - // This prevents creating empty directories when no artifacts will be written - const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0; - if (skipStandardArtifacts && !config.skill_format) { - return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } }; + if (!config.skill_format) { + return { success: true, results: { skills: 0 } }; } const targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); - const selectedModules = options.selectedModules || []; - const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; - this.skillWriteTracker = config.skill_format ? new Set() : null; + this.skillWriteTracker = new Set(); + const results = { skills: 0 }; - // Install standard artifacts (agents, workflows, tasks, tools) - if (!skipStandardArtifacts) { - // Install agents - if (!artifact_types || artifact_types.includes('agents')) { - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); - results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config); - } - - // Install workflows - if (!artifact_types || artifact_types.includes('workflows')) { - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); - } - - // 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')) { - const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); - const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); - const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config); - results.tasks = taskToolResult.tasks || 0; - results.tools = taskToolResult.tools || 0; - } - } - - // Install verbatim skills (type: skill) - if (config.skill_format) { - results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); - results.skillDirectories = this.skillWriteTracker ? this.skillWriteTracker.size : 0; - } + results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); + results.skillDirectories = this.skillWriteTracker.size; await this.printSummary(results, target_dir, options); this.skillWriteTracker = null; @@ -177,15 +141,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Installation result */ async installToMultipleTargets(projectDir, bmadDir, targets, options) { - const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; + const allResults = { skills: 0 }; for (const target of targets) { const result = await this.installToTarget(projectDir, bmadDir, target, options); if (result.success) { - allResults.agents += result.results.agents || 0; - allResults.workflows += result.results.workflows || 0; - allResults.tasks += result.results.tasks || 0; - allResults.tools += result.results.tools || 0; allResults.skills += result.results.skills || 0; } } @@ -193,118 +153,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return { success: true, results: allResults }; } - /** - * Write agent artifacts to target directory - * @param {string} targetPath - Target directory path - * @param {Array} artifacts - Agent artifacts - * @param {string} templateType - Template type to use - * @param {Object} config - Installation configuration - * @returns {Promise} Count of artifacts written - */ - async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) { - // Try to load platform-specific template, fall back to default-agent - const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent'); - let count = 0; - - for (const artifact of artifacts) { - const content = this.renderTemplate(template, artifact); - const filename = this.generateFilename(artifact, 'agent', extension); - - if (config.skill_format) { - await this.writeSkillFile(targetPath, artifact, content); - } else { - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); - } - count++; - } - - return count; - } - - /** - * Write workflow artifacts to target directory - * @param {string} targetPath - Target directory path - * @param {Array} artifacts - Workflow artifacts - * @param {string} templateType - Template type to use - * @param {Object} config - Installation configuration - * @returns {Promise} Count of artifacts written - */ - async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) { - let count = 0; - - for (const artifact of artifacts) { - if (artifact.type === 'workflow-command') { - const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`; - const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, 'default-workflow'); - const content = this.renderTemplate(template, artifact); - const filename = this.generateFilename(artifact, 'workflow', extension); - - if (config.skill_format) { - await this.writeSkillFile(targetPath, artifact, content); - } else { - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); - } - 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} 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); - - if (config.skill_format) { - await this.writeSkillFile(targetPath, artifact, content); - } else { - 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 * @param {string} templateType - Template type (claude, windsurf, etc.) @@ -711,13 +559,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} */ async printSummary(results, targetDir, options = {}) { if (options.silent) return; - const parts = []; - const totalDirs = - results.skillDirectories || (results.workflows || 0) + (results.tasks || 0) + (results.tools || 0) + (results.skills || 0); - const skillCount = totalDirs - (results.agents || 0); - if (skillCount > 0) parts.push(`${skillCount} skills`); - if (results.agents > 0) parts.push(`${results.agents} agents`); - await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`); + const count = results.skillDirectories || results.skills || 0; + if (count > 0) { + await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`); + } } /** diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index d0dee4ae0..0d7f91209 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -159,14 +159,9 @@ class IdeManager { // Build detail string from handler-returned data let detail = ''; if (handlerResult && handlerResult.results) { - // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } const r = handlerResult.results; - const parts = []; - const totalDirs = r.skillDirectories || (r.workflows || 0) + (r.tasks || 0) + (r.tools || 0) + (r.skills || 0); - const skillCount = totalDirs - (r.agents || 0); - if (skillCount > 0) parts.push(`${skillCount} skills`); - if (r.agents > 0) parts.push(`${r.agents} agents`); - detail = parts.join(', '); + const count = r.skillDirectories || r.skills || 0; + if (count > 0) detail = `${count} skills`; } // Propagate handler's success status (default true for backward compat) const success = handlerResult?.success !== false; diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 37820992e..0fc1b04dc 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -4,7 +4,6 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD /** * Generates launcher command files for each agent - * Similar to WorkflowCommandGenerator but for agents */ class AgentCommandGenerator { constructor(bmadFolderName = BMAD_FOLDER_NAME) { diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js deleted file mode 100644 index f21a5d174..000000000 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ /dev/null @@ -1,368 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const csv = require('csv-parse/sync'); -const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = 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_FOLDER_NAME) { - 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); - - // All tasks/tools in manifest are standalone (internal=true items are filtered during manifest generation) - const artifacts = []; - const bmadPrefix = `${BMAD_FOLDER_NAME}/`; - - // Collect task artifacts - for (const task of tasks || []) { - 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, - canonicalId: task.canonicalId || '', - // 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 tools || []) { - 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, - canonicalId: tool.canonicalId || '', - // 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: (tasks || []).length, - tools: (tools || []).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); - - // 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 tasks || []) { - 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 tools || []) { - 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: (tasks || []).length, - tools: (tools || []).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/... - // 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 `--- -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_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); - - let generatedCount = 0; - - // Generate command files for tasks - for (const task of tasks || []) { - 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 tools || []) { - 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: (tasks || []).length, - tools: (tools || []).length, - }; - } - - /** - * Generate task and tool commands using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_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); - - let generatedCount = 0; - - // Generate command files for tasks - for (const task of tasks || []) { - 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 tools || []) { - 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: (tasks || []).length, - tools: (tools || []).length, - }; - } - - /** - * Write task/tool artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_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-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 }; diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js deleted file mode 100644 index 996c8728d..000000000 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ /dev/null @@ -1,179 +0,0 @@ -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-skills/')) { - const match = workflowPath.match(/\/src\/bmm-skills\/(.+)/); - if (match) { - transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`; - } - } else if (workflowPath.includes('/src/core-skills/')) { - const match = workflowPath.match(/\/src\/core-skills\/(.+)/); - 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 };