installer fixes

This commit is contained in:
Brian Madison 2025-10-26 19:38:38 -05:00
parent 1cb88728e8
commit 63ef5b7bc6
20 changed files with 1152 additions and 179 deletions

View File

@ -1,4 +1,5 @@
<task id="bmad/core/tasks/index-docs" name="Index Docs" webskip="true"> <task id="bmad/core/tasks/index-docs" name="Index Docs"
description="Generates or updates an index.md of all documents in the specified directory" webskip="true" standalone="true">
<llm critical="true"> <llm critical="true">
<i>MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER</i> <i>MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER</i>
<i>DO NOT skip steps or change the sequence</i> <i>DO NOT skip steps or change the sequence</i>
@ -17,7 +18,8 @@
</step> </step>
<step n="3" title="Generate Descriptions"> <step n="3" title="Generate Descriptions">
<i>Read each file to understand its actual purpose and create brief (3-10 word) descriptions based on the content, not just the filename</i> <i>Read each file to understand its actual purpose and create brief (3-10 word) descriptions based on the content, not just the
filename</i>
</step> </step>
<step n="4" title="Create/Update Index"> <step n="4" title="Create/Update Index">

View File

@ -1,4 +1,6 @@
<tool id="bmad/core/tasks/shard-doc" name="Shard Document"> <tool id="bmad/core/tasks/shard-doc" name="Shard Document"
description="Splits large markdown documents into smaller, organized files based on level 2 (default) sections" webskip="true"
standalone="true">
<objective>Split large markdown documents into smaller, organized files based on level 2 sections using @kayvan/markdown-tree-parser tool</objective> <objective>Split large markdown documents into smaller, organized files based on level 2 sections using @kayvan/markdown-tree-parser tool</objective>
<llm critical="true"> <llm critical="true">

View File

@ -18,4 +18,6 @@ exit_triggers:
- "end party mode" - "end party mode"
- "stop party mode" - "stop party mode"
standalone: true
web_bundle: false web_bundle: false

View File

