installer fixes
This commit is contained in:
parent
1cb88728e8
commit
63ef5b7bc6
|
|
@ -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">
|
||||
<i>MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER</i>
|
||||
<i>DO NOT skip steps or change the sequence</i>
|
||||
|
|
@ -17,7 +18,8 @@
|
|||
</step>
|
||||
|
||||
<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 n="4" title="Create/Update Index">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<llm critical="true">
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ exit_triggers:
|
|||
- "end party mode"
|
||||
- "stop party mode"
|
||||
|
||||
standalone: true
|
||||
|
||||
web_bundle: false
|
||||
|
|
|
|||
|
|
@ -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:`));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,13 +486,19 @@ 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) {
|
||||
// Filter out any undefined/null values from the IDE list
|
||||
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
|
||||
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 = config.ides.some((ide) => !ideConfigurations[ide]);
|
||||
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
||||
|
||||
if (!needsPrompting) {
|
||||
spinner.start('Configuring IDEs...');
|
||||
|
|
@ -498,7 +510,7 @@ class Installer {
|
|||
console.log = () => {};
|
||||
}
|
||||
|
||||
for (const ide of config.ides) {
|
||||
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}...`;
|
||||
|
|
@ -532,16 +544,20 @@ class Installer {
|
|||
console.log = originalLog;
|
||||
|
||||
if (spinner.isSpinning) {
|
||||
spinner.succeed(`Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`);
|
||||
spinner.succeed(`Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`);
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`));
|
||||
console.log(chalk.green(`✓ Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Copy IDE-specific documentation
|
||||
// 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(config.ides, bmadDir);
|
||||
await this.copyIdeDocumentation(validIdesForDocs, bmadDir);
|
||||
spinner.succeed('IDE documentation copied');
|
||||
}
|
||||
}
|
||||
|
||||
// Run module-specific installers after IDE setup
|
||||
spinner.start('Running module-specific installers...');
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 <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 <task> tag (default: false)
|
||||
const standaloneMatch = content.match(/<task[^>]+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 <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
|
||||
* @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
|
||||
|
|
|
|||
|
|
@ -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<string>} 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,15 +363,90 @@ class BaseIdeSetup {
|
|||
// Recursively scan subdirectories
|
||||
const subFiles = await this.scanDirectory(fullPath, ext);
|
||||
files.push(...subFiles);
|
||||
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
||||
} 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, ext),
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>([^<]+)<\/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,24 +311,21 @@ 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)) {
|
||||
const dirs = [agentsDir, tasksDir, toolsDir, workflowsDir];
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (await fs.pathExists(dir)) {
|
||||
// Remove only BMAD files (those with module prefix)
|
||||
const files = await fs.readdir(agentsDir);
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
if (file.includes('-') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(agentsDir, file));
|
||||
await fs.remove(path.join(dir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>([^<]+)<\/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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,25 +74,57 @@ 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 = `---
|
||||
|
|
@ -99,7 +135,7 @@ 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('<agent');
|
||||
const isTask = content.includes('<task');
|
||||
const isTool = content.includes('<tool');
|
||||
const isWorkflow = content.includes('workflow:') || content.includes('name:');
|
||||
|
||||
let description = '';
|
||||
let globs = '';
|
||||
|
|
@ -191,16 +253,22 @@ specific agent expertise or task workflows.
|
|||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : metadata.name;
|
||||
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
|
||||
|
||||
// Manual rules for agents don't need globs
|
||||
globs = '';
|
||||
} else if (isTask) {
|
||||
// Extract task name if available
|
||||
const nameMatch = content.match(/<name>([^<]+)<\/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}`;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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('<agent');
|
||||
const isTask = content.includes('<task');
|
||||
const isTool = content.includes('<tool');
|
||||
const isWorkflow = content.includes('workflow:') || content.includes('name:');
|
||||
|
||||
let description = '';
|
||||
|
||||
|
|
@ -187,9 +229,17 @@ class QwenSetup extends BaseIdeSetup {
|
|||
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
|
||||
} else if (isTask) {
|
||||
// Extract task name if available
|
||||
const nameMatch = content.match(/<name>([^<]+)<\/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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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>([^<]+)<\/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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue