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:
@@ -26,14 +26,23 @@ module.exports = {
|
|||||||
// Regular install/update flow
|
// Regular install/update flow
|
||||||
const result = await installer.install(config);
|
const result = await installer.install(config);
|
||||||
|
|
||||||
console.log(chalk.green('\n✨ Installation complete!'));
|
// Check if installation was cancelled
|
||||||
console.log(
|
if (result && result.cancelled) {
|
||||||
chalk.cyan('BMAD Core and Selected Modules have been installed to:'),
|
process.exit(0);
|
||||||
chalk.bold(result.path || path.resolve(config.directory, 'bmad')),
|
return;
|
||||||
);
|
}
|
||||||
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.'));
|
// Check if installation succeeded
|
||||||
process.exit(0);
|
if (result && result.success) {
|
||||||
|
console.log(chalk.green('\n✨ Installation complete!'));
|
||||||
|
console.log(
|
||||||
|
chalk.cyan('BMAD Core and Selected Modules have been installed to:'),
|
||||||
|
chalk.bold(result.path || path.resolve(config.directory, 'bmad')),
|
||||||
|
);
|
||||||
|
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) {
|
} catch (error) {
|
||||||
// Check if error has a complete formatted message
|
// Check if error has a complete formatted message
|
||||||
if (error.fullMessage) {
|
if (error.fullMessage) {
|
||||||
|
|||||||
@@ -37,63 +37,80 @@ class Installer {
|
|||||||
* @param {Array} selectedModules - Selected modules from configuration
|
* @param {Array} selectedModules - Selected modules from configuration
|
||||||
* @returns {Object} Tool/IDE selection and configurations
|
* @returns {Object} Tool/IDE selection and configurations
|
||||||
*/
|
*/
|
||||||
async collectToolConfigurations(projectDir, selectedModules) {
|
async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false) {
|
||||||
// Prompt for tool selection
|
// Prompt for tool selection
|
||||||
const { UI } = require('../../../lib/ui');
|
const { UI } = require('../../../lib/ui');
|
||||||
const ui = new UI();
|
const ui = new UI();
|
||||||
const toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
|
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
|
// Collect IDE-specific configurations if any were selected
|
||||||
const ideConfigurations = {};
|
const ideConfigurations = {};
|
||||||
const bmadDir = path.join(projectDir, 'bmad');
|
|
||||||
|
|
||||||
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
||||||
console.log('\n'); // Add spacing before IDE questions
|
// Determine which IDEs are newly selected (not previously configured)
|
||||||
|
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
||||||
|
|
||||||
for (const ide of toolConfig.ides) {
|
if (newlySelectedIdes.length > 0) {
|
||||||
// List of IDEs that have interactive prompts
|
console.log('\n'); // Add spacing before IDE questions
|
||||||
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini'].includes(ide);
|
|
||||||
|
|
||||||
if (needsPrompts) {
|
for (const ide of newlySelectedIdes) {
|
||||||
// Get IDE handler and collect configuration
|
// List of IDEs that have interactive prompts
|
||||||
try {
|
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini'].includes(ide);
|
||||||
// Dynamically load the IDE setup module
|
|
||||||
const ideModule = require(`../ide/${ide}`);
|
|
||||||
|
|
||||||
// Get the setup class (handle different export formats)
|
if (needsPrompts) {
|
||||||
let SetupClass;
|
// Get IDE handler and collect configuration
|
||||||
const className =
|
try {
|
||||||
ide
|
// Dynamically load the IDE setup module
|
||||||
.split('-')
|
const ideModule = require(`../ide/${ide}`);
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join('') + 'Setup';
|
|
||||||
|
|
||||||
if (ideModule[className]) {
|
// Get the setup class (handle different export formats)
|
||||||
SetupClass = ideModule[className];
|
let SetupClass;
|
||||||
} else if (ideModule.default) {
|
const className =
|
||||||
SetupClass = ideModule.default;
|
ide
|
||||||
} else {
|
.split('-')
|
||||||
// Skip if no setup class found
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
continue;
|
.join('') + 'Setup';
|
||||||
|
|
||||||
|
if (ideModule[className]) {
|
||||||
|
SetupClass = ideModule[className];
|
||||||
|
} else if (ideModule.default) {
|
||||||
|
SetupClass = ideModule.default;
|
||||||
|
} else {
|
||||||
|
// Skip if no setup class found
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ideSetup = new SetupClass();
|
||||||
|
|
||||||
|
// Check if this IDE has a collectConfiguration method
|
||||||
|
if (typeof ideSetup.collectConfiguration === 'function') {
|
||||||
|
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||||
|
ideConfigurations[ide] = await ideSetup.collectConfiguration({
|
||||||
|
selectedModules: selectedModules || [],
|
||||||
|
projectDir,
|
||||||
|
bmadDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// IDE doesn't have a setup file or collectConfiguration method
|
||||||
|
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ideSetup = new SetupClass();
|
|
||||||
|
|
||||||
// Check if this IDE has a collectConfiguration method
|
|
||||||
if (typeof ideSetup.collectConfiguration === 'function') {
|
|
||||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
|
||||||
ideConfigurations[ide] = await ideSetup.collectConfiguration({
|
|
||||||
selectedModules: selectedModules || [],
|
|
||||||
projectDir,
|
|
||||||
bmadDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// IDE doesn't have a setup file or collectConfiguration method
|
|
||||||
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
@@ -139,12 +156,7 @@ class Installer {
|
|||||||
// Collect configurations for modules (core was already collected in UI.promptInstall if interactive)
|
// 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 moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory));
|
||||||
|
|
||||||
const toolSelection = await this.collectToolConfigurations(path.resolve(config.directory), config.modules);
|
// Tool selection will be collected after we determine if it's a reinstall/update/new install
|
||||||
|
|
||||||
// Merge tool selection into config
|
|
||||||
config.ides = toolSelection.ides;
|
|
||||||
config.skipIde = toolSelection.skipIde;
|
|
||||||
const ideConfigurations = toolSelection.configurations;
|
|
||||||
|
|
||||||
const spinner = ora('Preparing installation...').start();
|
const spinner = ora('Preparing installation...').start();
|
||||||
|
|
||||||
@@ -188,7 +200,7 @@ class Installer {
|
|||||||
const { action } = await this.promptUpdateAction();
|
const { action } = await this.promptUpdateAction();
|
||||||
if (action === 'cancel') {
|
if (action === 'cancel') {
|
||||||
console.log('Installation cancelled.');
|
console.log('Installation cancelled.');
|
||||||
return;
|
return { success: false, cancelled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'reinstall') {
|
if (action === 'reinstall') {
|
||||||
@@ -208,12 +220,15 @@ class Installer {
|
|||||||
|
|
||||||
if (!confirmReinstall) {
|
if (!confirmReinstall) {
|
||||||
console.log('Installation cancelled.');
|
console.log('Installation cancelled.');
|
||||||
return;
|
return { success: false, cancelled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing installation
|
// Remove existing installation
|
||||||
await fs.remove(bmadDir);
|
await fs.remove(bmadDir);
|
||||||
console.log(chalk.green('✓ Removed existing installation\n'));
|
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') {
|
} else if (action === 'update') {
|
||||||
// Store that we're updating for later processing
|
// Store that we're updating for later processing
|
||||||
config._isUpdate = true;
|
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
|
// Create bmad directory structure
|
||||||
spinner.text = 'Creating directory structure...';
|
spinner.text = 'Creating directory structure...';
|
||||||
await this.createDirectoryStructure(bmadDir);
|
await this.createDirectoryStructure(bmadDir);
|
||||||
|
|||||||
@@ -10,25 +10,32 @@ class Manifest {
|
|||||||
* @param {Array} installedFiles - List of installed files (no longer used, files tracked in files-manifest.csv)
|
* @param {Array} installedFiles - List of installed files (no longer used, files tracked in files-manifest.csv)
|
||||||
*/
|
*/
|
||||||
async create(bmadDir, data, installedFiles = []) {
|
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
|
// Ensure _cfg directory exists
|
||||||
await fs.ensureDir(path.dirname(manifestPath));
|
await fs.ensureDir(path.dirname(manifestPath));
|
||||||
|
|
||||||
// Load module configs to get module metadata
|
// Structure the manifest data
|
||||||
// If core is installed, add it to modules list
|
const manifestData = {
|
||||||
const allModules = [...(data.modules || [])];
|
installation: {
|
||||||
if (data.core) {
|
version: data.version || require(path.join(process.cwd(), 'package.json')).version,
|
||||||
allModules.unshift('core'); // Add core at the beginning
|
installDate: data.installDate || new Date().toISOString(),
|
||||||
}
|
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
||||||
const moduleConfigs = await this.loadModuleConfigs(allModules);
|
},
|
||||||
|
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)
|
await fs.writeFile(manifestPath, yamlContent, 'utf8');
|
||||||
const csvContent = this.generateManifestCsv({ ...data, modules: allModules }, [], moduleConfigs);
|
|
||||||
|
|
||||||
await fs.writeFile(manifestPath, csvContent, 'utf8');
|
|
||||||
return { success: true, path: manifestPath, filesTracked: 0 };
|
return { success: true, path: manifestPath, filesTracked: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,14 +45,24 @@ class Manifest {
|
|||||||
* @returns {Object|null} Manifest data or null if not found
|
* @returns {Object|null} Manifest data or null if not found
|
||||||
*/
|
*/
|
||||||
async read(bmadDir) {
|
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 {
|
try {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(yamlPath, 'utf8');
|
||||||
return this.parseManifestCsv(content);
|
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) {
|
} 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
|
* @param {Array} installedFiles - Updated list of installed files
|
||||||
*/
|
*/
|
||||||
async update(bmadDir, updates, installedFiles = null) {
|
async update(bmadDir, updates, installedFiles = null) {
|
||||||
|
const yaml = require('js-yaml');
|
||||||
const manifest = (await this.read(bmadDir)) || {};
|
const manifest = (await this.read(bmadDir)) || {};
|
||||||
|
|
||||||
// Merge updates
|
// Merge updates
|
||||||
Object.assign(manifest, updates);
|
Object.assign(manifest, updates);
|
||||||
manifest.lastUpdated = new Date().toISOString();
|
manifest.lastUpdated = new Date().toISOString();
|
||||||
|
|
||||||
// If new file list provided, reparse metadata
|
// Convert back to structured format for YAML
|
||||||
let fileMetadata = manifest.files || [];
|
const manifestData = {
|
||||||
if (installedFiles) {
|
installation: {
|
||||||
fileMetadata = await this.parseInstalledFiles(installedFiles);
|
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));
|
await fs.ensureDir(path.dirname(manifestPath));
|
||||||
|
|
||||||
const csvContent = this.generateManifestCsv({ ...manifest, ...updates }, fileMetadata);
|
const yamlContent = yaml.dump(manifestData, {
|
||||||
await fs.writeFile(manifestPath, csvContent, 'utf8');
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: false,
|
||||||
|
});
|
||||||
|
|
||||||
return { ...manifest, ...updates, files: fileMetadata };
|
await fs.writeFile(manifestPath, yamlContent, 'utf8');
|
||||||
|
|
||||||
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user