* fix(installer): route community installs through PluginResolver when marketplace.json ships
Community-catalog installs ignored .claude-plugin/marketplace.json, so modules
that nest module.yaml inside a setup skill's assets/ directory (e.g. Strategy 2
in PluginResolver) ended up half-installed: only module-help.csv and the
generated config.yaml landed in _bmad/<code>/, while the actual skill source
trees and module.yaml never got copied. The install would silently emit
"could not locate module.yaml" warnings and leave .agents/skills/ without
the module's skills.
The fix wires the existing PluginResolver onto the community path:
- CommunityModuleManager.cloneModule now detects marketplace.json after the
clone+ref-checkout completes and runs PluginResolver. The resolution is
stamped with channel/sha/registryApprovedTag/registryApprovedSha and cached
in _pluginResolutions, mirroring the existing _resolutions cache.
- OfficialModules.install consults the community plugin resolution and
delegates to installFromResolution (the same code path custom-source
installs already use). installFromResolution branches on communitySource
to write source: 'community' with the registry's approved tag/sha and
channel.
- resolveInstalledModuleYaml now searches the community-modules cache root
in addition to the external-modules cache, and the BMB setup-skill detector
walks src/skills/ and skills/ (not just the repo root) so collectAgents
FromModuleYaml and writeCentralConfig can find module.yaml in nested
marketplace-plugin layouts.
Backward compatibility: repos without marketplace.json (e.g. WDS, which
declares module_definition: src/module.yaml at the root) continue through
the legacy findModuleSource path with no behavior change. Verified against
the live zarlor/suno-band-manager community module and a 23-check fixture
suite covering Suno-shape, WDS-shape, and bare-repo layouts.
* fix(installer): harden community marketplace.json resolution path
Address review feedback on the community marketplace.json install path:
- Wrap PluginResolver.resolve() in try/catch so a malformed plugin entry
falls through to the legacy install path with a warn instead of
crashing cloneModule.
- Stop mutating the resolver's return object; shallow-clone before
stamping community provenance so install state cannot leak back into
resolver-owned objects.
- Warn when _selectPluginForModule lands on the single-plugin fallback
with a name that doesn't match the registry code or module_definition
hint, so a misconfigured marketplace.json can't silently install the
wrong plugin.
- Add CommunityModuleManager.resolveFromCache() and call it from
OfficialModules.install() when the in-process plugin cache is empty,
so callers that reach install() without pre-cloning still get the
marketplace-aware path. Reuses an existing channel resolution when
present, otherwise synthesizes a stable-channel stub from the registry
entry plus the cached repo's HEAD.
- Align installFromResolution()'s returned versionInfo.version with
manifestEntry.version precedence (communityVersion || cloneRef || ...)
so downstream summaries match what was written to the manifest.
Tests: lint, format:check, lint:md, test:install (290), test:channels
(83), test:refs (7) all green.
* fix(installer): resolve url-source custom modules from custom-modules cache
resolveInstalledModuleYaml previously only searched ~/.bmad/cache/external-modules/,
so modules installed via --custom-source <git-url> (cached at
~/.bmad/cache/custom-modules/<host>/<owner>/<repo>/) could not be located on
re-install runs. This caused warnings during npx bmad-method install:
[warn] collectAgentsFromModuleYaml: could not locate module.yaml for '<name>'
[warn] writeCentralConfig: could not locate module.yaml for '<name>'
Adds a fallback that walks the custom-modules cache via _findCacheRepoRoots
(identifying repo roots by .bmad-source.json or .claude-plugin/, not
marketplace.json, so direct-mode modules are also covered), reuses the same
searchRoot candidate-path logic, and matches by the discovered yaml's code
or name field.
Works without needing _resolutionCache to be populated, which fixes the
re-install scenario where no --custom-source flag is passed.
Closes#2312
* fix(installer): enumerate all module.yamls when walking custom-modules cache
A url-source custom-modules repo can host multiple plugins in discovery
mode (e.g. skills/module-a/module.yaml and skills/module-b/module.yaml).
The previous walk used searchRoot which returned only the first match,
so asking for module-b would surface module-a's yaml, fail the code/name
check, and skip the repo entirely — never inspecting module-b.
Splits the candidate-path traversal into searchRootAll (returns every
module.yaml in priority order) and a thin searchRoot wrapper for the
existing single-module fallbacks. The custom-modules walk now iterates
every yaml per repo and matches each against code or name.
- resolveInstalledModuleYaml: fall back to CustomModuleManager._resolutionCache for local
custom-source modules (external cache path doesn't exist for these); refactor candidate-path
search into shared searchRoot() helper; add *-setup/assets/module.yaml BMB standard path
- manifest-generator: use module code field (not display name) as TOML section key [modules.X]
Co-authored-by: cidemaxio <cidemaxio@users.noreply.github.com>
External official modules (bmb, cis, gds, tea, wds) are cloned to
~/.bmad/cache/external-modules/<name>/ and never copied into src/modules/,
so collectAgentsFromModuleYaml silently skipped them and their agents
never reached config.toml. Swap the hardcoded src/modules lookup for a
resolveInstalledModuleYaml() helper that also searches the external cache
(handling src/, skills/, nested, and root layouts) and warns instead of
silently skipping when a module.yaml can't be found.
fs-extra routes all operations through graceful-fs, which globally
monkey-patches node:fs with a deferred retry queue. During multi-module
installs (~500+ file ops), retried unlink operations from one module's
remove phase can fire after the next module's copy phase has written
files, silently deleting them non-deterministically.
Replace fs-extra with a thin fs-native.js wrapper over node:fs/promises
and node:fs. All 21 consumers now use native APIs with no global
monkey-patching, eliminating the retry-queue race condition entirely.
Closes#1779
* refactor(installer): restructure installer with clean separation of concerns
Move tools/cli/ to tools/installer/ with major structural cleanup:
- InstallPaths async factory for path resolution and directory creation
- Config value object (frozen) replaces mutable config bag
- ExistingInstall value object replaces stateful Detector class
- OfficialModules + CustomModules + ExternalModuleManager replace monolithic ModuleManager
- install() is prompt-free; all user interaction in ui.js
- Update state returned explicitly instead of mutating customConfig
- Delete dead code: dependency-resolver, _base-ide, IdeConfigManager,
platform-codes helpers, npx wrapper, xml-utils
- Flatten directory structure: custom/handler → custom-handler,
tools/cli/ → tools/installer/, lib/ directories removed
- Update all path references in package.json, tests, CI, and docs
* fix(installer): guard ExistingInstall.version and surface module.yaml errors
Guard ExistingInstall.version access with .installed check in
uninstall.js, ui.js, and installer.js to prevent throwing on
empty/partial _bmad dirs. Surface invalid module.yaml parse errors
as warnings instead of silently returning empty results.