Skip to content

fix: add missing sources/selectedFiles to CLI --custom-content config#1624

Closed
jschulte wants to merge 4 commits intobmad-code-org:mainfrom
jschulte:fix/cli-custom-content-config
Closed

fix: add missing sources/selectedFiles to CLI --custom-content config#1624
jschulte wants to merge 4 commits intobmad-code-org:mainfrom
jschulte:fix/cli-custom-content-config

Conversation

@jschulte
Copy link

What

Fixes the non-interactive --custom-content CLI flag so custom modules install correctly. Both CLI code paths (modify and fresh install) now produce the same config shape as the interactive promptCustomContentSource() method.

Why

The --custom-content flag builds a customContentConfig missing sources, selected, and selectedFiles properties. The installer expects these (set by the interactive path), so findModuleSource() never finds the custom module and throws "Source for module X is not available".

Fixes #1623

How

  • Added sources, selected, and selectedFiles to the config object in both the "modify existing install" flow (~line 338) and "fresh install" flow (~line 481) in tools/cli/lib/ui.js
  • The shape now matches what promptCustomContentSource() produces

Testing

  • All existing tests pass (npm test — schemas, refs, install component tests, lint, markdownlint, prettier)
  • Manually verified: npx bmad-method install --directory . --modules bmm,pantheon --custom-content ~/git/pantheon/src --tools claude-code --user-name Test --yes now installs the custom module successfully

The non-interactive --custom-content CLI flag builds a customContentConfig
object missing the `sources`, `selected`, and `selectedFiles` properties
that the installer expects. This causes findModuleSource() to fail with
"Source for module X is not available" because customModulePaths is never
populated.

Align both CLI code paths (modify and fresh install flows) with the config
shape produced by the interactive promptCustomContentSource() method.

Fixes bmad-code-org#1623
Copilot AI review requested due to automatic review settings February 11, 2026 03:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the non-interactive --custom-content CLI flow so the installer receives the expected customContent config shape and can resolve local custom module sources (matching the interactive promptCustomContentSource() contract).

Changes:

  • Add selected: true when --custom-content is provided and valid paths are found.
  • Populate sources and selectedFiles in both “modify existing install” and “fresh install” CLI code paths.
  • Derive selectedModuleIds from each custom module’s module.yaml code field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 484 to 490
