installer fixes

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

View File

@@ -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:`));

View File

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

View File

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

View File

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