diff --git a/tools/cli/README.md b/tools/cli/README.md index 6d698580f..8ba164d36 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -5,3 +5,56 @@ For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml. For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org. + +## Post-Install Notes + +Modules can display setup guidance to users after configuration is collected during `npx bmad-method install`. Notes are defined in the module's own `module.yaml` — no changes to the installer are needed. + +### Simple Format + +Always displayed after the module is configured: + +```yaml +post-install-notes: | + Remember to set the API_KEY environment variable. + See: https://example.com/setup +``` + +### Conditional Format + +Display different messages based on a config question's answer: + +```yaml +post-install-notes: + config_key_name: + value1: | + Instructions for value1... + value2: | + Instructions for value2... +``` + +Values without an entry (e.g., `none`) display nothing. Multiple config keys can each have their own conditional notes. + +### Example: TEA Module + +The TEA module uses the conditional format keyed on `tea_browser_automation`: + +```yaml +post-install-notes: + tea_browser_automation: + cli: | + Playwright CLI Setup: + npm install -g @playwright/cli@latest + playwright-cli install --skills + mcp: | + Playwright MCP Setup (two servers): + 1. playwright — npx @playwright/mcp@latest + 2. playwright-test — npx playwright run-test-mcp-server + auto: | + Playwright CLI Setup: + ... + Playwright MCP Setup (two servers): + ... +``` + +When a user selects `auto`, they see both CLI and MCP instructions. When they select `none`, nothing is shown. diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index 44d3805d7..b01098318 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -550,6 +550,8 @@ class ConfigCollector { } } + await this.displayModulePostConfigNotes(moduleName, moduleConfig); + return newKeys.length > 0 || newStaticKeys.length > 0; // Return true if we had any new fields (interactive or static) } @@ -923,6 +925,8 @@ class ConfigCollector { } } } + + await this.displayModulePostConfigNotes(moduleName, moduleConfig); } /** @@ -1195,6 +1199,59 @@ class ConfigCollector { return question; } + /** + * Display post-configuration notes for a module + * Shows prerequisite guidance based on collected config values + * Reads notes from the module's `post-install-notes` section in module.yaml + * Supports two formats: + * - Simple string: always displayed + * - Object keyed by config field name, with value-specific messages + * @param {string} moduleName - Module name + * @param {Object} moduleConfig - Parsed module.yaml content + */ + async displayModulePostConfigNotes(moduleName, moduleConfig) { + if (this._silentConfig) return; + if (!moduleConfig || !moduleConfig['post-install-notes']) return; + + const notes = moduleConfig['post-install-notes']; + const color = await prompts.getColor(); + + // Format 1: Simple string - always display + if (typeof notes === 'string') { + await prompts.log.message(''); + for (const line of notes.trim().split('\n')) { + await prompts.log.message(color.dim(line)); + } + return; + } + + // Format 2: Conditional on config values + if (typeof notes === 'object') { + const config = this.collectedConfig[moduleName]; + if (!config) return; + + let hasOutput = false; + for (const [configKey, valueMessages] of Object.entries(notes)) { + const selectedValue = config[configKey]; + if (!selectedValue || !valueMessages[selectedValue]) continue; + + if (hasOutput) await prompts.log.message(''); + hasOutput = true; + + const message = valueMessages[selectedValue]; + await prompts.log.message(''); + for (const line of message.trim().split('\n')) { + const trimmedLine = line.trim(); + if (trimmedLine.endsWith(':') && !trimmedLine.startsWith(' ')) { + await prompts.log.info(color.bold(trimmedLine)); + } else { + await prompts.log.message(color.dim(' ' + trimmedLine)); + } + } + } + } + } + /** * Deep merge two objects * @param {Object} target - Target object