diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index 64bf7a54..714b45ae 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -26,14 +26,23 @@ module.exports = { // Regular install/update flow const result = await installer.install(config); - 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); + // 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:'), + 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) { // Check if error has a complete formatted message if (error.fullMessage) { diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 12b4bc33..7a72ebe1 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -37,63 +37,80 @@ 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) { - 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) { - // List of IDEs that have interactive prompts - const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini'].includes(ide); + if (newlySelectedIdes.length > 0) { + console.log('\n'); // Add spacing before IDE questions - if (needsPrompts) { - // Get IDE handler and collect configuration - try { - // Dynamically load the IDE setup module - const ideModule = require(`../ide/${ide}`); + 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); - // Get the setup class (handle different export formats) - let SetupClass; - const className = - ide - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') + 'Setup'; + if (needsPrompts) { + // Get IDE handler and collect configuration + try { + // Dynamically load the IDE setup module + const ideModule = require(`../ide/${ide}`); - if (ideModule[className]) { - SetupClass = ideModule[className]; - } else if (ideModule.default) { - SetupClass = ideModule.default; - } else { - // Skip if no setup class found - continue; + // Get the setup class (handle different export formats) + let SetupClass; + const className = + ide + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .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 { @@ -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); diff --git a/tools/cli/installers/lib/core/manifest.js b/tools/cli/installers/lib/core/manifest.js index 610b428c..7410450f 100644 --- a/tools/cli/installers/lib/core/manifest.js +++ b/tools/cli/installers/lib/core/manifest.js @@ -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; } /**