fix(installer): stop --yes updates from silently deleting custom modules - #2707
fix(installer): stop --yes updates from silently deleting custom modules#2707eugeniawang wants to merge 2 commits into
Conversation
getDefaultModules() builds its keep-list from the built-in catalog and the external registry only, so an installed source: custom module is never a candidate on the --yes path. _retainUnavailableInstalledModules() preserves a module only when no source can be found, so a correctly-installed custom module with a cached source is read as a deselection and removed. The removal path has no success-side logging, so the module disappears with exit 0 and no output. Carry forward installed modules that appear in neither catalog, matching selectAllModules() on the interactive path, and log removals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Greptile SummaryThe PR updates non-interactive installer defaults so installed custom modules remain selected during updates and adds visibility when deselected modules are removed.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the eligible follow-up review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tools/installer/ui.js | Extends non-interactive default selection to retain uncatalogued installed modules while canonicalizing and deduplicating module codes. |
| tools/installer/core/installer.js | Adds a warning after each deselected module directory is successfully removed. |
| test/test-installer-default-modules.js | Adds offline regression tests for custom-module preservation, alias canonicalization, deduplication, and channel decisions. |
| package.json | Registers the new regression test and includes it in the main test and quality workflows. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Read installed module IDs] --> B[Load official and registry catalogs]
B --> C[Select catalog defaults and installed catalog modules]
C --> D[Resolve each remaining installed ID]
D --> E{Canonical ID already selected or core?}
E -- Yes --> F[Skip duplicate]
E -- No --> G[Carry module into update selection]
F --> H[Run update]
G --> H
H --> I[Remove only genuinely deselected modules]
I --> J[Log each removal]
Reviews (2): Last reviewed commit: "fix(installer): carry the canonical modu..." | Re-trigger Greptile
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe installer now preserves installed uncataloged modules during non-interactive updates and logs the identifier of each successfully removed deselected module. ChangesInstaller module lifecycle
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
alexeyv
left a comment
There was a problem hiding this comment.
Requesting one focused change before merge.
In getDefaultModules(), the new loop resolves an installed ID to its canonical code only for the membership checks, then appends the original ID:
const canonicalId = await externalManager.resolveCanonicalCode(moduleId);
// ...
defaultModules.push(moduleId);For the existing bauto → bmad-loop alias, this still returns bauto. That contradicts the comment and PR description, and it bypasses code that indexes registry and channel state by the canonical code. A pinned or next installation can consequently fall back to the registry default during a full --yes update.
Please carry the canonical ID forward and deduplicate selections by canonical ID. Please also add focused deterministic regression tests covering:
- A resolvable custom module remains selected during
--action update --yes. - An installed alias such as
bautoproduces the canonicalbmad-loopselection. - The canonicalized selection retains the installed module's channel decision.
The custom-module preservation change and removal logging otherwise look good.
The carry-forward loop resolved an installed id to its canonical code for the membership checks but appended the raw id, so an install recorded under a renamed code (bauto) stayed selected as bauto instead of bmad-loop. Channel state is keyed by the canonical code in buildPlan(), so a --next or --pin installation fell back to the registry default during a full --yes update. Push the canonical id and dedupe selections by it. The external-registry loop now populates `seen` as well, so a registry module that is also installed cannot be selected twice. Adds test/test-installer-default-modules.js covering custom-module survival, alias canonicalization, alias/canonical dedupe, and retention of the channel decision. Both catalogs are stubbed, so the tests are offline and independent of any BMAD install or user cache. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — you were right on both counts. Pushed as 064c609. Canonical ID carried forward. The loop now pushes for (const moduleId of installedModuleIds) {
const canonicalId = await externalManager.resolveCanonicalCode(moduleId);
if (canonicalId === 'core') continue;
if (seen.has(canonicalId)) continue;
seen.add(canonicalId);
defaultModules.push(canonicalId);
}One thing I had to change to make the dedupe actually canonical: Tests — Each test was confirmed to go red when the behaviour it guards is removed:
That third one is your channel point made concrete: pre-fix the selection is Full |
What
bmad install --action update --yesno longer deletes an installed module recorded in the manifest assource: customwhose source is still resolvable, and module removals are logged instead of happening silently.Why
What we hit. Two separate BMAD installs, in two unrelated directories, each with its own
_bmad/— not nested, no shared install:<dev>/_bmad/) —core,bmm,bmb,cis,tea,bmad-loop,wds. All built-in or from the registry.<video-project>/_bmad/) —core,bmm, andmanticore, the last installed fromgithub.com/bmad-code-org/bmad-manticoreand recordedsource: custom.One
bmad install --directory <video-project> --action update --yesagainst Project B deleted<video-project>/_bmad/manticore/outright, along with all 14 of itsmc-*skills under that project's.claude/skills/and.agents/skills/— 310 files. It removed the module's manifest entry and exited 0 with nothing in the output about any of it.coreandbmmin that same directory updated normally to6.10.1-next.55. Only the custom module was lost, and the interactive path preserves it correctly.Note the condition: a custom module whose source can no longer be found is preserved today, by the safety net below. It is the correctly-installed one, with its source still cached, that gets deleted.
getDefaultModules()(ui.js:1376-1407), used only by the--yespath, builds its keep-list from the built-in catalog and the external registry, so an installed custom module is never a candidate._retainUnavailableInstalledModules()(ui.js:151-192) is the safety net, but it preserves a module only when no source can be found:A correctly-installed custom module has a findable source in
~/.bmad/cache/custom-modules/, so it is read as a deliberate deselection and_removeDeselectedModules()(installer.js:150-166) deletes it. The only log statement in that function is inside itscatch, so a successful removal prints nothing.The interactive path already has the guard, at
ui.js:960-971:And
ui.js:345-350shows the--yespath was assumed to behave the same way:That comment assumes the
--yesbranch includes all installed modules.getDefaultModulesdoes not include custom ones.How
getDefaultModules()— carry forward installed modules present in neither catalog, matchingselectAllModules()and the assumption atui.js:345-350. Ids are normalized throughexternalManager.resolveCanonicalCode()first, the same way_retainUnavailableInstalledModules()does, so a renamed or aliased id is not carried forward unresolved._removeDeselectedModules()— log on the success path, so a module cannot disappear without a line of output.--modules, which bypassesgetDefaultModules()entirely (ui.js:338).Testing
After the fix,
manticoreand its 14 skills survive the same command andcore/bmmstill update to6.10.1-next.55. We also confirmed on the unpatched code that passing--custom-source https://github.com/bmad-code-org/bmad-manticorepreserves the module, which pinned the cause to module selection rather than to removal.Two limits on what we verified, both worth stating:
--toolsrequirement), and no module has ever been lost there. The failure appears only once asource: custommodule is present.getDefaultModules()never consulting custom sources at all, but we have not run a multi-custom-module case.