diff --git a/src/modules/bmm/_module-installer/install-config.yaml b/src/modules/bmm/_module-installer/install-config.yaml index 5480dd52..8ec4c7c4 100644 --- a/src/modules/bmm/_module-installer/install-config.yaml +++ b/src/modules/bmm/_module-installer/install-config.yaml @@ -47,7 +47,7 @@ dev_story_location: # TEA Agent Configuration tea_use_mcp_enhancements: prompt: "Enable Playwright MCP capabilities (healing, exploratory, verification)?" - default: true + default: false result: "{value}" # kb_location: # prompt: "Where should bmad knowledge base articles be stored?" diff --git a/tools/cli/installers/lib/core/ide-config-manager.js b/tools/cli/installers/lib/core/ide-config-manager.js new file mode 100644 index 00000000..56aa8812 --- /dev/null +++ b/tools/cli/installers/lib/core/ide-config-manager.js @@ -0,0 +1,152 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('js-yaml'); + +/** + * Manages IDE configuration persistence + * Saves and loads IDE-specific configurations to/from bmad/_cfg/ides/ + */ +class IdeConfigManager { + constructor() {} + + /** + * Get path to IDE config directory + * @param {string} bmadDir - BMAD installation directory + * @returns {string} Path to IDE config directory + */ + getIdeConfigDir(bmadDir) { + return path.join(bmadDir, '_cfg', 'ides'); + } + + /** + * Get path to specific IDE config file + * @param {string} bmadDir - BMAD installation directory + * @param {string} ideName - IDE name (e.g., 'claude-code') + * @returns {string} Path to IDE config file + */ + getIdeConfigPath(bmadDir, ideName) { + return path.join(this.getIdeConfigDir(bmadDir), `${ideName}.yaml`); + } + + /** + * Save IDE configuration + * @param {string} bmadDir - BMAD installation directory + * @param {string} ideName - IDE name + * @param {Object} configuration - IDE-specific configuration object + */ + async saveIdeConfig(bmadDir, ideName, configuration) { + const configDir = this.getIdeConfigDir(bmadDir); + await fs.ensureDir(configDir); + + const configPath = this.getIdeConfigPath(bmadDir, ideName); + const now = new Date().toISOString(); + + // Check if config already exists to preserve configured_date + let configuredDate = now; + if (await fs.pathExists(configPath)) { + try { + const existing = await this.loadIdeConfig(bmadDir, ideName); + if (existing && existing.configured_date) { + configuredDate = existing.configured_date; + } + } catch { + // Ignore errors reading existing config + } + } + + const configData = { + ide: ideName, + configured_date: configuredDate, + last_updated: now, + configuration: configuration || {}, + }; + + const yamlContent = yaml.dump(configData, { + indent: 2, + lineWidth: -1, + noRefs: true, + sortKeys: false, + }); + + await fs.writeFile(configPath, yamlContent, 'utf8'); + } + + /** + * Load IDE configuration + * @param {string} bmadDir - BMAD installation directory + * @param {string} ideName - IDE name + * @returns {Object|null} IDE configuration or null if not found + */ + async loadIdeConfig(bmadDir, ideName) { + const configPath = this.getIdeConfigPath(bmadDir, ideName); + + if (!(await fs.pathExists(configPath))) { + return null; + } + + try { + const content = await fs.readFile(configPath, 'utf8'); + const config = yaml.load(content); + return config; + } catch (error) { + console.warn(`Warning: Failed to load IDE config for ${ideName}:`, error.message); + return null; + } + } + + /** + * Load all IDE configurations + * @param {string} bmadDir - BMAD installation directory + * @returns {Object} Map of IDE name to configuration + */ + async loadAllIdeConfigs(bmadDir) { + const configDir = this.getIdeConfigDir(bmadDir); + const configs = {}; + + if (!(await fs.pathExists(configDir))) { + return configs; + } + + try { + const files = await fs.readdir(configDir); + for (const file of files) { + if (file.endsWith('.yaml')) { + const ideName = file.replace('.yaml', ''); + const config = await this.loadIdeConfig(bmadDir, ideName); + if (config) { + configs[ideName] = config.configuration; + } + } + } + } catch (error) { + console.warn('Warning: Failed to load IDE configs:', error.message); + } + + return configs; + } + + /** + * Check if IDE has saved configuration + * @param {string} bmadDir - BMAD installation directory + * @param {string} ideName - IDE name + * @returns {boolean} True if configuration exists + */ + async hasIdeConfig(bmadDir, ideName) { + const configPath = this.getIdeConfigPath(bmadDir, ideName); + return await fs.pathExists(configPath); + } + + /** + * Delete IDE configuration + * @param {string} bmadDir - BMAD installation directory + * @param {string} ideName - IDE name + */ + async deleteIdeConfig(bmadDir, ideName) { + const configPath = this.getIdeConfigPath(bmadDir, ideName); + if (await fs.pathExists(configPath)) { + await fs.remove(configPath); + } + } +} + +module.exports = { IdeConfigManager }; diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index f52488dd..dc5367c2 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -16,6 +16,7 @@ const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/p const { AgentPartyGenerator } = require('../../../lib/agent-party-generator'); const { CLIUtils } = require('../../../lib/cli-utils'); const { ManifestGenerator } = require('./manifest-generator'); +const { IdeConfigManager } = require('./ide-config-manager'); class Installer { constructor() { @@ -28,6 +29,7 @@ class Installer { this.xmlHandler = new XmlHandler(); this.dependencyResolver = new DependencyResolver(); this.configCollector = new ConfigCollector(); + this.ideConfigManager = new IdeConfigManager(); this.installedFiles = []; // Track all installed files } @@ -59,9 +61,19 @@ class Installer { previouslyConfiguredIdes = existingInstall.ides || []; } + // Load saved IDE configurations for already-configured IDEs + const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); + // Collect IDE-specific configurations if any were selected const ideConfigurations = {}; + // First, add saved configs for already-configured IDEs + for (const ide of toolConfig.ides || []) { + if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) { + ideConfigurations[ide] = savedIdeConfigs[ide]; + } + } + 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)); @@ -358,11 +370,17 @@ class Installer { spinner.stop(); let toolSelection; if (config._quickUpdate) { - // Quick update already has IDEs configured, skip prompting - // Set a flag to indicate all IDEs are pre-configured + // Quick update already has IDEs configured, use saved configurations const preConfiguredIdes = {}; + const savedIdeConfigs = config._savedIdeConfigs || {}; + for (const ide of config.ides || []) { - preConfiguredIdes[ide] = { _alreadyConfigured: true }; + // Use saved config if available, otherwise mark as already configured (legacy) + if (savedIdeConfigs[ide]) { + preConfiguredIdes[ide] = savedIdeConfigs[ide]; + } else { + preConfiguredIdes[ide] = { _alreadyConfigured: true }; + } } toolSelection = { ides: config.ides || [], @@ -467,7 +485,12 @@ class Installer { // Configure IDEs and copy documentation if (!config.skipIde && config.ides && config.ides.length > 0) { - spinner.start('Configuring IDEs...'); + // Check if any IDE might need prompting (no pre-collected config) + const needsPrompting = config.ides.some((ide) => !ideConfigurations[ide]); + + if (!needsPrompting) { + spinner.start('Configuring IDEs...'); + } // Temporarily suppress console output if not verbose const originalLog = console.log; @@ -476,7 +499,16 @@ class Installer { } for (const ide of config.ides) { - spinner.text = `Configuring ${ide}...`; + // 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, { @@ -484,12 +516,26 @@ class Installer { 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; - spinner.succeed(`Configured ${config.ides.length} IDE${config.ides.length > 1 ? 's' : ''}`); + 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 spinner.start('Copying IDE documentation...'); @@ -1447,6 +1493,9 @@ class Installer { const installedModules = existingInstall.modules.map((m) => m.id); const configuredIdes = existingInstall.ides || []; + // Load saved IDE configurations + const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); + // Get available modules (what we have source for) const availableModules = await this.moduleManager.listAvailable(); const availableModuleIds = new Set(availableModules.map((m) => m.id)); @@ -1506,6 +1555,7 @@ class Installer { actionType: 'install', // Use regular install flow _quickUpdate: true, // Flag to skip certain prompts _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them + _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer }; // Call the standard install method