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">
|
<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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:`));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue