diff --git a/src/core/tasks/index-docs.xml b/src/core/tasks/index-docs.xml index 75eeb5a7..3a485d18 100644 --- a/src/core/tasks/index-docs.xml +++ b/src/core/tasks/index-docs.xml @@ -1,4 +1,5 @@ - + MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER DO NOT skip steps or change the sequence @@ -17,7 +18,8 @@ - Read each file to understand its actual purpose and create brief (3-10 word) descriptions based on the content, not just the filename + Read each file to understand its actual purpose and create brief (3-10 word) descriptions based on the content, not just the + filename diff --git a/src/core/tools/shard-doc.xml b/src/core/tools/shard-doc.xml index 70a9c669..3a6ab307 100644 --- a/src/core/tools/shard-doc.xml +++ b/src/core/tools/shard-doc.xml @@ -1,4 +1,6 @@ - + Split large markdown documents into smaller, organized files based on level 2 sections using @kayvan/markdown-tree-parser tool diff --git a/src/core/workflows/party-mode/workflow.yaml b/src/core/workflows/party-mode/workflow.yaml index bfe03438..6baa4b62 100644 --- a/src/core/workflows/party-mode/workflow.yaml +++ b/src/core/workflows/party-mode/workflow.yaml @@ -18,4 +18,6 @@ exit_triggers: - "end party mode" - "stop party mode" +standalone: true + web_bundle: false diff --git a/tools/cli/installers/lib/core/dependency-resolver.js b/tools/cli/installers/lib/core/dependency-resolver.js index b829f881..c53aec58 100644 --- a/tools/cli/installers/lib/core/dependency-resolver.js +++ b/tools/cli/installers/lib/core/dependency-resolver.js @@ -599,6 +599,7 @@ class DependencyResolver { organized[module] = { agents: [], tasks: [], + tools: [], templates: [], data: [], other: [], @@ -626,6 +627,8 @@ class DependencyResolver { organized[module].agents.push(file); } else if (relative.startsWith('tasks/') || file.includes('/tasks/')) { organized[module].tasks.push(file); + } else if (relative.startsWith('tools/') || file.includes('/tools/')) { + organized[module].tools.push(file); } else if (relative.includes('template') || file.includes('/templates/')) { organized[module].templates.push(file); } else if (relative.includes('data/')) { @@ -646,7 +649,8 @@ class DependencyResolver { for (const [module, files] of Object.entries(organized)) { const isSelected = selectedModules.includes(module) || module === 'core'; - const totalFiles = files.agents.length + files.tasks.length + files.templates.length + files.data.length + files.other.length; + const totalFiles = + files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length; if (totalFiles > 0) { console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`)); diff --git a/tools/cli/installers/lib/core/detector.js b/tools/cli/installers/lib/core/detector.js index d8df39c5..ccff80d5 100644 --- a/tools/cli/installers/lib/core/detector.js +++ b/tools/cli/installers/lib/core/detector.js @@ -117,7 +117,8 @@ class Detector { // Check for IDE configurations from manifest if (result.manifest && result.manifest.ides) { - result.ides = result.manifest.ides; + // Filter out any undefined/null values + result.ides = result.manifest.ides.filter((ide) => ide && typeof ide === 'string'); } // Mark as installed if we found core or modules diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index dc5367c2..ea2e6dae 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -439,7 +439,13 @@ class Installer { // Install partial modules (only dependencies) for (const [module, files] of Object.entries(resolution.byModule)) { if (!config.modules.includes(module) && module !== 'core') { - const totalFiles = files.agents.length + files.tasks.length + files.templates.length + files.data.length + files.other.length; + const totalFiles = + files.agents.length + + files.tasks.length + + files.tools.length + + files.templates.length + + files.data.length + + files.other.length; if (totalFiles > 0) { spinner.start(`Installing ${module} dependencies...`); await this.installPartialModule(module, bmadDir, files); @@ -480,67 +486,77 @@ class Installer { }); spinner.succeed( - `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.files} files`, + `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, ); // Configure IDEs and copy documentation if (!config.skipIde && config.ides && config.ides.length > 0) { - // Check if any IDE might need prompting (no pre-collected config) - const needsPrompting = config.ides.some((ide) => !ideConfigurations[ide]); + // Filter out any undefined/null values from the IDE list + const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); - if (!needsPrompting) { - spinner.start('Configuring IDEs...'); - } + if (validIdes.length === 0) { + console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.')); + } else { + // Check if any IDE might need prompting (no pre-collected config) + const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); - // Temporarily suppress console output if not verbose - const originalLog = console.log; - if (!config.verbose) { - console.log = () => {}; - } - - for (const ide of config.ides) { - // Only show spinner if we have pre-collected config (no prompts expected) - if (ideConfigurations[ide] && !needsPrompting) { - spinner.text = `Configuring ${ide}...`; - } else if (!ideConfigurations[ide]) { - // Stop spinner before prompting - if (spinner.isSpinning) { - spinner.stop(); - } - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); - } - - // Pass pre-collected configuration to avoid re-prompting - await this.ideManager.setup(ide, projectDir, bmadDir, { - selectedModules: config.modules || [], - preCollectedConfig: ideConfigurations[ide] || null, - verbose: config.verbose, - }); - - // Save IDE configuration for future updates - if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { - await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); - } - - // Restart spinner if we stopped it - if (!ideConfigurations[ide] && !spinner.isSpinning) { + if (!needsPrompting) { spinner.start('Configuring IDEs...'); } + + // Temporarily suppress console output if not verbose + const originalLog = console.log; + if (!config.verbose) { + console.log = () => {}; + } + + for (const ide of validIdes) { + // Only show spinner if we have pre-collected config (no prompts expected) + if (ideConfigurations[ide] && !needsPrompting) { + spinner.text = `Configuring ${ide}...`; + } else if (!ideConfigurations[ide]) { + // Stop spinner before prompting + if (spinner.isSpinning) { + spinner.stop(); + } + console.log(chalk.cyan(`\nConfiguring ${ide}...`)); + } + + // Pass pre-collected configuration to avoid re-prompting + await this.ideManager.setup(ide, projectDir, bmadDir, { + selectedModules: config.modules || [], + preCollectedConfig: ideConfigurations[ide] || null, + verbose: config.verbose, + }); + + // Save IDE configuration for future updates + if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { + await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); + } + + // Restart spinner if we stopped it + if (!ideConfigurations[ide] && !spinner.isSpinning) { + spinner.start('Configuring IDEs...'); + } + } + + // Restore console.log + console.log = originalLog; + + if (spinner.isSpinning) { + spinner.succeed(`Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`); + } else { + console.log(chalk.green(`✓ Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`)); + } } - // Restore console.log - console.log = originalLog; - - if (spinner.isSpinning) { - spinner.succeed(`Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`); - } else { - console.log(chalk.green(`✓ Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`)); + // Copy IDE-specific documentation (only for valid IDEs) + const validIdesForDocs = (config.ides || []).filter((ide) => ide && typeof ide === 'string'); + if (validIdesForDocs.length > 0) { + spinner.start('Copying IDE documentation...'); + await this.copyIdeDocumentation(validIdesForDocs, bmadDir); + spinner.succeed('IDE documentation copied'); } - - // Copy IDE-specific documentation - spinner.start('Copying IDE documentation...'); - await this.copyIdeDocumentation(config.ides, bmadDir); - spinner.succeed('IDE documentation copied'); } // Run module-specific installers after IDE setup @@ -959,6 +975,22 @@ class Installer { } } + if (files.tools && files.tools.length > 0) { + const toolsDir = path.join(targetBase, 'tools'); + await fs.ensureDir(toolsDir); + + for (const toolPath of files.tools) { + const fileName = path.basename(toolPath); + const sourcePath = path.join(sourceBase, 'tools', fileName); + const targetPath = path.join(toolsDir, fileName); + + if (await fs.pathExists(sourcePath)) { + await fs.copy(sourcePath, targetPath); + this.installedFiles.push(targetPath); + } + } + } + if (files.templates && files.templates.length > 0) { const templatesDir = path.join(targetBase, 'templates'); await fs.ensureDir(templatesDir); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index b3543e88..61a9bbe4 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -12,6 +12,7 @@ class ManifestGenerator { this.workflows = []; this.agents = []; this.tasks = []; + this.tools = []; this.modules = []; this.files = []; this.selectedIdes = []; @@ -45,7 +46,8 @@ class ManifestGenerator { throw new TypeError('ManifestGenerator expected `options.ides` to be an array.'); } - this.selectedIdes = resolvedIdes; + // Filter out any undefined/null values from IDE list + this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string'); // Collect workflow data await this.collectWorkflows(selectedModules); @@ -56,12 +58,16 @@ class ManifestGenerator { // Collect task data await this.collectTasks(selectedModules); + // Collect tool data + await this.collectTools(selectedModules); + // Write manifest files and collect their paths const manifestFiles = [ await this.writeMainManifest(cfgDir), await this.writeWorkflowManifest(cfgDir), await this.writeAgentManifest(cfgDir), await this.writeTaskManifest(cfgDir), + await this.writeToolManifest(cfgDir), await this.writeFilesManifest(cfgDir), ]; @@ -69,6 +75,7 @@ class ManifestGenerator { workflows: this.workflows.length, agents: this.agents.length, tasks: this.tasks.length, + tools: this.tools.length, files: this.files.length, manifestFiles: manifestFiles, }; @@ -133,11 +140,15 @@ class ManifestGenerator { ? `bmad/core/workflows/${relativePath}/workflow.yaml` : `bmad/${moduleName}/workflows/${relativePath}/workflow.yaml`; + // Check for standalone property (default: false) + const standalone = workflow.standalone === true; + workflows.push({ name: workflow.name, description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV module: moduleName, path: installPath, + standalone: standalone, }); // Add to files list @@ -306,24 +317,34 @@ class ManifestGenerator { const files = await fs.readdir(dirPath); for (const file of files) { - if (file.endsWith('.md')) { + // Check for both .xml and .md files + if (file.endsWith('.xml') || file.endsWith('.md')) { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); // Extract task metadata from content if possible const nameMatch = content.match(/name="([^"]+)"/); + + // Try description attribute first, fall back to element + const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); + const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + + // Check for standalone attribute in tag (default: false) + const standaloneMatch = content.match(/]+standalone="true"/); + const standalone = !!standaloneMatch; // Build relative path for installation const installPath = moduleName === 'core' ? `bmad/core/tasks/${file}` : `bmad/${moduleName}/tasks/${file}`; - const taskName = file.replace('.md', ''); + const taskName = file.replace(/\.(xml|md)$/, ''); tasks.push({ name: taskName, displayName: nameMatch ? nameMatch[1] : taskName, - description: objMatch ? objMatch[1].trim().replaceAll('"', '""') : '', + description: description.replaceAll('"', '""'), module: moduleName, path: installPath, + standalone: standalone, }); // Add to files list @@ -339,6 +360,82 @@ class ManifestGenerator { return tasks; } + /** + * Collect all tools from core and selected modules + * Scans the INSTALLED bmad directory, not the source + */ + async collectTools(selectedModules) { + this.tools = []; + + // Get core tools from installed bmad directory + const coreToolsPath = path.join(this.bmadDir, 'core', 'tools'); + if (await fs.pathExists(coreToolsPath)) { + const coreTools = await this.getToolsFromDir(coreToolsPath, 'core'); + this.tools.push(...coreTools); + } + + // Get module tools from installed bmad directory + for (const moduleName of selectedModules) { + const toolsPath = path.join(this.bmadDir, moduleName, 'tools'); + + if (await fs.pathExists(toolsPath)) { + const moduleTools = await this.getToolsFromDir(toolsPath, moduleName); + this.tools.push(...moduleTools); + } + } + } + + /** + * Get tools from a directory + */ + async getToolsFromDir(dirPath, moduleName) { + const tools = []; + const files = await fs.readdir(dirPath); + + for (const file of files) { + // Check for both .xml and .md files + if (file.endsWith('.xml') || file.endsWith('.md')) { + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, 'utf8'); + + // Extract tool metadata from content if possible + const nameMatch = content.match(/name="([^"]+)"/); + + // Try description attribute first, fall back to element + const descMatch = content.match(/description="([^"]+)"/); + const objMatch = content.match(/([^<]+)<\/objective>/); + const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + + // Check for standalone attribute in tag (default: false) + const standaloneMatch = content.match(/]+standalone="true"/); + const standalone = !!standaloneMatch; + + // Build relative path for installation + const installPath = moduleName === 'core' ? `bmad/core/tools/${file}` : `bmad/${moduleName}/tools/${file}`; + + const toolName = file.replace(/\.(xml|md)$/, ''); + tools.push({ + name: toolName, + displayName: nameMatch ? nameMatch[1] : toolName, + description: description.replaceAll('"', '""'), + module: moduleName, + path: installPath, + standalone: standalone, + }); + + // Add to files list + this.files.push({ + type: 'tool', + name: toolName, + module: moduleName, + path: installPath, + }); + } + } + + return tools; + } + /** * Write main manifest as YAML with installation info only * @returns {string} Path to the manifest file @@ -416,12 +513,12 @@ class ManifestGenerator { // Get preserved rows from existing CSV (module is column 2, 0-indexed) const preservedRows = await this.getPreservedCsvRows(csvPath, 2); - // Create CSV header - let csv = 'name,description,module,path\n'; + // Create CSV header with standalone column + let csv = 'name,description,module,path,standalone\n'; // Add new rows for updated modules for (const workflow of this.workflows) { - csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`; + csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}","${workflow.standalone}"\n`; } // Add preserved rows for modules we didn't update @@ -470,12 +567,39 @@ class ManifestGenerator { // Get preserved rows from existing CSV (module is column 3, 0-indexed) const preservedRows = await this.getPreservedCsvRows(csvPath, 3); - // Create CSV header - let csv = 'name,displayName,description,module,path\n'; + // Create CSV header with standalone column + let csv = 'name,displayName,description,module,path,standalone\n'; // Add new rows for updated modules for (const task of this.tasks) { - csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}"\n`; + csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"\n`; + } + + // Add preserved rows for modules we didn't update + for (const row of preservedRows) { + csv += row + '\n'; + } + + await fs.writeFile(csvPath, csv); + return csvPath; + } + + /** + * Write tool manifest CSV + * @returns {string} Path to the manifest file + */ + async writeToolManifest(cfgDir) { + const csvPath = path.join(cfgDir, 'tool-manifest.csv'); + + // Get preserved rows from existing CSV (module is column 3, 0-indexed) + const preservedRows = await this.getPreservedCsvRows(csvPath, 3); + + // Create CSV header with standalone column + let csv = 'name,displayName,description,module,path,standalone\n'; + + // Add new rows for updated modules + for (const tool of this.tools) { + csv += `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"\n`; } // Add preserved rows for modules we didn't update diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index 05d40d6d..269bf9fd 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -156,15 +156,16 @@ class BaseIdeSetup { /** * Get list of tasks from BMAD installation * @param {string} bmadDir - BMAD installation directory + * @param {boolean} standaloneOnly - If true, only return standalone tasks * @returns {Array} List of task files */ - async getTasks(bmadDir) { + async getTasks(bmadDir, standaloneOnly = false) { const tasks = []; - // Get core tasks + // Get core tasks (scan for both .md and .xml) const coreTasksPath = path.join(bmadDir, 'core', 'tasks'); if (await fs.pathExists(coreTasksPath)) { - const coreTasks = await this.scanDirectory(coreTasksPath, '.md'); + const coreTasks = await this.scanDirectoryWithStandalone(coreTasksPath, ['.md', '.xml']); tasks.push( ...coreTasks.map((t) => ({ ...t, @@ -179,7 +180,7 @@ class BaseIdeSetup { if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') { const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks'); if (await fs.pathExists(moduleTasksPath)) { - const moduleTasks = await this.scanDirectory(moduleTasksPath, '.md'); + const moduleTasks = await this.scanDirectoryWithStandalone(moduleTasksPath, ['.md', '.xml']); tasks.push( ...moduleTasks.map((t) => ({ ...t, @@ -190,13 +191,157 @@ class BaseIdeSetup { } } + // Filter by standalone if requested + if (standaloneOnly) { + return tasks.filter((t) => t.standalone === true); + } + return tasks; } /** - * Scan a directory for files with specific extension + * Get list of tools from BMAD installation + * @param {string} bmadDir - BMAD installation directory + * @param {boolean} standaloneOnly - If true, only return standalone tools + * @returns {Array} List of tool files + */ + async getTools(bmadDir, standaloneOnly = false) { + const tools = []; + + // Get core tools (scan for both .md and .xml) + const coreToolsPath = path.join(bmadDir, 'core', 'tools'); + if (await fs.pathExists(coreToolsPath)) { + const coreTools = await this.scanDirectoryWithStandalone(coreToolsPath, ['.md', '.xml']); + tools.push( + ...coreTools.map((t) => ({ + ...t, + module: 'core', + })), + ); + } + + // Get module tools + const entries = await fs.readdir(bmadDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') { + const moduleToolsPath = path.join(bmadDir, entry.name, 'tools'); + if (await fs.pathExists(moduleToolsPath)) { + const moduleTools = await this.scanDirectoryWithStandalone(moduleToolsPath, ['.md', '.xml']); + tools.push( + ...moduleTools.map((t) => ({ + ...t, + module: entry.name, + })), + ); + } + } + } + + // Filter by standalone if requested + if (standaloneOnly) { + return tools.filter((t) => t.standalone === true); + } + + return tools; + } + + /** + * Get list of workflows from BMAD installation + * @param {string} bmadDir - BMAD installation directory + * @param {boolean} standaloneOnly - If true, only return standalone workflows + * @returns {Array} List of workflow files + */ + async getWorkflows(bmadDir, standaloneOnly = false) { + const workflows = []; + + // Get core workflows + const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows'); + if (await fs.pathExists(coreWorkflowsPath)) { + const coreWorkflows = await this.findWorkflowYamlFiles(coreWorkflowsPath); + workflows.push( + ...coreWorkflows.map((w) => ({ + ...w, + module: 'core', + })), + ); + } + + // Get module workflows + const entries = await fs.readdir(bmadDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') { + const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows'); + if (await fs.pathExists(moduleWorkflowsPath)) { + const moduleWorkflows = await this.findWorkflowYamlFiles(moduleWorkflowsPath); + workflows.push( + ...moduleWorkflows.map((w) => ({ + ...w, + module: entry.name, + })), + ); + } + } + } + + // Filter by standalone if requested + if (standaloneOnly) { + return workflows.filter((w) => w.standalone === true); + } + + return workflows; + } + + /** + * Recursively find workflow.yaml files + * @param {string} dir - Directory to search + * @returns {Array} List of workflow file info objects + */ + async findWorkflowYamlFiles(dir) { + const workflows = []; + + if (!(await fs.pathExists(dir))) { + return workflows; + } + + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively search subdirectories + const subWorkflows = await this.findWorkflowYamlFiles(fullPath); + workflows.push(...subWorkflows); + } else if (entry.isFile() && entry.name === 'workflow.yaml') { + // Read workflow.yaml to get name and standalone property + try { + const yaml = require('js-yaml'); + const content = await fs.readFile(fullPath, 'utf8'); + const workflowData = yaml.load(content); + + if (workflowData && workflowData.name) { + workflows.push({ + name: workflowData.name, + path: fullPath, + relativePath: path.relative(dir, fullPath), + filename: entry.name, + description: workflowData.description || '', + standalone: workflowData.standalone === true, // Check standalone property + }); + } + } catch { + // Skip invalid workflow files + } + } + } + + return workflows; + } + + /** + * Scan a directory for files with specific extension(s) * @param {string} dir - Directory to scan - * @param {string} ext - File extension to match + * @param {string|Array} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml']) * @returns {Array} List of file info objects */ async scanDirectory(dir, ext) { @@ -206,6 +351,9 @@ class BaseIdeSetup { return files; } + // Normalize ext to array + const extensions = Array.isArray(ext) ? ext : [ext]; + const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { @@ -215,13 +363,88 @@ class BaseIdeSetup { // Recursively scan subdirectories const subFiles = await this.scanDirectory(fullPath, ext); files.push(...subFiles); - } else if (entry.isFile() && entry.name.endsWith(ext)) { - files.push({ - name: path.basename(entry.name, ext), - path: fullPath, - relativePath: path.relative(dir, fullPath), - filename: entry.name, - }); + } else if (entry.isFile()) { + // Check if file matches any of the extensions + const matchedExt = extensions.find((e) => entry.name.endsWith(e)); + if (matchedExt) { + files.push({ + name: path.basename(entry.name, matchedExt), + path: fullPath, + relativePath: path.relative(dir, fullPath), + filename: entry.name, + }); + } + } + } + + return files; + } + + /** + * Scan a directory for files with specific extension(s) and check standalone attribute + * @param {string} dir - Directory to scan + * @param {string|Array} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml']) + * @returns {Array} List of file info objects with standalone property + */ + async scanDirectoryWithStandalone(dir, ext) { + const files = []; + + if (!(await fs.pathExists(dir))) { + return files; + } + + // Normalize ext to array + const extensions = Array.isArray(ext) ? ext : [ext]; + + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively scan subdirectories + const subFiles = await this.scanDirectoryWithStandalone(fullPath, ext); + files.push(...subFiles); + } else if (entry.isFile()) { + // Check if file matches any of the extensions + const matchedExt = extensions.find((e) => entry.name.endsWith(e)); + if (matchedExt) { + // Read file content to check for standalone attribute + let standalone = false; + try { + const content = await fs.readFile(fullPath, 'utf8'); + + // Check for standalone="true" in XML files + if (entry.name.endsWith('.xml')) { + // Look for standalone="true" in the opening tag (task or tool) + const standaloneMatch = content.match(/<(?:task|tool)[^>]+standalone="true"/); + standalone = !!standaloneMatch; + } else if (entry.name.endsWith('.md')) { + // Check for standalone: true in YAML frontmatter + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (frontmatterMatch) { + const yaml = require('js-yaml'); + try { + const frontmatter = yaml.load(frontmatterMatch[1]); + standalone = frontmatter.standalone === true; + } catch { + // Ignore YAML parse errors + } + } + } + } catch { + // If we can't read the file, assume not standalone + standalone = false; + } + + files.push({ + name: path.basename(entry.name, matchedExt), + path: fullPath, + relativePath: path.relative(dir, fullPath), + filename: entry.name, + standalone: standalone, + }); + } } } diff --git a/tools/cli/installers/lib/ide/auggie.js b/tools/cli/installers/lib/ide/auggie.js index 5029524e..dea71ca6 100644 --- a/tools/cli/installers/lib/ide/auggie.js +++ b/tools/cli/installers/lib/ide/auggie.js @@ -83,9 +83,11 @@ class AuggieSetup extends BaseIdeSetup { return { success: false, reason: 'no-locations' }; } - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only) const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const tasks = await this.getTasks(bmadDir, true); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); let totalInstalled = 0; @@ -93,11 +95,16 @@ class AuggieSetup extends BaseIdeSetup { for (const location of locations) { console.log(chalk.dim(`\n Installing to: ${location}`)); - const agentsDir = path.join(location, 'agents'); - const tasksDir = path.join(location, 'tasks'); + const bmadCommandsDir = path.join(location, 'bmad'); + const agentsDir = path.join(bmadCommandsDir, 'agents'); + const tasksDir = path.join(bmadCommandsDir, 'tasks'); + const toolsDir = path.join(bmadCommandsDir, 'tools'); + const workflowsDir = path.join(bmadCommandsDir, 'workflows'); await this.ensureDir(agentsDir); await this.ensureDir(tasksDir); + await this.ensureDir(toolsDir); + await this.ensureDir(workflowsDir); // Install agents for (const agent of agents) { @@ -119,7 +126,29 @@ class AuggieSetup extends BaseIdeSetup { totalInstalled++; } - console.log(chalk.green(` ✓ Installed ${agents.length} agents and ${tasks.length} tasks`)); + // Install tools + for (const tool of tools) { + const content = await this.readFile(tool.path); + const commandContent = this.createToolCommand(tool, content); + + const targetPath = path.join(toolsDir, `${tool.module}-${tool.name}.md`); + await this.writeFile(targetPath, commandContent); + totalInstalled++; + } + + // Install workflows + for (const workflow of workflows) { + const content = await this.readFile(workflow.path); + const commandContent = this.createWorkflowCommand(workflow, content); + + const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`); + await this.writeFile(targetPath, commandContent); + totalInstalled++; + } + + console.log( + chalk.green(` ✓ Installed ${agents.length} agents, ${tasks.length} tasks, ${tools.length} tools, ${workflows.length} workflows`), + ); } console.log(chalk.green(`\n✓ ${this.name} configured:`)); @@ -217,7 +246,7 @@ BMAD ${agent.module.toUpperCase()} module * Create task command content */ createTaskCommand(task, content) { - const nameMatch = content.match(/([^<]+)<\/name>/); + const nameMatch = content.match(/name="([^"]+)"/); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); return `# ${taskName} Task @@ -232,6 +261,44 @@ BMAD ${task.module.toUpperCase()} module `; } + /** + * Create tool command content + */ + createToolCommand(tool, content) { + const nameMatch = content.match(/name="([^"]+)"/); + const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); + + return `# ${toolName} Tool + +## Activation +Type \`@tool-${tool.name}\` to execute this tool. + +${content} + +## Module +BMAD ${tool.module.toUpperCase()} module +`; + } + + /** + * Create workflow command content + */ + createWorkflowCommand(workflow, content) { + return `# ${workflow.name} Workflow + +## Description +${workflow.description || 'No description provided'} + +## Activation +Type \`@workflow-${workflow.name}\` to execute this workflow. + +${content} + +## Module +BMAD ${workflow.module.toUpperCase()} module +`; + } + /** * Cleanup Auggie configuration */ @@ -244,22 +311,19 @@ BMAD ${task.module.toUpperCase()} module for (const location of locations) { const agentsDir = path.join(location, 'agents'); const tasksDir = path.join(location, 'tasks'); + const toolsDir = path.join(location, 'tools'); + const workflowsDir = path.join(location, 'workflows'); - if (await fs.pathExists(agentsDir)) { - // Remove only BMAD files (those with module prefix) - const files = await fs.readdir(agentsDir); - for (const file of files) { - if (file.includes('-') && file.endsWith('.md')) { - await fs.remove(path.join(agentsDir, file)); - } - } - } + const dirs = [agentsDir, tasksDir, toolsDir, workflowsDir]; - if (await fs.pathExists(tasksDir)) { - const files = await fs.readdir(tasksDir); - for (const file of files) { - if (file.includes('-') && file.endsWith('.md')) { - await fs.remove(path.join(tasksDir, file)); + for (const dir of dirs) { + if (await fs.pathExists(dir)) { + // Remove only BMAD files (those with module prefix) + const files = await fs.readdir(dir); + for (const file of files) { + if (file.includes('-') && file.endsWith('.md')) { + await fs.remove(path.join(dir, file)); + } } } } diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js index 98e03ee9..836272b1 100644 --- a/tools/cli/installers/lib/ide/claude-code.js +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -3,6 +3,7 @@ const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { WorkflowCommandGenerator } = require('./workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./task-tool-command-generator'); const { loadModuleInjectionConfig, shouldApplyInjection, @@ -146,11 +147,22 @@ class ClaudeCodeSetup extends BaseIdeSetup { const workflowGen = new WorkflowCommandGenerator(); const workflowResult = await workflowGen.generateWorkflowCommands(projectDir, bmadDir); + // Generate task and tool commands from manifests (if they exist) + const taskToolGen = new TaskToolCommandGenerator(); + const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir); + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); if (workflowResult.generated > 0) { console.log(chalk.dim(` - ${workflowResult.generated} workflow commands generated`)); } + if (taskToolResult.generated > 0) { + console.log( + chalk.dim( + ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, + ), + ); + } console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); return { diff --git a/tools/cli/installers/lib/ide/crush.js b/tools/cli/installers/lib/ide/crush.js index 5cbbf3b6..9e26fe1c 100644 --- a/tools/cli/installers/lib/ide/crush.js +++ b/tools/cli/installers/lib/ide/crush.js @@ -25,79 +25,69 @@ class CrushSetup extends BaseIdeSetup { // Create .crush/commands/bmad directory structure const crushDir = path.join(projectDir, this.configDir); const commandsDir = path.join(crushDir, this.commandsDir, 'bmad'); - const agentsDir = path.join(commandsDir, 'agents'); - const tasksDir = path.join(commandsDir, 'tasks'); - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); + await this.ensureDir(commandsDir); - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only) const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const tasks = await this.getTasks(bmadDir, true); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); - // Setup agents as commands - let agentCount = 0; - for (const agent of agents) { - const content = await this.readFile(agent.path); - const commandContent = this.createAgentCommand(agent, content, projectDir); - - const targetPath = path.join(agentsDir, `${agent.module}-${agent.name}.md`); - await this.writeFile(targetPath, commandContent); - agentCount++; - } - - // Setup tasks as commands - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); - - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; - } - - // Create module-specific subdirectories for better organization - await this.organizeByModule(commandsDir, agents, tasks, bmadDir); + // Organize by module + const agentCount = await this.organizeByModule(commandsDir, agents, tasks, tools, workflows, projectDir); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agent commands created`)); - console.log(chalk.dim(` - ${taskCount} task commands created`)); + console.log(chalk.dim(` - ${agentCount.agents} agent commands created`)); + console.log(chalk.dim(` - ${agentCount.tasks} task commands created`)); + console.log(chalk.dim(` - ${agentCount.tools} tool commands created`)); + console.log(chalk.dim(` - ${agentCount.workflows} workflow commands created`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); console.log(chalk.dim('\n Commands can be accessed via Crush command palette')); return { success: true, - agents: agentCount, - tasks: taskCount, + ...agentCount, }; } /** * Organize commands by module */ - async organizeByModule(commandsDir, agents, tasks, bmadDir) { + async organizeByModule(commandsDir, agents, tasks, tools, workflows, projectDir) { // Get unique modules const modules = new Set(); for (const agent of agents) modules.add(agent.module); for (const task of tasks) modules.add(task.module); + for (const tool of tools) modules.add(tool.module); + for (const workflow of workflows) modules.add(workflow.module); + + let agentCount = 0; + let taskCount = 0; + let toolCount = 0; + let workflowCount = 0; // Create module directories for (const module of modules) { const moduleDir = path.join(commandsDir, module); const moduleAgentsDir = path.join(moduleDir, 'agents'); const moduleTasksDir = path.join(moduleDir, 'tasks'); + const moduleToolsDir = path.join(moduleDir, 'tools'); + const moduleWorkflowsDir = path.join(moduleDir, 'workflows'); await this.ensureDir(moduleAgentsDir); await this.ensureDir(moduleTasksDir); + await this.ensureDir(moduleToolsDir); + await this.ensureDir(moduleWorkflowsDir); // Copy module-specific agents const moduleAgents = agents.filter((a) => a.module === module); for (const agent of moduleAgents) { const content = await this.readFile(agent.path); - const commandContent = this.createAgentCommand(agent, content, bmadDir); + const commandContent = this.createAgentCommand(agent, content, projectDir); const targetPath = path.join(moduleAgentsDir, `${agent.name}.md`); await this.writeFile(targetPath, commandContent); + agentCount++; } // Copy module-specific tasks @@ -107,8 +97,36 @@ class CrushSetup extends BaseIdeSetup { const commandContent = this.createTaskCommand(task, content); const targetPath = path.join(moduleTasksDir, `${task.name}.md`); await this.writeFile(targetPath, commandContent); + taskCount++; + } + + // Copy module-specific tools + const moduleTools = tools.filter((t) => t.module === module); + for (const tool of moduleTools) { + const content = await this.readFile(tool.path); + const commandContent = this.createToolCommand(tool, content); + const targetPath = path.join(moduleToolsDir, `${tool.name}.md`); + await this.writeFile(targetPath, commandContent); + toolCount++; + } + + // Copy module-specific workflows + const moduleWorkflows = workflows.filter((w) => w.module === module); + for (const workflow of moduleWorkflows) { + const content = await this.readFile(workflow.path); + const commandContent = this.createWorkflowCommand(workflow, content); + const targetPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`); + await this.writeFile(targetPath, commandContent); + workflowCount++; } } + + return { + agents: agentCount, + tasks: taskCount, + tools: toolCount, + workflows: workflowCount, + }; } /** @@ -154,7 +172,7 @@ Part of the BMAD ${agent.module.toUpperCase()} module. */ createTaskCommand(task, content) { // Extract task name - const nameMatch = content.match(/([^<]+)<\/name>/); + const nameMatch = content.match(/name="([^"]+)"/); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); let commandContent = `# /task-${task.name} Command @@ -177,6 +195,60 @@ Part of the BMAD ${task.module.toUpperCase()} module. return commandContent; } + /** + * Create tool command content + */ + createToolCommand(tool, content) { + // Extract tool name + const nameMatch = content.match(/name="([^"]+)"/); + const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); + + let commandContent = `# /tool-${tool.name} Command + +When this command is used, execute the following tool: + +## ${toolName} Tool + +${content} + +## Command Usage + +This command executes the ${toolName} tool from the BMAD ${tool.module.toUpperCase()} module. + +## Module + +Part of the BMAD ${tool.module.toUpperCase()} module. +`; + + return commandContent; + } + + /** + * Create workflow command content + */ + createWorkflowCommand(workflow, content) { + const workflowName = workflow.name ? this.formatTitle(workflow.name) : 'Workflow'; + + let commandContent = `# /${workflow.name} Command + +When this command is used, execute the following workflow: + +## ${workflowName} Workflow + +${content} + +## Command Usage + +This command executes the ${workflowName} workflow from the BMAD ${workflow.module.toUpperCase()} module. + +## Module + +Part of the BMAD ${workflow.module.toUpperCase()} module. +`; + + return commandContent; + } + /** * Format name as title */ diff --git a/tools/cli/installers/lib/ide/cursor.js b/tools/cli/installers/lib/ide/cursor.js index 8a6a0a64..13499bea 100644 --- a/tools/cli/installers/lib/ide/cursor.js +++ b/tools/cli/installers/lib/ide/cursor.js @@ -28,18 +28,22 @@ class CursorSetup extends BaseIdeSetup { await this.ensureDir(bmadRulesDir); - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only) const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const tasks = await this.getTasks(bmadDir, true); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); // Create directories for each module const modules = new Set(); - for (const item of [...agents, ...tasks]) modules.add(item.module); + for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module); for (const module of modules) { await this.ensureDir(path.join(bmadRulesDir, module)); await this.ensureDir(path.join(bmadRulesDir, module, 'agents')); await this.ensureDir(path.join(bmadRulesDir, module, 'tasks')); + await this.ensureDir(path.join(bmadRulesDir, module, 'tools')); + await this.ensureDir(path.join(bmadRulesDir, module, 'workflows')); } // Process and copy agents @@ -70,36 +74,68 @@ class CursorSetup extends BaseIdeSetup { taskCount++; } + // Process and copy tools + let toolCount = 0; + for (const tool of tools) { + const content = await this.readAndProcess(tool.path, { + module: tool.module, + name: tool.name, + }); + + const targetPath = path.join(bmadRulesDir, tool.module, 'tools', `${tool.name}.mdc`); + + await this.writeFile(targetPath, content); + toolCount++; + } + + // Process and copy workflows + let workflowCount = 0; + for (const workflow of workflows) { + const content = await this.readAndProcess(workflow.path, { + module: workflow.module, + name: workflow.name, + }); + + const targetPath = path.join(bmadRulesDir, workflow.module, 'workflows', `${workflow.name}.mdc`); + + await this.writeFile(targetPath, content); + workflowCount++; + } + // Create BMAD index file (but NOT .cursorrules - user manages that) - await this.createBMADIndex(bmadRulesDir, agents, tasks, modules); + await this.createBMADIndex(bmadRulesDir, agents, tasks, tools, workflows, modules); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${taskCount} tasks installed`)); + console.log(chalk.dim(` - ${toolCount} tools installed`)); + console.log(chalk.dim(` - ${workflowCount} workflows installed`)); console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, bmadRulesDir)}`)); return { success: true, agents: agentCount, tasks: taskCount, + tools: toolCount, + workflows: workflowCount, }; } /** * Create BMAD index file for easy navigation */ - async createBMADIndex(bmadRulesDir, agents, tasks, modules) { + async createBMADIndex(bmadRulesDir, agents, tasks, tools, workflows, modules) { const indexPath = path.join(bmadRulesDir, 'index.mdc'); let content = `--- description: BMAD Method - Master Index -globs: +globs: alwaysApply: true --- # BMAD Method - Cursor Rules Index -This is the master index for all BMAD agents and tasks available in your project. +This is the master index for all BMAD agents, tasks, tools, and workflows available in your project. ## Installation Complete! @@ -111,6 +147,8 @@ BMAD rules have been installed to: \`.cursor/rules/bmad/\` - Reference specific agents: @bmad/{module}/agents/{agent-name} - Reference specific tasks: @bmad/{module}/tasks/{task-name} +- Reference specific tools: @bmad/{module}/tools/{tool-name} +- Reference specific workflows: @bmad/{module}/workflows/{workflow-name} - Reference entire modules: @bmad/{module} - Reference this index: @bmad/index @@ -140,6 +178,26 @@ BMAD rules have been installed to: \`.cursor/rules/bmad/\` } content += '\n'; } + + // List tools for this module + const moduleTools = tools.filter((t) => t.module === module); + if (moduleTools.length > 0) { + content += `**Tools:**\n`; + for (const tool of moduleTools) { + content += `- @bmad/${module}/tools/${tool.name} - ${tool.name}\n`; + } + content += '\n'; + } + + // List workflows for this module + const moduleWorkflows = workflows.filter((w) => w.module === module); + if (moduleWorkflows.length > 0) { + content += `**Workflows:**\n`; + for (const workflow of moduleWorkflows) { + content += `- @bmad/${module}/workflows/${workflow.name} - ${workflow.name}\n`; + } + content += '\n'; + } } content += ` @@ -148,13 +206,15 @@ BMAD rules have been installed to: \`.cursor/rules/bmad/\` - All BMAD rules are Manual type - reference them explicitly when needed - Agents provide persona-based assistance with specific expertise - Tasks are reusable workflows for common operations +- Tools provide specialized functionality +- Workflows orchestrate multi-step processes - Each agent includes an activation block for proper initialization ## Configuration BMAD rules are configured as Manual rules (alwaysApply: false) to give you control over when they're included in your context. Reference them explicitly when you need -specific agent expertise or task workflows. +specific agent expertise, task workflows, tools, or guided workflows. `; await this.writeFile(indexPath, content); @@ -182,6 +242,8 @@ specific agent expertise or task workflows. // Determine the type and description based on content const isAgent = content.includes('([^<]+)<\/name>/); + const nameMatch = content.match(/name="([^"]+)"/); const taskName = nameMatch ? nameMatch[1] : metadata.name; description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`; - - // Tasks might be auto-attached to certain file types + globs = ''; + } else if (isTool) { + // Extract tool name if available + const nameMatch = content.match(/name="([^"]+)"/); + const toolName = nameMatch ? nameMatch[1] : metadata.name; + description = `BMAD ${metadata.module.toUpperCase()} Tool: ${toolName}`; + globs = ''; + } else if (isWorkflow) { + // Workflow + description = `BMAD ${metadata.module.toUpperCase()} Workflow: ${metadata.name}`; globs = ''; } else { description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`; diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index b438aaca..d58ca6b0 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -22,7 +22,13 @@ class IdeManager { // Get all JS files in the IDE directory const files = fs.readdirSync(ideDir).filter((file) => { // Skip base class, manager, utility files (starting with _), and helper modules - return file.endsWith('.js') && !file.startsWith('_') && file !== 'manager.js' && file !== 'workflow-command-generator.js'; + return ( + file.endsWith('.js') && + !file.startsWith('_') && + file !== 'manager.js' && + file !== 'workflow-command-generator.js' && + file !== 'task-tool-command-generator.js' + ); }); // Sort alphabetically for consistent ordering @@ -41,7 +47,12 @@ class IdeManager { if (HandlerClass) { const instance = new HandlerClass(); // Use the name property from the instance (set in constructor) - this.handlers.set(instance.name, instance); + // Only add if the instance has a valid name + if (instance.name && typeof instance.name === 'string') { + this.handlers.set(instance.name, instance); + } else { + console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`)); + } } } catch (error) { console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`)); @@ -60,9 +71,17 @@ class IdeManager { const ides = []; for (const [key, handler] of this.handlers) { + // Skip handlers without valid names + const name = handler.displayName || handler.name || key; + + // Filter out invalid entries (undefined name, empty key, etc.) + if (!key || !name || typeof key !== 'string' || typeof name !== 'string') { + continue; + } + ides.push({ value: key, - name: handler.displayName || handler.name || key, + name: name, preferred: handler.preferred || false, }); } @@ -71,10 +90,7 @@ class IdeManager { ides.sort((a, b) => { if (a.preferred && !b.preferred) return -1; if (!a.preferred && b.preferred) return 1; - // Ensure both names exist before comparing - const nameA = a.name || ''; - const nameB = b.name || ''; - return nameA.localeCompare(nameB); + return a.name.localeCompare(b.name); }); return ides; diff --git a/tools/cli/installers/lib/ide/opencode.js b/tools/cli/installers/lib/ide/opencode.js index 1e4d49ac..f9b2de7e 100644 --- a/tools/cli/installers/lib/ide/opencode.js +++ b/tools/cli/installers/lib/ide/opencode.js @@ -5,6 +5,7 @@ const chalk = require('chalk'); const yaml = require('js-yaml'); const { BaseIdeSetup } = require('./_base-ide'); const { WorkflowCommandGenerator } = require('./workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./task-tool-command-generator'); const { getAgentsFromBmad } = require('./shared/bmad-artifacts'); @@ -13,7 +14,7 @@ const { getAgentsFromBmad } = require('./shared/bmad-artifacts'); */ class OpenCodeSetup extends BaseIdeSetup { constructor() { - super('opencode', 'OpenCode', false); + super('opencode', 'OpenCode', true); // Mark as preferred/recommended this.configDir = '.opencode'; this.commandsDir = 'command'; this.agentsDir = 'agent'; @@ -64,11 +65,22 @@ class OpenCodeSetup extends BaseIdeSetup { workflowCommandCount++; } + // Install task and tool commands + const taskToolGen = new TaskToolCommandGenerator(); + const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir, commandsBaseDir); + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/bmad/`)); if (workflowCommandCount > 0) { console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated to .opencode/command/bmad/`)); } + if (taskToolResult.generated > 0) { + console.log( + chalk.dim( + ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, + ), + ); + } return { success: true, diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js index f8a3b0d0..7a90b58e 100644 --- a/tools/cli/installers/lib/ide/qwen.js +++ b/tools/cli/installers/lib/ide/qwen.js @@ -37,18 +37,22 @@ class QwenSetup extends BaseIdeSetup { // Clean up old configuration if exists await this.cleanupOldConfig(qwenDir); - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only for tools/workflows) const agents = await getAgentsFromBmad(bmadDir, options.selectedModules || []); const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); // Create directories for each module (including standalone) const modules = new Set(); - for (const item of [...agents, ...tasks]) modules.add(item.module); + for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module); for (const module of modules) { await this.ensureDir(path.join(bmadCommandsDir, module)); await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); + await this.ensureDir(path.join(bmadCommandsDir, module, 'tools')); + await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows')); } // Create TOML files for each agent @@ -75,7 +79,7 @@ class QwenSetup extends BaseIdeSetup { name: task.name, }); - const targetPath = path.join(bmadCommandsDir, task.module, 'agents', `${agent.name}.toml`); + const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`); await this.writeFile(targetPath, content); @@ -83,15 +87,51 @@ class QwenSetup extends BaseIdeSetup { console.log(chalk.green(` ✓ Added task: /bmad:${task.module}:tasks:${task.name}`)); } + // Create TOML files for each tool + let toolCount = 0; + for (const tool of tools) { + const content = await this.readAndProcess(tool.path, { + module: tool.module, + name: tool.name, + }); + + const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`); + + await this.writeFile(targetPath, content); + + toolCount++; + console.log(chalk.green(` ✓ Added tool: /bmad:${tool.module}:tools:${tool.name}`)); + } + + // Create TOML files for each workflow + let workflowCount = 0; + for (const workflow of workflows) { + const content = await this.readAndProcess(workflow.path, { + module: workflow.module, + name: workflow.name, + }); + + const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`); + + await this.writeFile(targetPath, content); + + workflowCount++; + console.log(chalk.green(` ✓ Added workflow: /bmad:${workflow.module}:workflows:${workflow.name}`)); + } + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${taskCount} tasks configured`)); + console.log(chalk.dim(` - ${toolCount} tools configured`)); + console.log(chalk.dim(` - ${workflowCount} workflows configured`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); return { success: true, agents: agentCount, tasks: taskCount, + tools: toolCount, + workflows: workflowCount, }; } @@ -177,6 +217,8 @@ class QwenSetup extends BaseIdeSetup { // Determine the type and description based on content const isAgent = content.includes('([^<]+)<\/name>/); + const nameMatch = content.match(/name="([^"]+)"/); const taskName = nameMatch ? nameMatch[1] : metadata.name; description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`; + } else if (isTool) { + // Extract tool name if available + const nameMatch = content.match(/name="([^"]+)"/); + const toolName = nameMatch ? nameMatch[1] : metadata.name; + description = `BMAD ${metadata.module.toUpperCase()} Tool: ${toolName}`; + } else if (isWorkflow) { + // Workflow + description = `BMAD ${metadata.module.toUpperCase()} Workflow: ${metadata.name}`; } else { description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`; } diff --git a/tools/cli/installers/lib/ide/task-tool-command-generator.js b/tools/cli/installers/lib/ide/task-tool-command-generator.js new file mode 100644 index 00000000..448ceea5 --- /dev/null +++ b/tools/cli/installers/lib/ide/task-tool-command-generator.js @@ -0,0 +1,119 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const csv = require('csv-parse/sync'); +const chalk = require('chalk'); + +/** + * Generates Claude Code command files for standalone tasks and tools + */ +class TaskToolCommandGenerator { + /** + * 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 + let itemPath = item.path; + if (itemPath.startsWith('bmad/')) { + itemPath = `{project-root}/${itemPath}`; + } + + return `--- +description: '${description.replaceAll("'", "''")}' +--- + +# ${item.displayName || item.name} + +LOAD and execute the ${type} at: ${itemPath} + +Follow all instructions in the ${type} file exactly as written. +`; + } + + /** + * Load task manifest CSV + */ + async loadTaskManifest(bmadDir) { + const manifestPath = path.join(bmadDir, '_cfg', '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, '_cfg', '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, + }); + } +} + +module.exports = { TaskToolCommandGenerator }; diff --git a/tools/cli/installers/lib/ide/trae.js b/tools/cli/installers/lib/ide/trae.js index 0268ab43..d3acee83 100644 --- a/tools/cli/installers/lib/ide/trae.js +++ b/tools/cli/installers/lib/ide/trae.js @@ -27,39 +27,74 @@ class TraeSetup extends BaseIdeSetup { await this.ensureDir(rulesDir); - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only) const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const tasks = await this.getTasks(bmadDir, true); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); // Process agents as rules - let ruleCount = 0; + let agentCount = 0; for (const agent of agents) { const content = await this.readFile(agent.path); const processedContent = this.createAgentRule(agent, content, bmadDir, projectDir); const targetPath = path.join(rulesDir, `${agent.module}-${agent.name}.md`); await this.writeFile(targetPath, processedContent); - ruleCount++; + agentCount++; } // Process tasks as rules + let taskCount = 0; for (const task of tasks) { const content = await this.readFile(task.path); const processedContent = this.createTaskRule(task, content); const targetPath = path.join(rulesDir, `task-${task.module}-${task.name}.md`); await this.writeFile(targetPath, processedContent); - ruleCount++; + taskCount++; } + // Process tools as rules + let toolCount = 0; + for (const tool of tools) { + const content = await this.readFile(tool.path); + const processedContent = this.createToolRule(tool, content); + + const targetPath = path.join(rulesDir, `tool-${tool.module}-${tool.name}.md`); + await this.writeFile(targetPath, processedContent); + toolCount++; + } + + // Process workflows as rules + let workflowCount = 0; + for (const workflow of workflows) { + const content = await this.readFile(workflow.path); + const processedContent = this.createWorkflowRule(workflow, content); + + const targetPath = path.join(rulesDir, `workflow-${workflow.module}-${workflow.name}.md`); + await this.writeFile(targetPath, processedContent); + workflowCount++; + } + + const totalRules = agentCount + taskCount + toolCount + workflowCount; + console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${ruleCount} rules created`)); + console.log(chalk.dim(` - ${agentCount} agent rules created`)); + console.log(chalk.dim(` - ${taskCount} task rules created`)); + console.log(chalk.dim(` - ${toolCount} tool rules created`)); + console.log(chalk.dim(` - ${workflowCount} workflow rules created`)); + console.log(chalk.dim(` - Total: ${totalRules} rules`)); console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, rulesDir)}`)); console.log(chalk.dim(` - Agents can be activated with @{agent-name}`)); return { success: true, - rules: ruleCount, + rules: totalRules, + agents: agentCount, + tasks: taskCount, + tools: toolCount, + workflows: workflowCount, }; } @@ -114,7 +149,7 @@ Part of the BMAD ${agent.module.toUpperCase()} module. */ createTaskRule(task, content) { // Extract task name from content - const nameMatch = content.match(/([^<]+)<\/name>/); + const nameMatch = content.match(/name="([^"]+)"/); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); let ruleContent = `# ${taskName} Task Rule @@ -139,6 +174,64 @@ Part of the BMAD ${task.module.toUpperCase()} module. return ruleContent; } + /** + * Create rule content for a tool + */ + createToolRule(tool, content) { + // Extract tool name from content + const nameMatch = content.match(/name="([^"]+)"/); + const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); + + let ruleContent = `# ${toolName} Tool Rule + +This rule defines the ${toolName} tool. + +## Tool Definition + +When this tool is triggered, execute the following: + +${content} + +## Usage + +Reference this tool with \`@tool-${tool.name}\` to execute it. + +## Module + +Part of the BMAD ${tool.module.toUpperCase()} module. +`; + + return ruleContent; + } + + /** + * Create rule content for a workflow + */ + createWorkflowRule(workflow, content) { + let ruleContent = `# ${workflow.name} Workflow Rule + +This rule defines the ${workflow.name} workflow. + +## Workflow Description + +${workflow.description || 'No description provided'} + +## Workflow Definition + +${content} + +## Usage + +Reference this workflow with \`@workflow-${workflow.name}\` to execute the guided workflow. + +## Module + +Part of the BMAD ${workflow.module.toUpperCase()} module. +`; + + return ruleContent; + } + /** * Format agent/task name as title */ diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js index 651eb4da..0b6de86e 100644 --- a/tools/cli/installers/lib/ide/windsurf.js +++ b/tools/cli/installers/lib/ide/windsurf.js @@ -27,18 +27,22 @@ class WindsurfSetup extends BaseIdeSetup { await this.ensureDir(workflowsDir); - // Get agents and tasks + // Get agents, tasks, tools, and workflows (standalone only) const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const tasks = await this.getTasks(bmadDir, true); + const tools = await this.getTools(bmadDir, true); + const workflows = await this.getWorkflows(bmadDir, true); // Create directories for each module const modules = new Set(); - for (const item of [...agents, ...tasks]) modules.add(item.module); + for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module); for (const module of modules) { await this.ensureDir(path.join(workflowsDir, module)); await this.ensureDir(path.join(workflowsDir, module, 'agents')); await this.ensureDir(path.join(workflowsDir, module, 'tasks')); + await this.ensureDir(path.join(workflowsDir, module, 'tools')); + await this.ensureDir(path.join(workflowsDir, module, 'workflows')); } // Process agents as workflows with organized structure @@ -65,9 +69,35 @@ class WindsurfSetup extends BaseIdeSetup { taskCount++; } + // Process tools as workflows with organized structure + let toolCount = 0; + for (const tool of tools) { + const content = await this.readFile(tool.path); + const processedContent = this.createToolWorkflowContent(tool, content); + + // Organized path: module/tools/tool-name.md + const targetPath = path.join(workflowsDir, tool.module, 'tools', `${tool.name}.md`); + await this.writeFile(targetPath, processedContent); + toolCount++; + } + + // Process workflows with organized structure + let workflowCount = 0; + for (const workflow of workflows) { + const content = await this.readFile(workflow.path); + const processedContent = this.createWorkflowWorkflowContent(workflow, content); + + // Organized path: module/workflows/workflow-name.md + const targetPath = path.join(workflowsDir, workflow.module, 'workflows', `${workflow.name}.md`); + await this.writeFile(targetPath, processedContent); + workflowCount++; + } + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${taskCount} tasks installed`)); + console.log(chalk.dim(` - ${toolCount} tools installed`)); + console.log(chalk.dim(` - ${workflowCount} workflows installed`)); console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`)); console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); @@ -75,7 +105,8 @@ class WindsurfSetup extends BaseIdeSetup { if (options.showHints !== false) { console.log(chalk.dim('\n Windsurf workflow settings:')); console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)')); - console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks)')); + console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks/tools)')); + console.log(chalk.dim(' - auto_execution_mode: 1 (recommended for workflows)')); console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu')); } @@ -83,6 +114,8 @@ class WindsurfSetup extends BaseIdeSetup { success: true, agents: agentCount, tasks: taskCount, + tools: toolCount, + workflows: workflowCount, }; } @@ -111,6 +144,36 @@ description: task-${task.name} auto_execution_mode: 2 --- +${content}`; + + return workflowContent; + } + + /** + * Create workflow content for a tool + */ + createToolWorkflowContent(tool, content) { + // Create simple Windsurf frontmatter matching original format + let workflowContent = `--- +description: tool-${tool.name} +auto_execution_mode: 2 +--- + +${content}`; + + return workflowContent; + } + + /** + * Create workflow content for a workflow + */ + createWorkflowWorkflowContent(workflow, content) { + // Create simple Windsurf frontmatter matching original format + let workflowContent = `--- +description: ${workflow.name} +auto_execution_mode: 1 +--- + ${content}`; return workflowContent; diff --git a/tools/cli/installers/lib/ide/workflow-command-generator.js b/tools/cli/installers/lib/ide/workflow-command-generator.js index aa13624a..fd54a95e 100644 --- a/tools/cli/installers/lib/ide/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/workflow-command-generator.js @@ -24,13 +24,16 @@ class WorkflowCommandGenerator { return { generated: 0 }; } + // Filter to only standalone workflows + const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true); + // Base commands directory const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad'); let generatedCount = 0; - // Generate a command file for each workflow, organized by module - for (const workflow of workflows) { + // Generate a command file for each standalone workflow, organized by module + for (const workflow of standaloneWorkflows) { const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows'); await fs.ensureDir(moduleWorkflowsDir); @@ -42,7 +45,7 @@ class WorkflowCommandGenerator { } // Also create a workflow launcher README in each module - const groupedWorkflows = this.groupWorkflowsByModule(workflows); + const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows); await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows); return { generated: generatedCount }; @@ -55,9 +58,12 @@ class WorkflowCommandGenerator { return { artifacts: [], counts: { commands: 0, launchers: 0 } }; } + // Filter to only standalone workflows + const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true); + const artifacts = []; - for (const workflow of workflows) { + for (const workflow of standaloneWorkflows) { const commandContent = await this.generateCommandContent(workflow, bmadDir); artifacts.push({ type: 'workflow-command', @@ -68,7 +74,7 @@ class WorkflowCommandGenerator { }); } - const groupedWorkflows = this.groupWorkflowsByModule(workflows); + const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows); for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) { artifacts.push({ type: 'workflow-launcher', @@ -82,7 +88,7 @@ class WorkflowCommandGenerator { return { artifacts, counts: { - commands: workflows.length, + commands: standaloneWorkflows.length, launchers: Object.keys(groupedWorkflows).length, }, }; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index c48f8ded..d354fcef 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -109,6 +109,11 @@ class UI { if (configuredIdes.length > 0) { ideChoices.push(new inquirer.Separator('── Previously Configured ──')); for (const ideValue of configuredIdes) { + // Skip empty or invalid IDE values + if (!ideValue || typeof ideValue !== 'string') { + continue; + } + // Find the IDE in either preferred or other lists const preferredIde = preferredIdes.find((ide) => ide.value === ideValue); const otherIde = otherIdes.find((ide) => ide.value === ideValue); @@ -121,6 +126,9 @@ class UI { checked: true, // Previously configured IDEs are checked by default }); processedIdes.add(ide.value); + } else { + // Warn about unrecognized IDE (but don't fail) + console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`)); } } }