Skip to content

Fix: --custom-content flag and workflow config.yaml copying#1651

Merged
bmadcode merged 7 commits intobmad-code-org:mainfrom
maxio-com:main
Feb 15, 2026
Merged

Fix: --custom-content flag and workflow config.yaml copying#1651
bmadcode merged 7 commits intobmad-code-org:mainfrom
maxio-com:main

Conversation

@cidemaxio
Copy link
Copy Markdown
Contributor

@cidemaxio cidemaxio commented Feb 14, 2026

What
When using --custom-content, the installer now populates sources, selected, and selectedFiles in the custom content config so the installer can resolve custom module sources.
Why
The installer expects config.customContent.sources to build customModulePaths, but the --custom-content path only set paths and selectedModuleIds. That left customModulePaths empty and caused findModuleSource() to fail with "Source for module 'X' is not available."
Fixes #1623
How
Added a sources array with { path, id, name } for each custom module when building config from --custom-content
Set selected: true and selectedFiles (paths to each module.yaml) on the custom content config
Applied the same logic in both the modify flow and the new-install flow in tools/cli/lib/ui.js
Incorporated fixes from PR #1624: empty module.yaml handling (skip + warn) and removal of paths from the config to match promptCustomContentSource()
Fixed quick-update with --custom-content: pass through for re-caching, add defensive pathExists checks for cache entries, and fix workflow-level config.yaml copying in manager.js
Testing
Ran npx bmad-method install --directory . --modules bmm,bmb --custom-content /path/to/bmad-story-orchestration --tools claude-code,cursor --yes and confirmed the install completes and custom modules are installed. Re-running the same command (quick-update) also succeeds.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Adds a sources array to the non-interactive custom content configuration paths in the CLI, populating it with objects containing path, id, and name for each custom content module. This data structure is now included in the config passed to the installer, aligning with the expected configuration shape.

Changes

Cohort / File(s) Summary
Custom Content Sources Array
tools/cli/lib/ui.js
Introduces in-memory sources collection in both the "prompt for custom content" and "install/update with custom content" flows, populating it with module metadata (path, id, name) and propagating it into the customContentConfig object.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions '--custom-content flag and workflow config.yaml copying' but the actual changes focus on fixing missing sources array population in custom content configuration, not on config.yaml copying. Update the title to accurately reflect the main change, such as 'Fix: Populate sources array for custom content configuration' or 'Fix: custom-content CLI flag missing sources metadata'.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #1623 by populating the sources array with {path, id, name}, setting selected: true, and including selectedFiles in both flows.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the --custom-content configuration issue in tools/cli/lib/ui.js with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Description check ✅ Passed The pull request description is directly related to the changeset and clearly explains the what, why, and how of the changes.

✏️ 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

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
Copy Markdown

@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.

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)

330-342: ⚠️ Potential issue | 🟡 Minor

No guard against duplicate module codes across custom content paths.

If a user passes --custom-content pathA,pathB and both contain a module.yaml with the same code value, the module ID is pushed twice into selectedModuleIds and sources. This could cause duplicate installation attempts or silent conflicts in the installer. The interactive flow (promptCustomContentSource) has no such guard either, but the CLI path is more susceptible since users may pass overlapping directories.

Consider deduplicating by moduleMeta.code:

🛡️ Proposed guard
+        if (selectedModuleIds.includes(moduleMeta.code)) {
+          await prompts.log.warn(`Skipping duplicate custom module: ${moduleMeta.code} from ${customPath}`);
+          continue;
+        }
+
         customPaths.push(expandedPath);
         selectedModuleIds.push(moduleMeta.code);

318-333: ⚠️ Potential issue | 🟡 Minor

yaml.parse() can return null for empty YAML, causing an uncaught TypeError on line 330.

If module.yaml is empty or contains only ---, yaml.parse() returns null. The subsequent moduleMeta.code access (line 330) sits outside the try/catch block and will throw TypeError: Cannot read properties of null. This is a pre-existing gap but lives squarely in the code path you're touching.

