mirror of
https://github.com/bmadcode/BMAD-METHOD.git
synced 2025-12-29 16:14:59 +00:00
installer fixes
This commit is contained in:
@@ -599,6 +599,7 @@ class DependencyResolver {
|
||||
organized[module] = {
|
||||
agents: [],
|
||||
tasks: [],
|
||||
tools: [],
|
||||
templates: [],
|
||||
data: [],
|
||||
other: [],
|
||||
@@ -626,6 +627,8 @@ class DependencyResolver {
|
||||
organized[module].agents.push(file);
|
||||
} else if (relative.startsWith('tasks/') || file.includes('/tasks/')) {
|
||||
organized[module].tasks.push(file);
|
||||
} else if (relative.startsWith('tools/') || file.includes('/tools/')) {
|
||||
organized[module].tools.push(file);
|
||||
} else if (relative.includes('template') || file.includes('/templates/')) {
|
||||
organized[module].templates.push(file);
|
||||
} else if (relative.includes('data/')) {
|
||||
@@ -646,7 +649,8 @@ class DependencyResolver {
|
||||
|
||||
for (const [module, files] of Object.entries(organized)) {
|
||||
const isSelected = selectedModules.includes(module) || module === 'core';
|
||||
const totalFiles = files.agents.length + files.tasks.length + files.templates.length + files.data.length + files.other.length;
|
||||
const totalFiles =
|
||||
files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;
|
||||
|
||||
if (totalFiles > 0) {
|
||||
console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`));
|
||||
|
||||
@@ -117,7 +117,8 @@ class Detector {
|
||||
|
||||
// Check for IDE configurations from manifest
|
||||
if (result.manifest && result.manifest.ides) {
|
||||
result.ides = result.manifest.ides;
|
||||
// Filter out any undefined/null values
|
||||
result.ides = result.manifest.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
}
|
||||
|
||||
// Mark as installed if we found core or modules
|
||||
|
||||
@@ -439,7 +439,13 @@ class Installer {
|
||||
// Install partial modules (only dependencies)
|
||||
for (const [module, files] of Object.entries(resolution.byModule)) {
|
||||
if (!config.modules.includes(module) && module !== 'core') {
|
||||
const totalFiles = files.agents.length + files.tasks.length + files.templates.length + files.data.length + files.other.length;
|
||||
const totalFiles =
|
||||
files.agents.length +
|
||||
files.tasks.length +
|
||||
files.tools.length +
|
||||
files.templates.length +
|
||||
files.data.length +
|
||||
files.other.length;
|
||||
if (totalFiles > 0) {
|
||||
spinner.start(`Installing ${module} dependencies...`);
|
||||
await this.installPartialModule(module, bmadDir, files);
|
||||
@@ -480,67 +486,77 @@ class Installer {
|
||||
});
|
||||
|
||||
spinner.succeed(
|
||||
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.files} files`,
|
||||
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
||||
);
|
||||
|
||||
// Configure IDEs and copy documentation
|
||||
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
||||
// Check if any IDE might need prompting (no pre-collected config)
|
||||
const needsPrompting = config.ides.some((ide) => !ideConfigurations[ide]);
|
||||
// Filter out any undefined/null values from the IDE list
|
||||
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
|
||||
if (!needsPrompting) {
|
||||
spinner.start('Configuring IDEs...');
|
||||
}
|
||||
if (validIdes.length === 0) {
|
||||
console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.'));
|
||||
} else {
|
||||
// Check if any IDE might need prompting (no pre-collected config)
|
||||
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
||||
|
||||
// Temporarily suppress console output if not verbose
|
||||
const originalLog = console.log;
|
||||
if (!config.verbose) {
|
||||
console.log = () => {};
|
||||
}
|
||||
|
||||
for (const ide of config.ides) {
|
||||
// Only show spinner if we have pre-collected config (no prompts expected)
|
||||
if (ideConfigurations[ide] && !needsPrompting) {
|
||||
spinner.text = `Configuring ${ide}...`;
|
||||
} else if (!ideConfigurations[ide]) {
|
||||
// Stop spinner before prompting
|
||||
if (spinner.isSpinning) {
|
||||
spinner.stop();
|
||||
}
|
||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||
}
|
||||
|
||||
// Pass pre-collected configuration to avoid re-prompting
|
||||
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||
selectedModules: config.modules || [],
|
||||
preCollectedConfig: ideConfigurations[ide] || null,
|
||||
verbose: config.verbose,
|
||||
});
|
||||
|
||||
// Save IDE configuration for future updates
|
||||
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
||||
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
||||
}
|
||||
|
||||
// Restart spinner if we stopped it
|
||||
if (!ideConfigurations[ide] && !spinner.isSpinning) {
|
||||
if (!needsPrompting) {
|
||||
spinner.start('Configuring IDEs...');
|
||||
}
|
||||
|
||||
// Temporarily suppress console output if not verbose
|
||||
const originalLog = console.log;
|
||||
if (!config.verbose) {
|
||||
console.log = () => {};
|
||||
}
|
||||
|
||||
for (const ide of validIdes) {
|
||||
// Only show spinner if we have pre-collected config (no prompts expected)
|
||||
if (ideConfigurations[ide] && !needsPrompting) {
|
||||
spinner.text = `Configuring ${ide}...`;
|
||||
} else if (!ideConfigurations[ide]) {
|
||||
// Stop spinner before prompting
|
||||
if (spinner.isSpinning) {
|
||||
spinner.stop();
|
||||
}
|
||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||
}
|
||||
|
||||
// Pass pre-collected configuration to avoid re-prompting
|
||||
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||
selectedModules: config.modules || [],
|
||||
preCollectedConfig: ideConfigurations[ide] || null,
|
||||
verbose: config.verbose,
|
||||
});
|
||||
|
||||
// Save IDE configuration for future updates
|
||||
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
||||
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
||||
}
|
||||
|
||||
// Restart spinner if we stopped it
|
||||
if (!ideConfigurations[ide] && !spinner.isSpinning) {
|
||||
spinner.start('Configuring IDEs...');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore console.log
|
||||
console.log = originalLog;
|
||||
|
||||
if (spinner.isSpinning) {
|
||||
spinner.succeed(`Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`);
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Restore console.log
|
||||
console.log = originalLog;
|
||||
|
||||
if (spinner.isSpinning) {
|
||||
spinner.succeed(`Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`);
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`));
|
||||
// Copy IDE-specific documentation (only for valid IDEs)
|
||||
const validIdesForDocs = (config.ides || []).filter((ide) => ide && typeof ide === 'string');
|
||||
if (validIdesForDocs.length > 0) {
|
||||
spinner.start('Copying IDE documentation...');
|
||||
await this.copyIdeDocumentation(validIdesForDocs, bmadDir);
|
||||
spinner.succeed('IDE documentation copied');
|
||||
}
|
||||
|
||||
// Copy IDE-specific documentation
|
||||
spinner.start('Copying IDE documentation...');
|
||||
await this.copyIdeDocumentation(config.ides, bmadDir);
|
||||
spinner.succeed('IDE documentation copied');
|
||||
}
|
||||
|
||||
// Run module-specific installers after IDE setup
|
||||
@@ -959,6 +975,22 @@ class Installer {
|
||||
}
|
||||
}
|
||||
|
||||
if (files.tools && files.tools.length > 0) {
|
||||
const toolsDir = path.join(targetBase, 'tools');
|
||||
await fs.ensureDir(toolsDir);
|
||||
|
||||
for (const toolPath of files.tools) {
|
||||
const fileName = path.basename(toolPath);
|
||||
const sourcePath = path.join(sourceBase, 'tools', fileName);
|
||||
const targetPath = path.join(toolsDir, fileName);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await fs.copy(sourcePath, targetPath);
|
||||
this.installedFiles.push(targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.templates && files.templates.length > 0) {
|
||||
const templatesDir = path.join(targetBase, 'templates');
|
||||
await fs.ensureDir(templatesDir);
|
||||
|
||||
@@ -12,6 +12,7 @@ class ManifestGenerator {
|
||||
this.workflows = [];
|
||||
this.agents = [];
|
||||
this.tasks = [];
|
||||
this.tools = [];
|
||||
this.modules = [];
|
||||
this.files = [];
|
||||
this.selectedIdes = [];
|
||||
@@ -45,7 +46,8 @@ class ManifestGenerator {
|
||||
throw new TypeError('ManifestGenerator expected `options.ides` to be an array.');
|
||||
}
|
||||
|
||||
this.selectedIdes = resolvedIdes;
|
||||
// Filter out any undefined/null values from IDE list
|
||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||
|
||||
// Collect workflow data
|
||||
await this.collectWorkflows(selectedModules);
|
||||
@@ -56,12 +58,16 @@ class ManifestGenerator {
|
||||
// Collect task data
|
||||
await this.collectTasks(selectedModules);
|
||||
|
||||
// Collect tool data
|
||||
await this.collectTools(selectedModules);
|
||||
|
||||
// Write manifest files and collect their paths
|
||||
const manifestFiles = [
|
||||
await this.writeMainManifest(cfgDir),
|
||||
await this.writeWorkflowManifest(cfgDir),
|
||||
await this.writeAgentManifest(cfgDir),
|
||||
await this.writeTaskManifest(cfgDir),
|
||||
await this.writeToolManifest(cfgDir),
|
||||
await this.writeFilesManifest(cfgDir),
|
||||
];
|
||||
|
||||
@@ -69,6 +75,7 @@ class ManifestGenerator {
|
||||
workflows: this.workflows.length,
|
||||
agents: this.agents.length,
|
||||
tasks: this.tasks.length,
|
||||
tools: this.tools.length,
|
||||
files: this.files.length,
|
||||
manifestFiles: manifestFiles,
|
||||
};
|
||||
@@ -133,11 +140,15 @@ class ManifestGenerator {
|
||||
? `bmad/core/workflows/${relativePath}/workflow.yaml`
|
||||
: `bmad/${moduleName}/workflows/${relativePath}/workflow.yaml`;
|
||||
|
||||
// Check for standalone property (default: false)
|
||||
const standalone = workflow.standalone === true;
|
||||
|
||||
workflows.push({
|
||||
name: workflow.name,
|
||||
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
standalone: standalone,
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
@@ -306,24 +317,34 @@ class ManifestGenerator {
|
||||
const files = await fs.readdir(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.md')) {
|
||||
// Check for both .xml and .md files
|
||||
if (file.endsWith('.xml') || file.endsWith('.md')) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Extract task metadata from content if possible
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
|
||||
// Try description attribute first, fall back to <objective> element
|
||||
const descMatch = content.match(/description="([^"]+)"/);
|
||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||
const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
||||
|
||||
// Check for standalone attribute in <task> tag (default: false)
|
||||
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
|
||||
const standalone = !!standaloneMatch;
|
||||
|
||||
// Build relative path for installation
|
||||
const installPath = moduleName === 'core' ? `bmad/core/tasks/${file}` : `bmad/${moduleName}/tasks/${file}`;
|
||||
|
||||
const taskName = file.replace('.md', '');
|
||||
const taskName = file.replace(/\.(xml|md)$/, '');
|
||||
tasks.push({
|
||||
name: taskName,
|
||||
displayName: nameMatch ? nameMatch[1] : taskName,
|
||||
description: objMatch ? objMatch[1].trim().replaceAll('"', '""') : '',
|
||||
description: description.replaceAll('"', '""'),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
standalone: standalone,
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
@@ -339,6 +360,82 @@ class ManifestGenerator {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all tools from core and selected modules
|
||||
* Scans the INSTALLED bmad directory, not the source
|
||||
*/
|
||||
async collectTools(selectedModules) {
|
||||
this.tools = [];
|
||||
|
||||
// Get core tools from installed bmad directory
|
||||
const coreToolsPath = path.join(this.bmadDir, 'core', 'tools');
|
||||
if (await fs.pathExists(coreToolsPath)) {
|
||||
const coreTools = await this.getToolsFromDir(coreToolsPath, 'core');
|
||||
this.tools.push(...coreTools);
|
||||
}
|
||||
|
||||
// Get module tools from installed bmad directory
|
||||
for (const moduleName of selectedModules) {
|
||||
const toolsPath = path.join(this.bmadDir, moduleName, 'tools');
|
||||
|
||||
if (await fs.pathExists(toolsPath)) {
|
||||
const moduleTools = await this.getToolsFromDir(toolsPath, moduleName);
|
||||
this.tools.push(...moduleTools);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools from a directory
|
||||
*/
|
||||
async getToolsFromDir(dirPath, moduleName) {
|
||||
const tools = [];
|
||||
const files = await fs.readdir(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
// Check for both .xml and .md files
|
||||
if (file.endsWith('.xml') || file.endsWith('.md')) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Extract tool metadata from content if possible
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
|
||||
// Try description attribute first, fall back to <objective> element
|
||||
const descMatch = content.match(/description="([^"]+)"/);
|
||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||
const description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
||||
|
||||
// Check for standalone attribute in <tool> tag (default: false)
|
||||
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
|
||||
const standalone = !!standaloneMatch;
|
||||
|
||||
// Build relative path for installation
|
||||
const installPath = moduleName === 'core' ? `bmad/core/tools/${file}` : `bmad/${moduleName}/tools/${file}`;
|
||||
|
||||
const toolName = file.replace(/\.(xml|md)$/, '');
|
||||
tools.push({
|
||||
name: toolName,
|
||||
displayName: nameMatch ? nameMatch[1] : toolName,
|
||||
description: description.replaceAll('"', '""'),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
standalone: standalone,
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
this.files.push({
|
||||
type: 'tool',
|
||||
name: toolName,
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write main manifest as YAML with installation info only
|
||||
* @returns {string} Path to the manifest file
|
||||
@@ -416,12 +513,12 @@ class ManifestGenerator {
|
||||
// Get preserved rows from existing CSV (module is column 2, 0-indexed)
|
||||
const preservedRows = await this.getPreservedCsvRows(csvPath, 2);
|
||||
|
||||
// Create CSV header
|
||||
let csv = 'name,description,module,path\n';
|
||||
// Create CSV header with standalone column
|
||||
let csv = 'name,description,module,path,standalone\n';
|
||||
|
||||
// Add new rows for updated modules
|
||||
for (const workflow of this.workflows) {
|
||||
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
|
||||
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}","${workflow.standalone}"\n`;
|
||||
}
|
||||
|
||||
// Add preserved rows for modules we didn't update
|
||||
@@ -470,12 +567,39 @@ class ManifestGenerator {
|
||||
// Get preserved rows from existing CSV (module is column 3, 0-indexed)
|
||||
const preservedRows = await this.getPreservedCsvRows(csvPath, 3);
|
||||
|
||||
// Create CSV header
|
||||
let csv = 'name,displayName,description,module,path\n';
|
||||
// Create CSV header with standalone column
|
||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||
|
||||
// Add new rows for updated modules
|
||||
for (const task of this.tasks) {
|
||||
csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}"\n`;
|
||||
csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"\n`;
|
||||
}
|
||||
|
||||
// Add preserved rows for modules we didn't update
|
||||
for (const row of preservedRows) {
|
||||
csv += row + '\n';
|
||||
}
|
||||
|
||||
await fs.writeFile(csvPath, csv);
|
||||
return csvPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write tool manifest CSV
|
||||
* @returns {string} Path to the manifest file
|
||||
*/
|
||||
async writeToolManifest(cfgDir) {
|
||||
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
||||
|
||||
// Get preserved rows from existing CSV (module is column 3, 0-indexed)
|
||||
const preservedRows = await this.getPreservedCsvRows(csvPath, 3);
|
||||
|
||||
// Create CSV header with standalone column
|
||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||
|
||||
// Add new rows for updated modules
|
||||
for (const tool of this.tools) {
|
||||
csv += `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"\n`;
|
||||
}
|
||||
|
||||
// Add preserved rows for modules we didn't update
|
||||
|
||||
Reference in New Issue
Block a user