const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
customContentConfig = {
hasCustomContent: true,
selected: true,
paths: customPaths,
sources: sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as earlier block: sources currently sets name to the module id, but the interactive flow uses module.yaml's name when present. Consider populating sources during the module.yaml parse loop (including name) to fully match promptCustomContentSource() output and avoid depending on positional indexing.

Copilot uses AI. Check for mistakes.
Comment on lines 339 to 347
const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
customModuleResult = {
selectedCustomModules: selectedModuleIds,
customContentConfig: {
hasCustomContent: true,
selected: true,
paths: customPaths,
sources: sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sources is built with name set to the module id, but the interactive promptCustomContentSource() uses module.yaml's name (fallback to code). Since you already parse module.yaml earlier in this block, consider building sources during that loop so name matches the interactive config shape and avoids relying on index alignment between customPaths and selectedModuleIds.

Copilot uses AI. Check for mistakes.
Comment on lines 338 to 349
if (customPaths.length > 0) {
const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
customModuleResult = {
selectedCustomModules: selectedModuleIds,
customContentConfig: {
hasCustomContent: true,
selected: true,
paths: customPaths,
sources: sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds,
},
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR changes the non-interactive --custom-content behavior in both modify and fresh-install flows, but there doesn’t appear to be automated coverage for the flag (no tests reference custom-content). Consider adding an integration/unit test that runs an install with --custom-content and asserts the generated config includes selected: true, sources, and selectedFiles so this regression doesn’t reappear.

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

The change augments the customContentConfig object returned by CLI argument handling to include additional metadata fields (sources, selected, selectedFiles) that the installer expects, ensuring the custom content installation flow works correctly with non-interactive CLI usage.

Changes

Cohort / File(s) Summary
Custom Content Configuration Augmentation
tools/cli/lib/ui.js
Added sources array mapping custom paths to module ids/names, selected: true flag, and selectedFiles array containing module.yaml paths to customContentConfig object across both custom content handling code paths.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

Suggested reviewers

  • bmadcode
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding missing sources/selectedFiles properties to the CLI --custom-content config to fix the non-interactive flag.
Description check ✅ Passed The description is clearly related to the changeset, explaining what was fixed, why it was needed, how it was implemented, and providing testing evidence.
Linked Issues check ✅ Passed The PR successfully addresses issue #1623 by adding the missing sources, selected, and selectedFiles properties to customContentConfig in both CLI code paths to align with the interactive config shape.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the --custom-content CLI flag issue by augmenting the config object shape in the two affected code paths in tools/cli/lib/ui.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tools/cli/lib/ui.js (3)

328-332: ⚠️ Potential issue | 🟡 Minor

moduleMeta.code access after yaml.parse can throw if YAML is empty.

yaml.parse('') returns null. The try-catch on lines 319–327 (and 464–472) only guards readFile and yaml.parse — but line 329 (and 474) accesses moduleMeta.code outside that catch. If the file is empty or contains only comments, moduleMeta is null and !moduleMeta.code throws an uncaught TypeError.

Proposed fix (apply to both occurrences)
+           if (!moduleMeta) {
+             await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
+             continue;
+           }
            if (!moduleMeta.code) {

Also applies to: 473-477


306-336: ⚠️ Potential issue | 🟡 Minor

No deduplication of custom content paths.

If a user passes the same path twice via --custom-content /foo,/foo, both iterations will pass validation, and the same module will appear twice in customPaths, selectedModuleIds, sources, and selectedFiles. The interactive flow avoids this by design (one-at-a-time with a "continue" prompt). Consider deduplicating on expandedPath or moduleMeta.code.

Also applies to: 451-481


352-367: ⚠️ Potential issue | 🔴 Critical

--yes path for modify flow preserves existing custom modules but doesn't populate the installer's custom module metadata.

When options.yes is truthy and options.customContent is falsy (lines 352–367), customModuleResult.customContentConfig remains { hasCustomContent: false } even though custom module names are preserved in selectedCustomModules. The installer (line 306–340) builds its customModulePaths map only from customContent.selectedFiles, customContent.sources, or customContent.cachedModules—none of which are populated by this code path. As a result, when moduleManager.findModuleSource() is called for cached modules during installation (line 1270), it returns null because the customModulePaths map is empty, causing cached modules to silently skip installation. This mirrors the bug the PR aims to address.

🤖 Fix all issues with AI agents
In `@tools/cli/lib/ui.js`:
- Line 339: The CLI branches that build the sources array (where const sources =
customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name:
selectedModuleIds[i] }))) are setting name to the module code only; update both
CLI mappings to capture the module metadata (moduleMeta) alongside
selectedModuleIds and set name: moduleMeta.name || moduleMeta.code (matching
promptCustomContentSource()). Ensure you retrieve moduleMeta for each
selectedModuleIds[i] (e.g., from the same module lookup used elsewhere) and use
it when constructing the objects in both places where sources is created.
- Around line 338-350: Remove the unused "paths" property from the
customContentConfig object construction so the CLI branch produces the same
shape as promptCustomContentSource(); specifically, in the code that builds
customModuleResult (the customContentConfig object created alongside
selectedCustomModules), drop paths: customPaths and rely on sources (objects
with path, id, name) and selectedFiles for path information; do the same removal
in the other CLI branch that sets customContentConfig to ensure both branches
match promptCustomContentSource()'s shape.
🧹 Nitpick comments (3)
tools/cli/lib/ui.js (3)

297-351: Near-identical ~50-line blocks duplicated across modify and fresh-install paths.

The custom-content-from-CLI logic (path validation loop, metadata reading, sources/selectedFiles construction) is copy-pasted between the modify flow (lines 297–351) and the fresh-install flow (lines 442–492). Any future fix (e.g., the name bug above) must be applied in both places, and they will inevitably drift.

Consider extracting a shared helper, e.g. buildCustomContentConfigFromPaths(paths), that both code paths call.

Also applies to: 442-492


338-350: Modify flow builds sources via index-correlated parallel arrays.

customPaths and selectedModuleIds are two parallel arrays kept in sync by push order inside the same loop. This works today, but it's a fragile pattern — any refactor that reorders or filters one array without the other silently corrupts the mapping. The fresh-install path (line 484) has the same pattern. Using a single array of objects (as suggested in the name fix above) eliminates this class of bug.


309-336: require('yaml') called inside the loop on every iteration.

Lines 322 and 467 do const yaml = require('yaml') inside for loops. Node caches require calls, so there's no correctness issue, but it's unconventional — the require should be hoisted to the top of the function (or the file) for clarity. This is pre-existing, not introduced by this PR, so low priority.

Also applies to: 454-481

Comment on lines 338 to 350
if (customPaths.length > 0) {
const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
customModuleResult = {
selectedCustomModules: selectedModuleIds,
customContentConfig: {
hasCustomContent: true,
selected: true,
paths: customPaths,
sources: sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds,
},
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check file size and read the relevant sections
wc -l tools/cli/lib/ui.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 93


🏁 Script executed:

# Read the specific lines mentioned in the review comment
echo "=== Lines 338-350 ==="
sed -n '338,350p' tools/cli/lib/ui.js

echo "=== Lines 483-492 ==="
sed -n '483,492p' tools/cli/lib/ui.js

echo "=== Lines 1452-1527 (promptCustomContentSource) ==="
sed -n '1452,1527p' tools/cli/lib/ui.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 3928


🏁 Script executed:

# Search for where customContentConfig is consumed
rg -n 'customContentConfig' tools/cli/lib/ui.js -A 3 -B 3

Repository: bmad-code-org/BMAD-METHOD

Length of output: 6029


🏁 Script executed:

# Search for paths property usage in customContentConfig context
rg -n '\.paths' tools/cli/lib/ui.js -C 2

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Search more broadly for how customContentConfig is used
rg -n 'customContent\.' tools/cli/lib/ui.js -C 2

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Search for where the install or processing happens with customContent
rg -n 'customContent:' tools/cli/lib/ui.js -B 5 -A 5

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1612


🏁 Script executed:

# Look for files that might consume customContentConfig outside ui.js
fd --type=f -name '*.js' | xargs grep -l 'customContent' | head -20

Repository: bmad-code-org/BMAD-METHOD

Length of output: 299


🏁 Script executed:

# Check what properties are actually accessed on customContentConfig
rg -n 'customContentConfig\.' tools/cli/lib/ui.js -C 1 | head -100

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1843


🏁 Script executed:

# Find and check files that consume customContent to see if they use paths
find tools -name '*.js' -type f | xargs grep -l 'customContent' 2>/dev/null

Repository: bmad-code-org/BMAD-METHOD

Length of output: 175


🏁 Script executed:

# Check the installer or main entry point
rg -n '\.paths' tools/ --type=js -C 2 | head -50

Repository: bmad-code-org/BMAD-METHOD

Length of output: 745


🏁 Script executed:

# Look at what the installer does with customContent
fd --type=f '*.js' tools | xargs grep -l 'install\|installer' | head -5

Repository: bmad-code-org/BMAD-METHOD

Length of output: 352


🏁 Script executed:

# Check imports in ui.js to understand the flow
head -50 tools/cli/lib/ui.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1843


🏁 Script executed:

# Check the installer.js to see how customContent is consumed
sed -n '1,100p' tools/cli/installers/lib/core/installer.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 4488


🏁 Script executed:

# Search for where customContent is used in installer.js
rg -n 'customContent' tools/cli/installers/lib/core/installer.js -C 3 | head -80

Repository: bmad-code-org/BMAD-METHOD

Length of output: 2159


🏁 Script executed:

# Check modules/manager.js for customContent usage
rg -n 'customContent\|paths' tools/cli/installers/lib/modules/manager.js -C 2 | head -60

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Search more broadly for where the paths property from customContentConfig would be used
rg -n 'customPaths\|paths\[' tools/cli/lib/ui.js -C 2

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Check if paths property is ever accessed anywhere in the codebase
rg -n '\bpaths\b' tools/cli/installers/lib/core/installer.js -C 2 | grep -A 3 -B 3 'customContent\|custom'

Repository: bmad-code-org/BMAD-METHOD

Length of output: 895


🏁 Script executed:

# Search for any usage of the paths property at all in custom content context
rg -n 'customContent.*paths\|paths.*customContent' tools/ --type=js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Let's check if sources property is the replacement for paths
rg -n '\.sources' tools/cli/installers/lib/modules/manager.js -C 2

Repository: bmad-code-org/BMAD-METHOD

Length of output: 51


🏁 Script executed:

# Check the full context around custom content handling in installer
sed -n '303,340p' tools/cli/installers/lib/core/installer.js

Repository: bmad-code-org/BMAD-METHOD

Length of output: 1987


Remove the unused paths property from customContentConfig in both CLI branches.

Lines 345 and 488 set paths: customPaths on customContentConfig, but promptCustomContentSource() never sets this property, and the installer never consumes it. All necessary path information is already provided via sources (containing source objects with path, id, name) and selectedFiles. Remove paths to eliminate shape divergence between CLI and interactive flows.

🤖 Prompt for AI Agents
In `@tools/cli/lib/ui.js` around lines 338 - 350, Remove the unused "paths"
property from the customContentConfig object construction so the CLI branch
produces the same shape as promptCustomContentSource(); specifically, in the
code that builds customModuleResult (the customContentConfig object created
alongside selectedCustomModules), drop paths: customPaths and rely on sources
(objects with path, id, name) and selectedFiles for path information; do the
same removal in the other CLI branch that sets customContentConfig to ensure
both branches match promptCustomContentSource()'s shape.

}

if (customPaths.length > 0) {
const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
Copy link

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

name in sources always equals code, diverging from the interactive flow.

In promptCustomContentSource() (line 1506), the interactive path sets name: moduleData.name || moduleData.code. Here, both CLI paths set name: selectedModuleIds[i], which is always moduleMeta.code. If a module's module.yaml has a human-readable name field (e.g., "Pantheon Custom Module"), it's silently discarded and the terse code is used instead.

This means downstream consumers that display or key on sources[].name will behave differently depending on whether the user went through the interactive or CLI path.

Proposed fix (apply to both line 339 and line 484)

You'd need to capture moduleMeta.name alongside moduleMeta.code in the loop. For example, collect objects instead of bare IDs:

  const customPaths = [];
- const selectedModuleIds = [];
+ const selectedModuleEntries = []; // { id, name }

  for (const customPath of paths) {
    ...
    customPaths.push(expandedPath);
-   selectedModuleIds.push(moduleMeta.code);
+   selectedModuleEntries.push({ id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
  }

  if (customPaths.length > 0) {
-   const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleIds[i], name: selectedModuleIds[i] }));
+   const selectedModuleIds = selectedModuleEntries.map(e => e.id);
+   const sources = customPaths.map((p, i) => ({ path: p, id: selectedModuleEntries[i].id, name: selectedModuleEntries[i].name }));
    ...
  }

Also applies to: 484-484

🤖 Prompt for AI Agents
In `@tools/cli/lib/ui.js` at line 339, The CLI branches that build the sources
array (where const sources = customPaths.map((p, i) => ({ path: p, id:
selectedModuleIds[i], name: selectedModuleIds[i] }))) are setting name to the
module code only; update both CLI mappings to capture the module metadata
(moduleMeta) alongside selectedModuleIds and set name: moduleMeta.name ||
moduleMeta.code (matching promptCustomContentSource()). Ensure you retrieve
moduleMeta for each selectedModuleIds[i] (e.g., from the same module lookup used
elsewhere) and use it when constructing the objects in both places where sources
is created.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is valid. Can you change this @jschulte ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dracic Should be updated now!

jschulte and others added 2 commits February 11, 2026 10:17
- Use module display name in sources (name: moduleMeta.name || code)
- Add null guard for empty module.yaml (yaml.parse returns null)
- Remove unused paths property to match promptCustomContentSource shape
cidemaxio added a commit to maxio-com/BMAD-METHOD that referenced this pull request Feb 15, 2026
…warn) and removed paths from the config to match promptCustomContentSource()
bmadcode pushed a commit that referenced this pull request Feb 15, 2026
* fix custom install bug

* fix manager.js

* From PR #1624: added empty module.yaml handling (skip + warn) and removed paths from the config to match promptCustomContentSource()

* fix: custom-content quick-update ENOENT, pass --custom-content through, add PR#1624 improvements to allow update installs to work using non-interactive mode
@bmadcode
Copy link
Collaborator

This may have been resolved with the other just merged PR / this PR is still having conflicts. Going to close as it seems like a duplicate - but please reopen or let me know in discord if I am mistaken in closing this one or if a problem still exists @jschulte @dracic - thanks guys!

@bmadcode bmadcode closed this Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] --custom-content CLI flag fails: missing sources/selectedFiles in config

3 participants