🛡️ Proposed guard
           moduleMeta = yaml.parse(moduleYaml);
         } catch (error) {
           await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
           continue;
         }

-        if (!moduleMeta.code) {
+        if (!moduleMeta || !moduleMeta.code) {
           await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
           continue;
         }

Apply the same fix in the new-install block (line 480).


344-356: ⚠️ Potential issue | 🟡 Minor

Remove the redundant paths field from the custom content config.

The paths field included at lines 345-356 is never referenced anywhere in the codebase. The installer only consumes sources, selectedFiles, selected, and hasCustomContent — all already present in the config. Additionally, the interactive flow (promptCustomContentSource) does not include a paths field, creating shape inconsistency between the CLI and interactive code paths. Since selectedFiles already contains the full file paths to each module.yaml, the paths field is redundant and should be removed.

🧹 Nitpick comments (2)
tools/cli/lib/ui.js (2)

297-356: Massive code duplication between modify-flow and new-install flow.

Lines 297–356 (modify/update path) and lines 447–503 (new-install path) are near-identical copy-paste blocks for building customContentConfig from options.customContent. This violates DRY and means any future bug fix or shape change must be applied in two places — exactly the kind of drift that caused this bug in the first place.

Extract a shared helper, e.g. buildCustomContentConfigFromPaths(paths), that returns { customPaths, selectedModuleIds, sources, customContentConfig }.

♻️ Sketch of extracted helper
// Add as a method on the UI class:
async _buildCustomContentConfig(rawPaths) {
  const customPaths = [];
  const selectedModuleIds = [];
  const sources = [];

  for (const customPath of rawPaths) {
    const expandedPath = this.expandUserPath(customPath);
    const validation = this.validateCustomContentPathSync(expandedPath);
    if (validation) {
      await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
      continue;
    }

    let moduleMeta;
    try {
      const moduleYamlPath = path.join(expandedPath, 'module.yaml');
      const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
      const yaml = require('yaml');
      moduleMeta = yaml.parse(moduleYaml);
    } catch (error) {
      await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
      continue;
    }

    if (!moduleMeta.code) {
      await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
      continue;
    }

    customPaths.push(expandedPath);
    selectedModuleIds.push(moduleMeta.code);
    sources.push({
      path: expandedPath,
      id: moduleMeta.code,
      name: moduleMeta.name || moduleMeta.code,
    });
  }

  if (customPaths.length === 0) {
    return { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
  }

  return {
    selectedCustomModules: selectedModuleIds,
    customContentConfig: {
      hasCustomContent: true,
      paths: customPaths,
      selectedModuleIds,
      sources,
      selected: true,
      selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
    },
  };
}

Then both call sites collapse to:

const result = await this._buildCustomContentConfig(paths);
// use result.customContentConfig / result.selectedCustomModules

Also applies to: 447-503


337-353: No automated test coverage for the CLI custom content config shape.

This bug existed because the non-interactive config shape diverged from the interactive one, and no test caught it. The fix is correct, but without a test asserting the expected config shape from --custom-content, this can silently regress again. Consider adding a unit test that exercises the options.customContent branch and asserts the presence of sources, selected, and selectedFiles in the returned config.

@dracic
Copy link
Copy Markdown
Contributor

dracic commented Feb 14, 2026

Is this duplicate of #1624 ?

@cidemaxio
Copy link
Copy Markdown
Contributor Author

quite possibly a duplicate.

@dracic
Copy link
Copy Markdown
Contributor

dracic commented Feb 14, 2026

Better one PR too many than one too few.

@cidemaxio cidemaxio changed the title Fix: --custom-content CLI flag fails with "Source for module not available" Fix: --custom-content flag and workflow config.yaml copying Feb 15, 2026
…warn) and removed paths from the config to match promptCustomContentSource()
…h, add PR#1624 improvements to allow update installs to work using non-interactive mode
@cidemaxio
Copy link
Copy Markdown
Contributor Author

I included the changes from the referenced PR. I also made fixes so that update installs work.

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