From f06f21fb798bf2554dde698423a72d11efafd89b Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 8 Apr 2026 00:29:15 -0500 Subject: [PATCH] fix: preserve installed community/custom modules in modify flow When a user does "Modify Installation" and declines to browse community modules, previously installed community/custom modules are now auto-kept. If the user does browse, their selections are trusted (they can deselect). Also fix stale docs: class doc for SHA pinning, JSDoc return type. --- tools/installer/modules/community-manager.js | 2 +- tools/installer/ui.js | 36 +++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/tools/installer/modules/community-manager.js b/tools/installer/modules/community-manager.js index ea8a9e866..0f88cffff 100644 --- a/tools/installer/modules/community-manager.js +++ b/tools/installer/modules/community-manager.js @@ -13,7 +13,7 @@ const CATEGORIES_URL = `${MARKETPLACE_BASE}/categories.yaml`; * Manages community modules from the BMad marketplace registry. * Fetches community-index.yaml and categories.yaml from GitHub. * Returns empty results when the registry is unreachable. - * Community modules are pinned to approved tags (not HEAD). + * Community modules are pinned to approved SHA when set; uses HEAD otherwise. */ class CommunityModuleManager { constructor() { diff --git a/tools/installer/ui.js b/tools/installer/ui.js index d819861a0..de8783666 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -571,14 +571,34 @@ class UI { // Phase 1: Official modules const officialSelected = await this._selectOfficialModules(installedModuleIds); + // Determine which installed modules are NOT official (community or custom). + // These must be preserved even if the user declines to browse community/custom. + const officialCodes = new Set(officialSelected); + const externalManager = new ExternalModuleManager(); + const registryModules = await externalManager.listAvailable(); + const officialRegistryCodes = new Set(registryModules.map((m) => m.code)); + const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id)); + // Phase 2: Community modules (category drill-down) - const communitySelected = await this._browseCommunityModules(installedModuleIds); + // Returns { codes, didBrowse } so we know if the user entered the flow + const communityResult = await this._browseCommunityModules(installedModuleIds); // Phase 3: Custom URL modules const customSelected = await this._addCustomUrlModules(installedModuleIds); // Merge all selections - return [...officialSelected, ...communitySelected, ...customSelected]; + const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]); + + // Auto-include installed non-official modules that the user didn't get + // a chance to manage (they declined to browse). If they did browse, + // trust their selections - they could have deselected intentionally. + if (!communityResult.didBrowse) { + for (const code of installedNonOfficial) { + allSelected.add(code); + } + } + + return [...allSelected]; } /** @@ -641,14 +661,14 @@ class UI { * Browse and select community modules using category drill-down. * Featured/promoted modules appear at the top. * @param {Set} installedModuleIds - Currently installed module IDs - * @returns {Array} Selected community module codes + * @returns {Object} { codes: string[], didBrowse: boolean } */ async _browseCommunityModules(installedModuleIds = new Set()) { const browseCommunity = await prompts.confirm({ message: 'Would you like to browse community modules?', default: false, }); - if (!browseCommunity) return []; + if (!browseCommunity) return { codes: [], didBrowse: false }; const { CommunityModuleManager } = require('./modules/community-manager'); const communityMgr = new CommunityModuleManager(); @@ -667,12 +687,12 @@ class UI { } catch (error) { s.error('Failed to load community catalog'); await prompts.log.warn(` ${error.message}`); - return []; + return { codes: [], didBrowse: false }; } if (allCommunity.length === 0) { await prompts.log.info('No community modules are currently available.'); - return []; + return { codes: [], didBrowse: false }; } const selectedCodes = new Set(); @@ -794,13 +814,13 @@ class UI { await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n')); } - return [...selectedCodes]; + return { codes: [...selectedCodes], didBrowse: true }; } /** * Prompt user to install modules from custom GitHub URLs. * @param {Set} installedModuleIds - Currently installed module IDs - * @returns {Array} Selected custom module objects with metadata + * @returns {Array} Selected custom module code strings */ async _addCustomUrlModules(installedModuleIds = new Set()) { const addCustom = await prompts.confirm({