fix: installer IDE selection and cancellation handling

- Fix manifest reading to use manifest.yaml instead of manifest.csv
- Show previously configured IDEs as selected by default in UI
- Skip configuration prompts for already configured IDEs during updates
- Properly collect IDE configurations during full reinstall
- Handle installation cancellation without throwing errors
This commit is contained in:
Brian Madison
2025-10-04 19:46:16 -05:00
parent c632564849
commit 6181a0bd07
3 changed files with 151 additions and 83 deletions

View File

@@ -26,6 +26,14 @@ module.exports = {
// Regular install/update flow
const result = await installer.install(config);
// Check if installation was cancelled
if (result && result.cancelled) {
process.exit(0);
return;
}
// Check if installation succeeded
if (result && result.success) {
console.log(chalk.green('\n✨ Installation complete!'));
console.log(
chalk.cyan('BMAD Core and Selected Modules have been installed to:'),
@@ -34,6 +42,7 @@ module.exports = {
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
console.log(chalk.cyan('Check docs/alpha-release-notes.md in this repository for important information.'));
process.exit(0);
}
} catch (error) {
// Check if error has a complete formatted message
if (error.fullMessage) {

View File

@@ -37,20 +37,30 @@ class Installer {
* @param {Array} selectedModules - Selected modules from configuration
* @returns {Object} Tool/IDE selection and configurations
*/
async collectToolConfigurations(projectDir, selectedModules) {
async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false) {
// Prompt for tool selection
const { UI } = require('../../../lib/ui');
const ui = new UI();
const toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
// Check for already configured IDEs (but ignore them if doing a full reinstall)
const { Detector } = require('./detector');
const detector = new Detector();
const bmadDir = path.join(projectDir, 'bmad');
const existingInstall = await detector.detect(bmadDir);
const previouslyConfiguredIdes = isFullReinstall ? [] : existingInstall.ides || [];
// Collect IDE-specific configurations if any were selected
const ideConfigurations = {};
const bmadDir = path.join(projectDir, 'bmad');
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
// Determine which IDEs are newly selected (not previously configured)
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
if (newlySelectedIdes.length > 0) {
console.log('\n'); // Add spacing before IDE questions
for (const ide of toolConfig.ides) {
for (const ide of newlySelectedIdes) {
// List of IDEs that have interactive prompts
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini'].includes(ide);
@@ -96,6 +106,13 @@ class Installer {
}
}
// Log which IDEs are already configured and being kept
const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide));
if (keptIdes.length > 0) {
console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`));
}
}
return {
ides: toolConfig.ides,
skipIde: toolConfig.skipIde,
@@ -139,12 +156,7 @@ class Installer {
// Collect configurations for modules (core was already collected in UI.promptInstall if interactive)
const moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory));
const toolSelection = await this.collectToolConfigurations(path.resolve(config.directory), config.modules);
// Merge tool selection into config
config.ides = toolSelection.ides;
config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations;
// Tool selection will be collected after we determine if it's a reinstall/update/new install
const spinner = ora('Preparing installation...').start();
@@ -188,7 +200,7 @@ class Installer {
const { action } = await this.promptUpdateAction();
if (action === 'cancel') {
console.log('Installation cancelled.');
return;
return { success: false, cancelled: true };
}
if (action === 'reinstall') {
@@ -208,12 +220,15 @@ class Installer {
if (!confirmReinstall) {
console.log('Installation cancelled.');
return;
return { success: false, cancelled: true };
}
// Remove existing installation
await fs.remove(bmadDir);
console.log(chalk.green('✓ Removed existing installation\n'));
// Mark this as a full reinstall so we re-collect IDE configurations
config._isFullReinstall = true;
} else if (action === 'update') {
// Store that we're updating for later processing
config._isUpdate = true;
@@ -275,6 +290,21 @@ class Installer {
}
}
// Now collect tool configurations after we know if it's a reinstall
spinner.stop();
const toolSelection = await this.collectToolConfigurations(
path.resolve(config.directory),
config.modules,
config._isFullReinstall || false,
);
// Merge tool selection into config
config.ides = toolSelection.ides;
config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations;
spinner.start('Continuing installation...');
// Create bmad directory structure
spinner.text = 'Creating directory structure...';
await this.createDirectoryStructure(bmadDir);

View File

@@ -10,25 +10,32 @@ class Manifest {
* @param {Array} installedFiles - List of installed files (no longer used, files tracked in files-manifest.csv)
*/
async create(bmadDir, data, installedFiles = []) {
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.csv');
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
const yaml = require('js-yaml');
// Ensure _cfg directory exists
await fs.ensureDir(path.dirname(manifestPath));
// Load module configs to get module metadata
// If core is installed, add it to modules list
const allModules = [...(data.modules || [])];
if (data.core) {
allModules.unshift('core'); // Add core at the beginning
}
const moduleConfigs = await this.loadModuleConfigs(allModules);
// Structure the manifest data
const manifestData = {
installation: {
version: data.version || require(path.join(process.cwd(), 'package.json')).version,
installDate: data.installDate || new Date().toISOString(),
lastUpdated: data.lastUpdated || new Date().toISOString(),
},
modules: data.modules || [],
ides: data.ides || [],
};
// Don't store installation path in manifest
// Write YAML manifest
const yamlContent = yaml.dump(manifestData, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
// Generate CSV content (no file metadata)
const csvContent = this.generateManifestCsv({ ...data, modules: allModules }, [], moduleConfigs);
await fs.writeFile(manifestPath, csvContent, 'utf8');
await fs.writeFile(manifestPath, yamlContent, 'utf8');
return { success: true, path: manifestPath, filesTracked: 0 };
}
@@ -38,14 +45,24 @@ class Manifest {
* @returns {Object|null} Manifest data or null if not found
*/
async read(bmadDir) {
const csvPath = path.join(bmadDir, '_cfg', 'manifest.csv');
const yamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
const yaml = require('js-yaml');
if (await fs.pathExists(csvPath)) {
if (await fs.pathExists(yamlPath)) {
try {
const content = await fs.readFile(csvPath, 'utf8');
return this.parseManifestCsv(content);
const content = await fs.readFile(yamlPath, 'utf8');
const manifestData = yaml.load(content);
// Flatten the structure for compatibility with existing code
return {
version: manifestData.installation?.version,
installDate: manifestData.installation?.installDate,
lastUpdated: manifestData.installation?.lastUpdated,
modules: manifestData.modules || [],
ides: manifestData.ides || [],
};
} catch (error) {
console.error('Failed to read CSV manifest:', error.message);
console.error('Failed to read YAML manifest:', error.message);
}
}
@@ -59,25 +76,37 @@ class Manifest {
* @param {Array} installedFiles - Updated list of installed files
*/
async update(bmadDir, updates, installedFiles = null) {
const yaml = require('js-yaml');
const manifest = (await this.read(bmadDir)) || {};
// Merge updates
Object.assign(manifest, updates);
manifest.lastUpdated = new Date().toISOString();
// If new file list provided, reparse metadata
let fileMetadata = manifest.files || [];
if (installedFiles) {
fileMetadata = await this.parseInstalledFiles(installedFiles);
}
// Convert back to structured format for YAML
const manifestData = {
installation: {
version: manifest.version,
installDate: manifest.installDate,
lastUpdated: manifest.lastUpdated,
},
modules: manifest.modules || [],
ides: manifest.ides || [],
};
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.csv');
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
await fs.ensureDir(path.dirname(manifestPath));
const csvContent = this.generateManifestCsv({ ...manifest, ...updates }, fileMetadata);
await fs.writeFile(manifestPath, csvContent, 'utf8');
const yamlContent = yaml.dump(manifestData, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
return { ...manifest, ...updates, files: fileMetadata };
await fs.writeFile(manifestPath, yamlContent, 'utf8');
return manifest;
}
/**