@ -599,6 +599,7 @@ class DependencyResolver {
organized[module] = { organized[module] = {
agents: [], agents: [],
tasks: [], tasks: [],
tools: [],
templates: [], templates: [],
data: [], data: [],
other: [], other: [],
@ -626,6 +627,8 @@ class DependencyResolver {
organized[module].agents.push(file); organized[module].agents.push(file);
} else if (relative.startsWith('tasks/') || file.includes('/tasks/')) { } else if (relative.startsWith('tasks/') || file.includes('/tasks/')) {
organized[module].tasks.push(file); 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/')) { } else if (relative.includes('template') || file.includes('/templates/')) {
organized[module].templates.push(file); organized[module].templates.push(file);
} else if (relative.includes('data/')) { } else if (relative.includes('data/')) {
@ -646,7 +649,8 @@ class DependencyResolver {
for (const [module, files] of Object.entries(organized)) { for (const [module, files] of Object.entries(organized)) {
const isSelected = selectedModules.includes(module) || module === 'core'; 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) { if (totalFiles > 0) {
console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`)); console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`));

View File

@ -117,7 +117,8 @@ class Detector {
// Check for IDE configurations from manifest // Check for IDE configurations from manifest
if (result.manifest && result.manifest.ides) { 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 // Mark as installed if we found core or modules

View File

@ -439,7 +439,13 @@ class Installer {
// Install partial modules (only dependencies) // Install partial modules (only dependencies)
for (const [module, files] of Object.entries(resolution.byModule)) { for (const [module, files] of Object.entries(resolution.byModule)) {
if (!config.modules.includes(module) && module !== 'core') { 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) { if (totalFiles > 0) {
spinner.start(`Installing ${module} dependencies...`); spinner.start(`Installing ${module} dependencies...`);
await this.installPartialModule(module, bmadDir, files); await this.installPartialModule(module, bmadDir, files);
@ -480,67 +486,77 @@ class Installer {
}); });
spinner.succeed( 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 // Configure IDEs and copy documentation
if (!config.skipIde && config.ides && config.ides.length > 0) { if (!config.skipIde && config.ides && config.ides.length > 0) {
// Check if any IDE might need prompting (no pre-collected config) // Filter out any undefined/null values from the IDE list
const needsPrompting = config.ides.some((ide) => !ideConfigurations[ide]); const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
if (!needsPrompting) { if (validIdes.length === 0) {
spinner.start('Configuring IDEs...'); 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 if (!needsPrompting) {
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) {
spinner.start('Configuring IDEs...'); 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 // Copy IDE-specific documentation (only for valid IDEs)
console.log = originalLog; const validIdesForDocs = (config.ides || []).filter((ide) => ide && typeof ide === 'string');
if (validIdesForDocs.length > 0) {
if (spinner.isSpinning) { spinner.start('Copying IDE documentation...');
spinner.succeed(`Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`); await this.copyIdeDocumentation(validIdesForDocs, bmadDir);
} else { spinner.succeed('IDE documentation copied');
console.log(chalk.green(`✓ Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`));
} }
// 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 // 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) { if (files.templates && files.templates.length > 0) {
const templatesDir = path.join(targetBase, 'templates'); const templatesDir = path.join(targetBase, 'templates');
await fs.ensureDir(templatesDir); await fs.ensureDir(templatesDir);

View File

@ -12,6 +12,7 @@ class ManifestGenerator {
this.workflows = []; this.workflows = [];
this.agents = []; this.agents = [];
this.tasks = []; this.tasks = [];
this.tools = [];
this.modules = []; this.modules = [];
this.files = []; this.files = [];
this.selectedIdes = []; this.selectedIdes = [];
@ -45,7 +46,8 @@ class ManifestGenerator {
throw new TypeError('ManifestGenerator expected `options.ides` to be an array.'); 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 // Collect workflow data
await this.collectWorkflows(selectedModules); await this.collectWorkflows(selectedModules);
@ -56,12 +58,16 @@ class ManifestGenerator {
// Collect task data // Collect task data
await this.collectTasks(selectedModules); await this.collectTasks(selectedModules);
// Collect tool data
await this.collectTools(selectedModules);
// Write manifest files and collect their paths // Write manifest files and collect their paths
const manifestFiles = [ const manifestFiles = [
await this.writeMainManifest(cfgDir), await this.writeMainManifest(cfgDir),
await this.writeWorkflowManifest(cfgDir), await this.writeWorkflowManifest(cfgDir),
await this.writeAgentManifest(cfgDir), await this.writeAgentManifest(cfgDir),
await this.writeTaskManifest(cfgDir), await this.writeTaskManifest(cfgDir),
await this.writeToolManifest(cfgDir),
await this.writeFilesManifest(cfgDir), await this.writeFilesManifest(cfgDir),
]; ];
@ -69,6 +75,7 @@ class ManifestGenerator {
workflows: this.workflows.length, workflows: this.workflows.length,
agents: this.agents.length, agents: this.agents.length,
tasks: this.tasks.length, tasks: this.tasks.length,
tools: this.tools.length,
files: this.files.length, files: this.files.length,
manifestFiles: manifestFiles, manifestFiles: manifestFiles,
}; };
@ -133,11 +140,15 @@ class ManifestGenerator {
? `bmad/core/workflows/${relativePath}/workflow.yaml` ? `bmad/core/workflows/${relativePath}/workflow.yaml`
: `bmad/${moduleName}/workflows/${relativePath}/workflow.yaml`; : `bmad/${moduleName}/workflows/${relativePath}/workflow.yaml`;
// Check for standalone property (default: false)
const standalone = workflow.standalone === true;
workflows.push({ workflows.push({
name: workflow.name, name: workflow.name,
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone,
}); });
// Add to files list // Add to files list
@ -306,24 +317,34 @@ class ManifestGenerator {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
for (const file of files) { for (const file of files) {
if (file.endsWith('.md')) { // Check for both .xml and .md files
if (file.endsWith('.xml') || file.endsWith('.md')) {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
// Extract task metadata from content if possible // Extract task metadata from content if possible
const nameMatch = content.match(/name="([^"]+)"/); const nameMatch = content.match(/name="([^"]+)"/);
// Try description attribute first, fall back to <objective> element
const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/); const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
// Check for standalone attribute in <task> tag (default: false)
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
const standalone = !!standaloneMatch;
// Build relative path for installation // Build relative path for installation
const installPath = moduleName === 'core' ? `bmad/core/tasks/${file}` : `bmad/${moduleName}/tasks/${file}`; 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({ tasks.push({
name: taskName, name: taskName,
displayName: nameMatch ? nameMatch[1] : taskName, displayName: nameMatch ? nameMatch[1] : taskName,
description: objMatch ? objMatch[1].trim().replaceAll('"', '""') : '', description: description.replaceAll('"', '""'),
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone,
}); });
// Add to files list // Add to files list
@ -339,6 +360,82 @@ class ManifestGenerator {
return tasks; 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 <objective> element
const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
// Check for standalone attribute in <tool> tag (default: false)
const standaloneMatch = content.match(/<tool[^>]+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 * Write main manifest as YAML with installation info only
* @returns {string} Path to the manifest file * @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) // Get preserved rows from existing CSV (module is column 2, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 2); const preservedRows = await this.getPreservedCsvRows(csvPath, 2);
// Create CSV header // Create CSV header with standalone column
let csv = 'name,description,module,path\n'; let csv = 'name,description,module,path,standalone\n';
// Add new rows for updated modules // Add new rows for updated modules
for (const workflow of this.workflows) { 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 // 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) // Get preserved rows from existing CSV (module is column 3, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 3); const preservedRows = await this.getPreservedCsvRows(csvPath, 3);
// Create CSV header // Create CSV header with standalone column
let csv = 'name,displayName,description,module,path\n'; let csv = 'name,displayName,description,module,path,standalone\n';
// Add new rows for updated modules // Add new rows for updated modules
for (const task of this.tasks) { 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 // Add preserved rows for modules we didn't update

View File

@ -156,15 +156,16 @@ class BaseIdeSetup {
/** /**
* Get list of tasks from BMAD installation * Get list of tasks from BMAD installation
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory
* @param {boolean} standaloneOnly - If true, only return standalone tasks
* @returns {Array} List of task files * @returns {Array} List of task files
*/ */
async getTasks(bmadDir) { async getTasks(bmadDir, standaloneOnly = false) {
const tasks = []; const tasks = [];
// Get core tasks // Get core tasks (scan for both .md and .xml)
const coreTasksPath = path.join(bmadDir, 'core', 'tasks'); const coreTasksPath = path.join(bmadDir, 'core', 'tasks');
if (await fs.pathExists(coreTasksPath)) { if (await fs.pathExists(coreTasksPath)) {
const coreTasks = await this.scanDirectory(coreTasksPath, '.md'); const coreTasks = await this.scanDirectoryWithStandalone(coreTasksPath, ['.md', '.xml']);
tasks.push( tasks.push(
...coreTasks.map((t) => ({ ...coreTasks.map((t) => ({
...t, ...t,
@ -179,7 +180,7 @@ class BaseIdeSetup {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') { if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') {
const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks'); const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks');
if (await fs.pathExists(moduleTasksPath)) { if (await fs.pathExists(moduleTasksPath)) {
const moduleTasks = await this.scanDirectory(moduleTasksPath, '.md'); const moduleTasks = await this.scanDirectoryWithStandalone(moduleTasksPath, ['.md', '.xml']);
tasks.push( tasks.push(
...moduleTasks.map((t) => ({ ...moduleTasks.map((t) => ({
...t, ...t,
@ -190,13 +191,157 @@ class BaseIdeSetup {
} }
} }
// Filter by standalone if requested
if (standaloneOnly) {
return tasks.filter((t) => t.standalone === true);
}
return tasks; 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} dir - Directory to scan
* @param {string} ext - File extension to match * @param {string|Array<string>} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
* @returns {Array} List of file info objects * @returns {Array} List of file info objects
*/ */
async scanDirectory(dir, ext) { async scanDirectory(dir, ext) {
@ -206,6 +351,9 @@ class BaseIdeSetup {
return files; return files;
} }
// Normalize ext to array
const extensions = Array.isArray(ext) ? ext : [ext];
const entries = await fs.readdir(dir, { withFileTypes: true }); const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
@ -215,13 +363,88 @@ class BaseIdeSetup {
// Recursively scan subdirectories // Recursively scan subdirectories
const subFiles = await this.scanDirectory(fullPath, ext); const subFiles = await this.scanDirectory(fullPath, ext);
files.push(...subFiles); files.push(...subFiles);
} else if (entry.isFile() && entry.name.endsWith(ext)) { } else if (entry.isFile()) {
files.push({ // Check if file matches any of the extensions
name: path.basename(entry.name, ext), const matchedExt = extensions.find((e) => entry.name.endsWith(e));
path: fullPath, if (matchedExt) {
relativePath: path.relative(dir, fullPath), files.push({
filename: entry.name, 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<string>} 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,
});
}
} }
} }

View File

@ -83,9 +83,11 @@ class AuggieSetup extends BaseIdeSetup {
return { success: false, reason: 'no-locations' }; 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 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; let totalInstalled = 0;
@ -93,11 +95,16 @@ class AuggieSetup extends BaseIdeSetup {
for (const location of locations) { for (const location of locations) {
console.log(chalk.dim(`\n Installing to: ${location}`)); console.log(chalk.dim(`\n Installing to: ${location}`));
const agentsDir = path.join(location, 'agents'); const bmadCommandsDir = path.join(location, 'bmad');
const tasksDir = path.join(location, 'tasks'); 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(agentsDir);
await this.ensureDir(tasksDir); await this.ensureDir(tasksDir);
await this.ensureDir(toolsDir);
await this.ensureDir(workflowsDir);
// Install agents // Install agents
for (const agent of agents) { for (const agent of agents) {
@ -119,7 +126,29 @@ class AuggieSetup extends BaseIdeSetup {
totalInstalled++; 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:`)); console.log(chalk.green(`\n${this.name} configured:`));
@ -217,7 +246,7 @@ BMAD ${agent.module.toUpperCase()} module
* Create task command content * Create task command content
*/ */
createTaskCommand(task, content) { createTaskCommand(task, content) {
const nameMatch = content.match(/<name>([^<]+)<\/name>/); const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
return `# ${taskName} Task 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 * Cleanup Auggie configuration
*/ */
@ -244,22 +311,19 @@ BMAD ${task.module.toUpperCase()} module
for (const location of locations) { for (const location of locations) {
const agentsDir = path.join(location, 'agents'); const agentsDir = path.join(location, 'agents');
const tasksDir = path.join(location, 'tasks'); const tasksDir = path.join(location, 'tasks');
const toolsDir = path.join(location, 'tools');
const workflowsDir = path.join(location, 'workflows');
if (await fs.pathExists(agentsDir)) { const dirs = [agentsDir, tasksDir, toolsDir, workflowsDir];
// 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));
}
}
}
if (await fs.pathExists(tasksDir)) { for (const dir of dirs) {
const files = await fs.readdir(tasksDir); if (await fs.pathExists(dir)) {
for (const file of files) { // Remove only BMAD files (those with module prefix)
if (file.includes('-') && file.endsWith('.md')) { const files = await fs.readdir(dir);
await fs.remove(path.join(tasksDir, file)); for (const file of files) {
if (file.includes('-') && file.endsWith('.md')) {
await fs.remove(path.join(dir, file));
}
} }
} }
} }

View File

@ -3,6 +3,7 @@ const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { WorkflowCommandGenerator } = require('./workflow-command-generator'); const { WorkflowCommandGenerator } = require('./workflow-command-generator');
const { TaskToolCommandGenerator } = require('./task-tool-command-generator');
const { const {
loadModuleInjectionConfig, loadModuleInjectionConfig,
shouldApplyInjection, shouldApplyInjection,
@ -146,11 +147,22 @@ class ClaudeCodeSetup extends BaseIdeSetup {
const workflowGen = new WorkflowCommandGenerator(); const workflowGen = new WorkflowCommandGenerator();
const workflowResult = await workflowGen.generateWorkflowCommands(projectDir, bmadDir); 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${agentCount} agents installed`));
if (workflowResult.generated > 0) { if (workflowResult.generated > 0) {
console.log(chalk.dim(` - ${workflowResult.generated} workflow commands generated`)); 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)}`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
return { return {

View File

@ -25,79 +25,69 @@ class CrushSetup extends BaseIdeSetup {
// Create .crush/commands/bmad directory structure // Create .crush/commands/bmad directory structure
const crushDir = path.join(projectDir, this.configDir); const crushDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(crushDir, this.commandsDir, 'bmad'); 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(commandsDir);
await this.ensureDir(tasksDir);
// Get agents and tasks // Get agents, tasks, tools, and workflows (standalone only)
const agents = await this.getAgents(bmadDir); 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 // Organize by module
let agentCount = 0; const agentCount = await this.organizeByModule(commandsDir, agents, tasks, tools, workflows, projectDir);
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);
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`)); console.log(chalk.dim(` - ${agentCount.agents} agent commands created`));
console.log(chalk.dim(` - ${taskCount} task 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(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim('\n Commands can be accessed via Crush command palette')); console.log(chalk.dim('\n Commands can be accessed via Crush command palette'));
return { return {
success: true, success: true,
agents: agentCount, ...agentCount,
tasks: taskCount,
}; };
} }
/** /**
* Organize commands by module * Organize commands by module
*/ */
async organizeByModule(commandsDir, agents, tasks, bmadDir) { async organizeByModule(commandsDir, agents, tasks, tools, workflows, projectDir) {
// Get unique modules // Get unique modules
const modules = new Set(); const modules = new Set();
for (const agent of agents) modules.add(agent.module); for (const agent of agents) modules.add(agent.module);
for (const task of tasks) modules.add(task.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 // Create module directories
for (const module of modules) { for (const module of modules) {
const moduleDir = path.join(commandsDir, module); const moduleDir = path.join(commandsDir, module);
const moduleAgentsDir = path.join(moduleDir, 'agents'); const moduleAgentsDir = path.join(moduleDir, 'agents');
const moduleTasksDir = path.join(moduleDir, 'tasks'); 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(moduleAgentsDir);
await this.ensureDir(moduleTasksDir); await this.ensureDir(moduleTasksDir);
await this.ensureDir(moduleToolsDir);
await this.ensureDir(moduleWorkflowsDir);
// Copy module-specific agents // Copy module-specific agents
const moduleAgents = agents.filter((a) => a.module === module); const moduleAgents = agents.filter((a) => a.module === module);
for (const agent of moduleAgents) { for (const agent of moduleAgents) {
const content = await this.readFile(agent.path); 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`); const targetPath = path.join(moduleAgentsDir, `${agent.name}.md`);
await this.writeFile(targetPath, commandContent); await this.writeFile(targetPath, commandContent);
agentCount++;
} }
// Copy module-specific tasks // Copy module-specific tasks
@ -107,8 +97,36 @@ class CrushSetup extends BaseIdeSetup {
const commandContent = this.createTaskCommand(task, content); const commandContent = this.createTaskCommand(task, content);
const targetPath = path.join(moduleTasksDir, `${task.name}.md`); const targetPath = path.join(moduleTasksDir, `${task.name}.md`);
await this.writeFile(targetPath, commandContent); 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) { createTaskCommand(task, content) {
// Extract task name // Extract task name
const nameMatch = content.match(/<name>([^<]+)<\/name>/); const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let commandContent = `# /task-${task.name} Command let commandContent = `# /task-${task.name} Command
@ -177,6 +195,60 @@ Part of the BMAD ${task.module.toUpperCase()} module.
return commandContent; 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 * Format name as title
*/ */

View File

@ -28,18 +28,22 @@ class CursorSetup extends BaseIdeSetup {
await this.ensureDir(bmadRulesDir); await this.ensureDir(bmadRulesDir);
// Get agents and tasks // Get agents, tasks, tools, and workflows (standalone only)
const agents = await this.getAgents(bmadDir); 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 // Create directories for each module
const modules = new Set(); 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) { for (const module of modules) {
await this.ensureDir(path.join(bmadRulesDir, module)); await this.ensureDir(path.join(bmadRulesDir, module));
await this.ensureDir(path.join(bmadRulesDir, module, 'agents')); await this.ensureDir(path.join(bmadRulesDir, module, 'agents'));
await this.ensureDir(path.join(bmadRulesDir, module, 'tasks')); 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 // Process and copy agents
@ -70,36 +74,68 @@ class CursorSetup extends BaseIdeSetup {
taskCount++; 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) // 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${agentCount} agents installed`));
console.log(chalk.dim(` - ${taskCount} tasks 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)}`)); console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, bmadRulesDir)}`));
return { return {
success: true, success: true,
agents: agentCount, agents: agentCount,
tasks: taskCount, tasks: taskCount,
tools: toolCount,
workflows: workflowCount,
}; };
} }
/** /**
* Create BMAD index file for easy navigation * 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'); const indexPath = path.join(bmadRulesDir, 'index.mdc');
let content = `--- let content = `---
description: BMAD Method - Master Index description: BMAD Method - Master Index
globs: globs:
alwaysApply: true alwaysApply: true
--- ---
# BMAD Method - Cursor Rules Index # 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! ## 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 agents: @bmad/{module}/agents/{agent-name}
- Reference specific tasks: @bmad/{module}/tasks/{task-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 entire modules: @bmad/{module}
- Reference this index: @bmad/index - Reference this index: @bmad/index
@ -140,6 +178,26 @@ BMAD rules have been installed to: \`.cursor/rules/bmad/\`
} }
content += '\n'; 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 += ` 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 - All BMAD rules are Manual type - reference them explicitly when needed
- Agents provide persona-based assistance with specific expertise - Agents provide persona-based assistance with specific expertise
- Tasks are reusable workflows for common operations - 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 - Each agent includes an activation block for proper initialization
## Configuration ## Configuration
BMAD rules are configured as Manual rules (alwaysApply: false) to give you control 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 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); await this.writeFile(indexPath, content);
@ -182,6 +242,8 @@ specific agent expertise or task workflows.
// Determine the type and description based on content // Determine the type and description based on content
const isAgent = content.includes('<agent'); const isAgent = content.includes('<agent');
const isTask = content.includes('<task'); const isTask = content.includes('<task');
const isTool = content.includes('<tool');
const isWorkflow = content.includes('workflow:') || content.includes('name:');
let description = ''; let description = '';
let globs = ''; let globs = '';
@ -191,16 +253,22 @@ specific agent expertise or task workflows.
const titleMatch = content.match(/title="([^"]+)"/); const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : metadata.name; const title = titleMatch ? titleMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`; description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
// Manual rules for agents don't need globs
globs = ''; globs = '';
} else if (isTask) { } else if (isTask) {
// Extract task name if available // Extract task name if available
const nameMatch = content.match(/<name>([^<]+)<\/name>/); const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : metadata.name; const taskName = nameMatch ? nameMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`; description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`;
globs = '';
// Tasks might be auto-attached to certain file types } 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 = ''; globs = '';
} else { } else {
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`; description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;

View File

@ -22,7 +22,13 @@ class IdeManager {
// Get all JS files in the IDE directory // Get all JS files in the IDE directory
const files = fs.readdirSync(ideDir).filter((file) => { const files = fs.readdirSync(ideDir).filter((file) => {
// Skip base class, manager, utility files (starting with _), and helper modules // 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 // Sort alphabetically for consistent ordering
@ -41,7 +47,12 @@ class IdeManager {
if (HandlerClass) { if (HandlerClass) {
const instance = new HandlerClass(); const instance = new HandlerClass();
// Use the name property from the instance (set in constructor) // 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) { } catch (error) {
console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`)); console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`));
@ -60,9 +71,17 @@ class IdeManager {
const ides = []; const ides = [];
for (const [key, handler] of this.handlers) { 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({ ides.push({
value: key, value: key,
name: handler.displayName || handler.name || key, name: name,
preferred: handler.preferred || false, preferred: handler.preferred || false,
}); });
} }
@ -71,10 +90,7 @@ class IdeManager {
ides.sort((a, b) => { ides.sort((a, b) => {
if (a.preferred && !b.preferred) return -1; if (a.preferred && !b.preferred) return -1;
if (!a.preferred && b.preferred) return 1; if (!a.preferred && b.preferred) return 1;
// Ensure both names exist before comparing return a.name.localeCompare(b.name);
const nameA = a.name || '';
const nameB = b.name || '';
return nameA.localeCompare(nameB);
}); });
return ides; return ides;

View File

@ -5,6 +5,7 @@ const chalk = require('chalk');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./workflow-command-generator'); const { WorkflowCommandGenerator } = require('./workflow-command-generator');
const { TaskToolCommandGenerator } = require('./task-tool-command-generator');
const { getAgentsFromBmad } = require('./shared/bmad-artifacts'); const { getAgentsFromBmad } = require('./shared/bmad-artifacts');
@ -13,7 +14,7 @@ const { getAgentsFromBmad } = require('./shared/bmad-artifacts');
*/ */
class OpenCodeSetup extends BaseIdeSetup { class OpenCodeSetup extends BaseIdeSetup {
constructor() { constructor() {
super('opencode', 'OpenCode', false); super('opencode', 'OpenCode', true); // Mark as preferred/recommended
this.configDir = '.opencode'; this.configDir = '.opencode';
this.commandsDir = 'command'; this.commandsDir = 'command';
this.agentsDir = 'agent'; this.agentsDir = 'agent';
@ -64,11 +65,22 @@ class OpenCodeSetup extends BaseIdeSetup {
workflowCommandCount++; 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/bmad/`)); console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/bmad/`));
if (workflowCommandCount > 0) { if (workflowCommandCount > 0) {
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated to .opencode/command/bmad/`)); 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 { return {
success: true, success: true,

View File

@ -37,18 +37,22 @@ class QwenSetup extends BaseIdeSetup {
// Clean up old configuration if exists // Clean up old configuration if exists
await this.cleanupOldConfig(qwenDir); 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 agents = await getAgentsFromBmad(bmadDir, options.selectedModules || []);
const tasks = await getTasksFromBmad(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) // Create directories for each module (including standalone)
const modules = new Set(); 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) { for (const module of modules) {
await this.ensureDir(path.join(bmadCommandsDir, module)); await this.ensureDir(path.join(bmadCommandsDir, module));
await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); await this.ensureDir(path.join(bmadCommandsDir, module, 'agents'));
await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); 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 // Create TOML files for each agent
@ -75,7 +79,7 @@ class QwenSetup extends BaseIdeSetup {
name: task.name, 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); 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}`)); 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks 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)}`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
return { return {
success: true, success: true,
agents: agentCount, agents: agentCount,
tasks: taskCount, tasks: taskCount,
tools: toolCount,
workflows: workflowCount,
}; };
} }
@ -177,6 +217,8 @@ class QwenSetup extends BaseIdeSetup {
// Determine the type and description based on content // Determine the type and description based on content
const isAgent = content.includes('<agent'); const isAgent = content.includes('<agent');
const isTask = content.includes('<task'); const isTask = content.includes('<task');
const isTool = content.includes('<tool');
const isWorkflow = content.includes('workflow:') || content.includes('name:');
let description = ''; let description = '';
@ -187,9 +229,17 @@ class QwenSetup extends BaseIdeSetup {
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`; description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
} else if (isTask) { } else if (isTask) {
// Extract task name if available // Extract task name if available
const nameMatch = content.match(/<name>([^<]+)<\/name>/); const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : metadata.name; const taskName = nameMatch ? nameMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`; 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 { } else {
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`; description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;
} }

View File

@ -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 };

View File

@ -27,39 +27,74 @@ class TraeSetup extends BaseIdeSetup {
await this.ensureDir(rulesDir); await this.ensureDir(rulesDir);
// Get agents and tasks // Get agents, tasks, tools, and workflows (standalone only)
const agents = await this.getAgents(bmadDir); 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 // Process agents as rules
let ruleCount = 0; let agentCount = 0;
for (const agent of agents) { for (const agent of agents) {
const content = await this.readFile(agent.path); const content = await this.readFile(agent.path);
const processedContent = this.createAgentRule(agent, content, bmadDir, projectDir); const processedContent = this.createAgentRule(agent, content, bmadDir, projectDir);
const targetPath = path.join(rulesDir, `${agent.module}-${agent.name}.md`); const targetPath = path.join(rulesDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, processedContent); await this.writeFile(targetPath, processedContent);
ruleCount++; agentCount++;
} }
// Process tasks as rules // Process tasks as rules
let taskCount = 0;
for (const task of tasks) { for (const task of tasks) {
const content = await this.readFile(task.path); const content = await this.readFile(task.path);
const processedContent = this.createTaskRule(task, content); const processedContent = this.createTaskRule(task, content);
const targetPath = path.join(rulesDir, `task-${task.module}-${task.name}.md`); const targetPath = path.join(rulesDir, `task-${task.module}-${task.name}.md`);
await this.writeFile(targetPath, processedContent); 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.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(` - Rules directory: ${path.relative(projectDir, rulesDir)}`));
console.log(chalk.dim(` - Agents can be activated with @{agent-name}`)); console.log(chalk.dim(` - Agents can be activated with @{agent-name}`));
return { return {
success: true, 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) { createTaskRule(task, content) {
// Extract task name from content // Extract task name from content
const nameMatch = content.match(/<name>([^<]+)<\/name>/); const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let ruleContent = `# ${taskName} Task Rule let ruleContent = `# ${taskName} Task Rule
@ -139,6 +174,64 @@ Part of the BMAD ${task.module.toUpperCase()} module.
return ruleContent; 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 * Format agent/task name as title
*/ */

View File

@ -27,18 +27,22 @@ class WindsurfSetup extends BaseIdeSetup {
await this.ensureDir(workflowsDir); await this.ensureDir(workflowsDir);
// Get agents and tasks // Get agents, tasks, tools, and workflows (standalone only)
const agents = await this.getAgents(bmadDir); 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 // Create directories for each module
const modules = new Set(); 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) { for (const module of modules) {
await this.ensureDir(path.join(workflowsDir, module)); await this.ensureDir(path.join(workflowsDir, module));
await this.ensureDir(path.join(workflowsDir, module, 'agents')); await this.ensureDir(path.join(workflowsDir, module, 'agents'));
await this.ensureDir(path.join(workflowsDir, module, 'tasks')); 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 // Process agents as workflows with organized structure
@ -65,9 +69,35 @@ class WindsurfSetup extends BaseIdeSetup {
taskCount++; 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${agentCount} agents installed`));
console.log(chalk.dim(` - ${taskCount} tasks 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(` - Organized in modules: ${[...modules].join(', ')}`));
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
@ -75,7 +105,8 @@ class WindsurfSetup extends BaseIdeSetup {
if (options.showHints !== false) { if (options.showHints !== false) {
console.log(chalk.dim('\n Windsurf workflow settings:')); 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: 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')); console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu'));
} }
@ -83,6 +114,8 @@ class WindsurfSetup extends BaseIdeSetup {
success: true, success: true,
agents: agentCount, agents: agentCount,
tasks: taskCount, tasks: taskCount,
tools: toolCount,
workflows: workflowCount,
}; };
} }
@ -111,6 +144,36 @@ description: task-${task.name}
auto_execution_mode: 2 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}`; ${content}`;
return workflowContent; return workflowContent;

View File

@ -24,13 +24,16 @@ class WorkflowCommandGenerator {
return { generated: 0 }; return { generated: 0 };
} }
// Filter to only standalone workflows
const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true);
// Base commands directory // Base commands directory
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad'); const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0; let generatedCount = 0;
// Generate a command file for each workflow, organized by module // Generate a command file for each standalone workflow, organized by module
for (const workflow of workflows) { for (const workflow of standaloneWorkflows) {
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows'); const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir); await fs.ensureDir(moduleWorkflowsDir);
@ -42,7 +45,7 @@ class WorkflowCommandGenerator {
} }
// Also create a workflow launcher README in each module // Also create a workflow launcher README in each module
const groupedWorkflows = this.groupWorkflowsByModule(workflows); const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows);
await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows); await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
return { generated: generatedCount }; return { generated: generatedCount };
@ -55,9 +58,12 @@ class WorkflowCommandGenerator {
return { artifacts: [], counts: { commands: 0, launchers: 0 } }; 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 = []; const artifacts = [];
for (const workflow of workflows) { for (const workflow of standaloneWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir); const commandContent = await this.generateCommandContent(workflow, bmadDir);
artifacts.push({ artifacts.push({
type: 'workflow-command', 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))) { for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) {
artifacts.push({ artifacts.push({
type: 'workflow-launcher', type: 'workflow-launcher',
@ -82,7 +88,7 @@ class WorkflowCommandGenerator {
return { return {
artifacts, artifacts,
counts: { counts: {
commands: workflows.length, commands: standaloneWorkflows.length,
launchers: Object.keys(groupedWorkflows).length, launchers: Object.keys(groupedWorkflows).length,
}, },
}; };

View File

@ -109,6 +109,11 @@ class UI {
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
ideChoices.push(new inquirer.Separator('── Previously Configured ──')); ideChoices.push(new inquirer.Separator('── Previously Configured ──'));
for (const ideValue of configuredIdes) { 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 // Find the IDE in either preferred or other lists
const preferredIde = preferredIdes.find((ide) => ide.value === ideValue); const preferredIde = preferredIdes.find((ide) => ide.value === ideValue);
const otherIde = otherIdes.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 checked: true, // Previously configured IDEs are checked by default
}); });
processedIdes.add(ide.value); 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`));
} }
} }
} }