Compare commits

..

No commits in common. "3c1df006fe386d5df2ff13c4bc7d6da5e5de22e0" and "dde139a560d94fd7e588306a72bad5a64e66c894" have entirely different histories.

20 changed files with 274 additions and 269 deletions

View File

@ -78,15 +78,15 @@ Work through phases 1-3. **Use fresh chats for each workflow.**
### Phase 1: Analysis (Optional) ### Phase 1: Analysis (Optional)
All workflows in this phase are optional: All workflows in this phase are optional:
- **brainstorming** (`bmad-bmm-brainstorming`) — Guided ideation - **brainstorming** — Guided ideation
- **research** (`bmad-bmm-research`) — Market and technical research - **research** — Market and technical research
- **create-product-brief** (`bmad-bmm-create-product-brief`) — Recommended foundation document - **create-product-brief** — Recommended foundation document
### Phase 2: Planning (Required) ### Phase 2: Planning (Required)
**For BMad Method and Enterprise tracks:** **For BMad Method and Enterprise tracks:**
1. Load the **PM agent** in a new chat 1. Load the **PM agent** in a new chat
2. Run the `create-prd` workflow (`bmad-bmm-create-prd`) 2. Run the `prd` workflow
3. Output: `PRD.md` 3. Output: `PRD.md`
**For Quick Flow track:** **For Quick Flow track:**
@ -100,7 +100,7 @@ If your project has a user interface, load the **UX-Designer agent** and run the
**Create Architecture** **Create Architecture**
1. Load the **Architect agent** in a new chat 1. Load the **Architect agent** in a new chat
2. Run `create-architecture` (`bmad-bmm-create-architecture`) 2. Run `create-architecture`
3. Output: Architecture document with technical decisions 3. Output: Architecture document with technical decisions
**Create Epics and Stories** **Create Epics and Stories**
@ -110,12 +110,12 @@ Epics and stories are now created *after* architecture. This produces better qua
::: :::
1. Load the **PM agent** in a new chat 1. Load the **PM agent** in a new chat
2. Run `create-epics-and-stories` (`bmad-bmm-create-epics-and-stories`) 2. Run `create-epics-and-stories`
3. The workflow uses both PRD and Architecture to create technically-informed stories 3. The workflow uses both PRD and Architecture to create technically-informed stories
**Implementation Readiness Check** *(Highly Recommended)* **Implementation Readiness Check** *(Highly Recommended)*
1. Load the **Architect agent** in a new chat 1. Load the **Architect agent** in a new chat
2. Run `check-implementation-readiness` (`bmad-bmm-check-implementation-readiness`) 2. Run `check-implementation-readiness`
3. Validates cohesion across all planning documents 3. Validates cohesion across all planning documents
## Step 2: Build Your Project ## Step 2: Build Your Project
@ -124,7 +124,7 @@ Once planning is complete, move to implementation. **Each workflow should run in
### Initialize Sprint Planning ### Initialize Sprint Planning
Load the **SM agent** and run `sprint-planning` (`bmad-bmm-sprint-planning`). This creates `sprint-status.yaml` to track all epics and stories. Load the **SM agent** and run `sprint-planning`. This creates `sprint-status.yaml` to track all epics and stories.
### The Build Cycle ### The Build Cycle
@ -132,11 +132,11 @@ For each story, repeat this cycle with fresh chats:
| Step | Agent | Workflow | Purpose | | Step | Agent | Workflow | Purpose |
| ---- | ----- | -------------- | ---------------------------------- | | ---- | ----- | -------------- | ---------------------------------- |
| 1 | SM | `create-story` (`bmad-bmm-create-story`) | Create story file from epic | | 1 | SM | `create-story` | Create story file from epic |
| 2 | DEV | `dev-story` (`bmad-bmm-dev-story`) | Implement the story | | 2 | DEV | `dev-story` | Implement the story |
| 3 | DEV | `code-review` (`bmad-bmm-code-review`) | Quality validation *(recommended)* | | 3 | DEV | `code-review` | Quality validation *(recommended)* |
After completing all stories in an epic, load the **SM agent** and run `retrospective` (`bmad-bmm-retrospective`). After completing all stories in an epic, load the **SM agent** and run `retrospective`.
## What You've Accomplished ## What You've Accomplished
@ -162,17 +162,17 @@ your-project/
## Quick Reference ## Quick Reference
| Workflow | Slash Command | Agent | Purpose | | Workflow | Agent | Purpose |
| -------------------------------- | ------------------------------------- | --------- | ------------------------------------ | | -------------------------------- | --------- | ------------------------------------ |
| `help` | `bmad-help` | Any | Get guidance on what to do next | | `help` | Any | Get guidance on what to do next |
| `create-prd` | `bmad-bmm-create-prd` | PM | Create Product Requirements Document | | `prd` | PM | Create Product Requirements Document |
| `create-architecture` | `bmad-bmm-create-architecture` | Architect | Create architecture document | | `create-architecture` | Architect | Create architecture document |
| `create-epics-and-stories` | `bmad-bmm-create-epics-and-stories` | PM | Break down PRD into epics | | `create-epics-and-stories` | PM | Break down PRD into epics |
| `check-implementation-readiness` | `bmad-bmm-check-implementation-readiness` | Architect | Validate planning cohesion | | `check-implementation-readiness` | Architect | Validate planning cohesion |
| `sprint-planning` | `bmad-bmm-sprint-planning` | SM | Initialize sprint tracking | | `sprint-planning` | SM | Initialize sprint tracking |
| `create-story` | `bmad-bmm-create-story` | SM | Create a story file | | `create-story` | SM | Create a story file |
| `dev-story` | `bmad-bmm-dev-story` | DEV | Implement a story | | `dev-story` | DEV | Implement a story |
| `code-review` | `bmad-bmm-code-review` | DEV | Review implemented code | | `code-review` | DEV | Review implemented code |
## Common Questions ## Common Questions
@ -183,7 +183,7 @@ Only for BMad Method and Enterprise tracks. Quick Flow skips from tech-spec to i
Yes. The SM agent has a `correct-course` workflow for handling scope changes. Yes. The SM agent has a `correct-course` workflow for handling scope changes.
**What if I want to brainstorm first?** **What if I want to brainstorm first?**
Load the Analyst agent and run `brainstorming` (`bmad-bmm-brainstorming`) before starting your PRD. Load the Analyst agent and run `brainstorming` before starting your PRD.
**Do I need to follow a strict order?** **Do I need to follow a strict order?**
Not strictly. Once you learn the flow, you can run workflows directly using the Quick Reference above. Not strictly. Once you learn the flow, you can run workflows directly using the Quick Reference above.
@ -192,7 +192,7 @@ Not strictly. Once you learn the flow, you can run workflows directly using the
- **During workflows** — Agents guide you with questions and explanations - **During workflows** — Agents guide you with questions and explanations
- **Community** — [Discord](https://discord.gg/gk8jAdXWmj) (#bmad-method-help, #report-bugs-and-issues) - **Community** — [Discord](https://discord.gg/gk8jAdXWmj) (#bmad-method-help, #report-bugs-and-issues)
- **Stuck?** — Run `help` (`bmad-help` on most platforms) to see what to do next - **Stuck?** — Run `help` to see what to do next
## Key Takeaways ## Key Takeaways

View File

@ -59,7 +59,7 @@ input_file_patterns:
<check if="{{story_path}} is provided by user or user provided the epic and story number such as 2-4 or 1.6 or epic 1 story 5"> <check if="{{story_path}} is provided by user or user provided the epic and story number such as 2-4 or 1.6 or epic 1 story 5">
<action>Parse user-provided story path: extract epic_num, story_num, story_title from format like "1-2-user-auth"</action> <action>Parse user-provided story path: extract epic_num, story_num, story_title from format like "1-2-user-auth"</action>
<action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action> <action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2</action> <action>GOTO step 2a</action>
</check> </check>
<action>Check if {{sprint_status}} file exists for auto discover</action> <action>Check if {{sprint_status}} file exists for auto discover</action>
@ -85,12 +85,12 @@ input_file_patterns:
<check if="user provides epic-story number"> <check if="user provides epic-story number">
<action>Parse user input: extract epic_num, story_num, story_title</action> <action>Parse user input: extract epic_num, story_num, story_title</action>
<action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action> <action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2</action> <action>GOTO step 2a</action>
</check> </check>
<check if="user provides story docs path"> <check if="user provides story docs path">
<action>Use user-provided path for story documents</action> <action>Use user-provided path for story documents</action>
<action>GOTO step 2</action> <action>GOTO step 2a</action>
</check> </check>
</check> </check>
@ -152,7 +152,7 @@ input_file_patterns:
<output>📊 Epic {{epic_num}} status updated to in-progress</output> <output>📊 Epic {{epic_num}} status updated to in-progress</output>
</check> </check>
<action>GOTO step 2</action> <action>GOTO step 2a</action>
</check> </check>
</step> </step>
@ -174,17 +174,10 @@ input_file_patterns:
(As a, I want, so that) - Detailed acceptance criteria (already BDD formatted) - Technical requirements specific to this story - (As a, I want, so that) - Detailed acceptance criteria (already BDD formatted) - Technical requirements specific to this story -
Business context and value - Success criteria <!-- Previous story analysis for context continuity --> Business context and value - Success criteria <!-- Previous story analysis for context continuity -->
<check if="story_num > 1"> <check if="story_num > 1">
<action>Set {{previous_story_num}} = {{story_num}} - 1</action> <action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** -
<action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action> Dev notes and learnings from previous story - Review feedback and corrections needed - Files that were created/modified and their
<action>Extract previous story intelligence: patterns - Testing approaches that worked/didn't work - Problems encountered and solutions found - Code patterns established <action>Extract
- Dev notes and learnings from previous story all learnings that could impact current story implementation</action>
- Review feedback and corrections needed
- Files that were created/modified and their patterns
- Testing approaches that worked/didn't work
- Problems encountered and solutions found
- Code patterns established
</action>
<action>Extract all learnings that could impact current story implementation</action>
</check> </check>
<!-- Git intelligence for previous work patterns --> <!-- Git intelligence for previous work patterns -->

View File

@ -15,17 +15,9 @@
- Set status_file_found = false - Set status_file_found = false
- Set standalone_mode = true - Set standalone_mode = true
- Set warning = "" - Set warning = ""
- Set status_load_error_reason = ""
- Set suggestion = "" - Set suggestion = ""
- Set next_workflow = "" - Set next_workflow = ""
- Set next_agent = "" - Set next_agent = ""
- Set status_file_path = ""
- Set field_type = ""
- Set workflow_mode = ""
- Set scan_level = ""
- Set subworkflow_success = false
- Set status_update_success = false
- Set cached_project_types = ""
</action> </action>
<action>Attempt to load workflow status directly from `{output_folder}/bmm-workflow-status.yaml`: <action>Attempt to load workflow status directly from `{output_folder}/bmm-workflow-status.yaml`:
@ -37,18 +29,10 @@
- Extract field_type, warning, suggestion, next_workflow, next_agent if present - Extract field_type, warning, suggestion, next_workflow, next_agent if present
- If file is missing, unreadable, or malformed: - If file is missing, unreadable, or malformed:
- Keep defaults and continue in standalone mode - Keep defaults and continue in standalone mode
- Set status_load_error_reason from the caught file/parse error (e.g., missing file, permission denied, YAML parse error)
- Set warning = "Unable to load workflow status from {output_folder}/bmm-workflow-status.yaml: {{status_load_error_reason}}"
- Output warning and continue in standalone mode
</action> </action>
<check if="status_exists == false"> <check if="status_exists == false">
<check if="suggestion != ''">
<output>{{suggestion}}</output> <output>{{suggestion}}</output>
</check>
<check if="warning != ''">
<output>{{warning}}</output>
</check>
<output>Note: Documentation workflow can run standalone. Continuing without progress tracking.</output> <output>Note: Documentation workflow can run standalone. Continuing without progress tracking.</output>
<action>Set standalone_mode = true</action> <action>Set standalone_mode = true</action>
<action>Set status_file_found = false</action> <action>Set status_file_found = false</action>
@ -78,9 +62,7 @@
<output>Note: This may be auto-invoked by prd for brownfield documentation.</output> <output>Note: This may be auto-invoked by prd for brownfield documentation.</output>
<ask>Continue with documentation? (y/n)</ask> <ask>Continue with documentation? (y/n)</ask>
<check if="n"> <check if="n">
<check if="suggestion != ''">
<output>{{suggestion}}</output> <output>{{suggestion}}</output>
</check>
<action>Exit workflow</action> <action>Exit workflow</action>
</check> </check>
</check> </check>
@ -96,40 +78,18 @@
<check if="project-scan-report.json exists"> <check if="project-scan-report.json exists">
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action> <action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
<action>Validate last_updated from state file:
- If last_updated is missing or invalid, set state_age_hours = 999 and mark state as stale
- Otherwise parse last_updated into validated_last_updated
</action>
<action>Extract cached project_type_id(s) from state file if present</action> <action>Extract cached project_type_id(s) from state file if present</action>
<action>Calculate state_age_hours: <action>Calculate age of state file (current time - last_updated)</action>
- If validated_last_updated exists, compute current time - validated_last_updated
- Otherwise keep state_age_hours = 999
</action>
<check if="state_age_hours >= 24"> <check if="state file age >= 24 hours">
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action> <action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Attempt to create archive directory: {output_folder}/.archive/</action> <action>Create archive directory: {output_folder}/.archive/</action>
<check if="archive directory creation failed"> <action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<output>Failed to create archive directory at {output_folder}/.archive/. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Attempt to archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<check if="archive move failed">
<output>Failed to archive old state file. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Set workflow_mode = ""</action>
<action>Set scan_level = ""</action>
<action>Set cached_project_types = ""</action>
<action>Set current_step = ""</action>
<action>Set subworkflow_success = false</action>
<action>Continue to Step 3</action> <action>Continue to Step 3</action>
</check> </check>
<check if="state_age_hours < 24"> <check if="state file age < 24 hours">
<ask>I found an in-progress workflow state from {{last_updated}}. <ask>I found an in-progress workflow state from {{last_updated}}.
@ -152,25 +112,15 @@ Your choice [1/2/3]:
<check if="user selects 1"> <check if="user selects 1">
<action>Set resume_mode = true</action> <action>Set resume_mode = true</action>
<action>Validate persisted mode before assigning workflow_mode: <action>Set workflow_mode = {{mode}}</action>
- If mode is one of [deep_dive, initial_scan, full_rescan], set workflow_mode = {{mode}}
- Otherwise set workflow_mode = "full_rescan", set resume_mode = false, and continue as fresh scan
</action>
<action>Set subworkflow_success = false</action> <action>Set subworkflow_success = false</action>
<action>Load findings summaries from state file</action> <action>Load findings summaries from state file</action>
<action>Load cached project_type_id(s) from state file</action> <action>Load cached project_type_id(s) from state file</action>
<critical>CONDITIONAL CSV LOADING FOR RESUME:</critical> <critical>CONDITIONAL CSV LOADING FOR RESUME:</critical>
<check if="cached_project_types == ''">
<output>No cached project types found. Falling back to full CSV load.</output>
<action>Load project-types.csv and architecture_registry.csv</action>
<action>Load documentation_requirements_csv for active project classification</action>
</check>
<check if="cached_project_types != ''">
<action>For each cached project_type_id, load ONLY the corresponding row from: {documentation_requirements_csv}</action> <action>For each cached project_type_id, load ONLY the corresponding row from: {documentation_requirements_csv}</action>
<action>Skip loading project-types.csv and architecture_registry.csv (not needed on resume)</action> <action>Skip loading project-types.csv and architecture_registry.csv (not needed on resume)</action>
<action>Store loaded doc requirements for use in remaining steps</action> <action>Store loaded doc requirements for use in remaining steps</action>
</check>
<action>Display: "Resuming {{workflow_mode}} from {{current_step}} with cached project type(s): {{cached_project_types}}"</action> <action>Display: "Resuming {{workflow_mode}} from {{current_step}} with cached project type(s): {{cached_project_types}}"</action>
@ -202,21 +152,9 @@ Your choice [1/2/3]:
</check> </check>
<check if="user selects 2"> <check if="user selects 2">
<action>Attempt to create archive directory: {output_folder}/.archive/</action> <action>Create archive directory: {output_folder}/.archive/</action>
<check if="archive directory creation failed"> <action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<output>Failed to create archive directory. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Attempt to move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<check if="archive move failed">
<output>Failed to archive old state file. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Reset workflow_mode, scan_level, cached_project_types, current_step to defaults</action>
<action>Set subworkflow_success = false</action>
<action>Continue to Step 3</action> <action>Continue to Step 3</action>
</check> </check>
@ -260,7 +198,6 @@ Your choice [1/2/3]:
<check if="user selects 1"> <check if="user selects 1">
<action>Set workflow_mode = "full_rescan"</action> <action>Set workflow_mode = "full_rescan"</action>
<action>Set scan_level = "standard"</action>
<action>Display: "Starting full project rescan..."</action> <action>Display: "Starting full project rescan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action> <action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action> <action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
@ -297,7 +234,6 @@ Your choice [1/2/3]:
<check if="index.md does not exist"> <check if="index.md does not exist">
<action>Set workflow_mode = "initial_scan"</action> <action>Set workflow_mode = "initial_scan"</action>
<action>Set scan_level = "initial"</action>
<action>Display: "No existing documentation found. Starting initial project scan..."</action> <action>Display: "No existing documentation found. Starting initial project scan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action> <action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action> <action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
@ -346,13 +282,8 @@ Your choice [1/2/3]:
<output>**Status Updated:** Progress tracking updated. <output>**Status Updated:** Progress tracking updated.
**Next Steps:** **Next Steps:**
- **Next required:** {{next_workflow}} ({{next_agent}} agent)
- Run `bmad-help` if you need recommended next workflows.</output> - Run `bmad-help` if you need recommended next workflows.</output>
<check if="next_workflow != '' AND next_agent != ''">
<output>- **Next required:** {{next_workflow}} ({{next_agent}} agent)</output>
</check>
<check if="next_workflow == '' OR next_agent == ''">
<output>- **Next required:** not specified</output>
</check>
</check> </check>
<check if="status_file_found == false OR status_update_success != true"> <check if="status_file_found == false OR status_update_success != true">

View File

@ -31,7 +31,7 @@ Execute a validation checklist against a target file and report findings clearly
- Path-like tokens in checklist items - Path-like tokens in checklist items
- First matching path from glob patterns supplied by checklist/input - First matching path from glob patterns supplied by checklist/input
- Normalize all candidate paths relative to repo root and resolve `.`/`..`. - Normalize all candidate paths relative to repo root and resolve `.`/`..`.
- Validate candidate existence and expected file type (`.md`, `.yaml`, `.yml`, `.json`, or checklist-defined extension). - Validate candidate existence and expected file type (`.yaml`, `.yml`, `.json`, or checklist-defined extension).
- If multiple valid candidates remain, prefer explicit key fields over inferred tokens. - If multiple valid candidates remain, prefer explicit key fields over inferred tokens.
- If no valid candidate is found, prompt user with schema example: - If no valid candidate is found, prompt user with schema example:
- `Please provide the exact file path (relative to repo root), e.g. ./workflows/ci.yml` - `Please provide the exact file path (relative to repo root), e.g. ./workflows/ci.yml`
@ -50,17 +50,11 @@ Execute a validation checklist against a target file and report findings clearly
- If checklist requires edits/auto-fixes, follow safe-edit protocol: - If checklist requires edits/auto-fixes, follow safe-edit protocol:
- Ask for confirmation before editing. - Ask for confirmation before editing.
- Create backup snapshot of target file before changes. - Create backup snapshot of target file before changes.
- Use deterministic backup location: `{project-root}/.bmad-tmp/validate-workflow/`.
- Name backup as `{target-file-name}.{timestamp}.bak` and diff as `{target-file-name}.{timestamp}.diff`.
- If temp backup directory cannot be created, fall back to adjacent backup file `{target-file}.bak`.
- Generate reversible diff preview and show it to user. - Generate reversible diff preview and show it to user.
- Apply edits only after user approval. - Apply edits only after user approval.
- Run syntax/validation checks against edited file. - Run syntax/validation checks against edited file.
- If validation fails or user cancels, rollback from backup and report rollback status. - If validation fails or user cancels, rollback from backup and report rollback status.
- Record full backup and diff paths in task output. - Record backup/diff locations in task output.
- Support `retain_artifacts` flag (default `false`) to keep backup/diff artifacts when requested.
6. **Finalize** 6. **Finalize**
- Confirm completion and provide the final validation summary. - Confirm completion and provide the final validation summary.
- If edits succeeded and `retain_artifacts` is `false`, delete backup/diff artifacts and report cleanup status.
- If edits failed or rollback occurred, preserve backup/diff artifacts and report rollback path explicitly.

View File

@ -114,12 +114,6 @@ async function runTests() {
console.log(`========================================${colors.reset}\n`); console.log(`========================================${colors.reset}\n`);
const projectRoot = path.join(__dirname, '..'); const projectRoot = path.join(__dirname, '..');
const tmpRoots = [];
const trackTmp = async (prefix) => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tmpRoots.push(dir);
return dir;
};
// ============================================================ // ============================================================
// Test 1: YAML → XML Agent Compilation (In-Memory) // Test 1: YAML → XML Agent Compilation (In-Memory)
@ -392,7 +386,7 @@ async function runTests() {
path.join(projectRoot, 'src', 'bmm', 'workflows', 'document-project'), path.join(projectRoot, 'src', 'bmm', 'workflows', 'document-project'),
path.join(projectRoot, 'tools', 'cli', 'installers', 'lib'), path.join(projectRoot, 'tools', 'cli', 'installers', 'lib'),
]; ];
const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml', '.js', '.cjs', '.mjs']); const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml']);
const forbiddenRefPattern = /(^|[^a-zA-Z0-9_-])workflow\.xml\b/; const forbiddenRefPattern = /(^|[^a-zA-Z0-9_-])workflow\.xml\b/;
const offenders = []; const offenders = [];
@ -438,7 +432,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 11: Gemini Template Extension Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 11: Gemini Template Extension Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-gemini-install-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-install-'));
const projectDir = path.join(tmpRoot, 'project'); const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
@ -476,7 +470,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 12: Manifest Stale Entry Cleanup Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 12: Manifest Stale Entry Cleanup Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-manifest-clean-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-manifest-clean-'));
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.copy(path.join(projectRoot, 'src', 'core'), path.join(bmadDir, 'core')); await fs.copy(path.join(projectRoot, 'src', 'core'), path.join(bmadDir, 'core'));
await fs.copy(path.join(projectRoot, 'src', 'bmm'), path.join(bmadDir, 'bmm')); await fs.copy(path.join(projectRoot, 'src', 'bmm'), path.join(bmadDir, 'bmm'));
@ -511,7 +505,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 13: Internal Task Exposure Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 13: Internal Task Exposure Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-task-filter-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-task-filter-'));
const projectDir = path.join(tmpRoot, 'project'); const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
const commandsDir = path.join(tmpRoot, 'commands'); const commandsDir = path.join(tmpRoot, 'commands');
@ -598,7 +592,7 @@ web_bundle:
console.log(`${colors.yellow}Test Suite 16: Task/Tool Standalone + CRLF Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 16: Task/Tool Standalone + CRLF Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-standalone-crlf-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-standalone-crlf-'));
const coreTasksDir = path.join(tmpRoot, '_bmad', 'core', 'tasks'); const coreTasksDir = path.join(tmpRoot, '_bmad', 'core', 'tasks');
const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools'); const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools');
await fs.ensureDir(coreTasksDir); await fs.ensureDir(coreTasksDir);
@ -723,7 +717,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-codex-visibility-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-visibility-'));
const projectDir = path.join(tmpRoot, 'project'); const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
@ -757,7 +751,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`);
try { try {
const tmpRoot = await trackTmp('bmad-empty-target-'); const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-empty-target-'));
const projectDir = path.join(tmpRoot, 'project'); const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
@ -862,10 +856,6 @@ internal: true
console.log(''); console.log('');
for (const tmpRoot of tmpRoots) {
await fs.remove(tmpRoot).catch(() => {});
}
// ============================================================ // ============================================================
// Summary // Summary
// ============================================================ // ============================================================

View File

@ -1201,11 +1201,19 @@ class Installer {
lines.push(` ${icon} ${r.step}${detail}`); lines.push(` ${icon} ${r.step}${detail}`);
} }
// Context and warnings // Add context info
lines.push(''); lines.push('');
if (context.bmadDir) { if (context.bmadDir) {
lines.push(` Installed to: ${color.dim(context.bmadDir)}`); lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
} }
if (context.modules && context.modules.length > 0) {
lines.push(` Modules: ${color.dim(context.modules.join(', '))}`);
}
if (context.ides && context.ides.length > 0) {
lines.push(` Tools: ${color.dim(context.ides.join(', '))}`);
}
// Custom/modified file warnings
if (context.customFiles && context.customFiles.length > 0) { if (context.customFiles && context.customFiles.length > 0) {
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
} }

View File

@ -75,10 +75,10 @@ class WorkflowCommandGenerator {
if (workflowRelPath.includes('_bmad/')) { if (workflowRelPath.includes('_bmad/')) {
const parts = workflowRelPath.split(/_bmad\//); const parts = workflowRelPath.split(/_bmad\//);
if (parts.length > 1) { if (parts.length > 1) {
workflowRelPath = parts.at(-1); workflowRelPath = parts.slice(1).join('/');
} }
} else if (workflowRelPath.includes('/src/') || workflowRelPath.startsWith('src/')) { } else if (workflowRelPath.includes('/src/')) {
const match = workflowRelPath.match(/(?:^|\/)src\/([^/]+)\/(.+)/); const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
if (match) { if (match) {
workflowRelPath = `${match[1]}/${match[2]}`; workflowRelPath = `${match[1]}/${match[2]}`;
} }
@ -119,9 +119,30 @@ class WorkflowCommandGenerator {
* Generate command content for a workflow * Generate command content for a workflow
*/ */
async generateCommandContent(workflow, bmadDir) { async generateCommandContent(workflow, bmadDir) {
// Load the workflow command template // Determine template based on workflow file type
const template = await fs.readFile(this.templatePath, 'utf8'); const templatePath = path.join(path.dirname(this.templatePath), 'workflow-commander.md');
const workflowPath = this.mapSourcePathToInstalled(workflow.path);
// Load the appropriate template
const template = await fs.readFile(templatePath, 'utf8');
// Convert source path to installed path
// From: /Users/.../src/bmm/workflows/.../workflow.md
// To: {project-root}/_bmad/bmm/workflows/.../workflow.md
let workflowPath = workflow.path;
// Extract the relative path from source
if (workflowPath.includes('/src/bmm/')) {
// bmm is directly under src/
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
workflowPath = `${this.bmadFolderName}/bmm/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
workflowPath = `${this.bmadFolderName}/core/${match[1]}`;
}
}
// Replace template variables // Replace template variables
return template return template
@ -191,15 +212,14 @@ class WorkflowCommandGenerator {
When running any workflow: When running any workflow:
1. Resolve loader paths: 1. Resolve loader paths:
- Primary: {project-root}/${this.bmadFolderName}/core/tasks/workflow.md - Primary: {project-root}/${this.bmadFolderName}/core/tasks/workflow.md
- Optional dev fallback: {project-root}/src/core/tasks/workflow.md (only if it exists and is readable) - Fallback: {project-root}/src/core/tasks/workflow.md
2. Check the primary path exists and is readable before loading 2. Check the primary path exists and is readable before loading
3. If primary is missing/unreadable, log a warning with the primary path and error 3. If primary is missing/unreadable, log a warning with the path and error, then try fallback
4. Only if the dev fallback exists and is readable, try the fallback path; otherwise skip it 4. If fallback is also missing/unreadable, log an error with both attempted paths and stop
5. If no readable loader is found, log an error with all attempted readable paths and stop 5. LOAD the resolved workflow loader file
6. LOAD the resolved workflow loader file 6. Pass the workflow path as 'workflow-config' parameter
7. Pass the workflow path as 'workflow-config' parameter 7. Follow workflow.md instructions EXACTLY
8. Follow workflow.md instructions EXACTLY 8. Save outputs after EACH section
9. Save outputs after EACH section
## Modes ## Modes
- Normal: Full interaction - Normal: Full interaction
@ -210,33 +230,21 @@ When running any workflow:
} }
transformWorkflowPath(workflowPath) { transformWorkflowPath(workflowPath) {
return this.mapSourcePathToInstalled(workflowPath, true); let transformed = workflowPath;
if (workflowPath.includes('/src/bmm/')) {
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
} }
mapSourcePathToInstalled(sourcePath, includeProjectRootPrefix = false) { return transformed;
if (!sourcePath) {
return sourcePath;
}
const normalized = sourcePath.replaceAll('\\', '/');
const srcMatch = normalized.match(/(?:^|\/)src\/([^/]+)\/(.+)/);
if (srcMatch) {
const mapped = `${this.bmadFolderName}/${srcMatch[1]}/${srcMatch[2]}`;
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
if (normalized.includes('_bmad/')) {
const parts = normalized.split(/_bmad\//);
const relative = parts.at(-1);
const mapped = `${this.bmadFolderName}/${relative}`;
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
if (normalized.startsWith(`${this.bmadFolderName}/`)) {
return includeProjectRootPrefix ? `{project-root}/${normalized}` : normalized;
}
return sourcePath;
} }
async loadWorkflowManifest(bmadDir) { async loadWorkflowManifest(bmadDir) {

View File

@ -3,7 +3,6 @@ name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}} Follow all instructions in the workflow file exactly as written.
4. Follow all runner instructions exactly

View File

@ -4,8 +4,4 @@ description: '{{description}}'
disable-model-invocation: true disable-model-invocation: true
--- ---
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS: IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
1. LOAD the FULL @{project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. Execute workflow runner with parameter: workflow-config: {{bmadFolderName}}/{{path}}
4. FOLLOW the runner instructions exactly as written

View File

@ -2,11 +2,15 @@ description = """{{description}}"""
prompt = """ prompt = """
Execute the BMAD '{{name}}' workflow. Execute the BMAD '{{name}}' workflow.
CRITICAL: Use the workflow runner task, not direct workflow-file execution. CRITICAL: This is a structured YAML workflow. Follow these steps precisely:
WORKFLOW INSTRUCTIONS: 1. LOAD the workflow definition from {project-root}/{{bmadFolderName}}/{{workflow_path}}
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md 2. PARSE the YAML structure to understand:
2. READ its entire contents - Workflow phases and steps
3. PASS this parameter to the runner: workflow-config: {{workflow_path}} - Required inputs and outputs
4. FOLLOW every runner step exactly as specified - Dependencies between steps
3. EXECUTE each step in order
4. VALIDATE outputs before proceeding to next step
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
""" """

View File

@ -2,11 +2,13 @@ description = """{{description}}"""
prompt = """ prompt = """
Execute the BMAD '{{name}}' workflow. Execute the BMAD '{{name}}' workflow.
CRITICAL: Use the workflow runner task, not direct workflow-file execution. CRITICAL: You must load and follow the workflow definition exactly.
WORKFLOW INSTRUCTIONS: WORKFLOW INSTRUCTIONS:
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md 1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{workflow_path}}
2. READ its entire contents 2. READ its entire contents
3. PASS this parameter to the runner: workflow-config: {{workflow_path}} 3. FOLLOW every step precisely as specified
4. FOLLOW every runner step exactly as specified 4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
""" """

View File

@ -4,11 +4,4 @@ inclusion: manual
# {{name}} # {{name}}
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS: IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!
1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.md]]
2. READ its entire contents
3. Execute workflow runner with YAML parameter:
```yaml
workflow-config: {{bmadFolderName}}/{{path}}
```
4. FOLLOW the runner instructions exactly as written

View File

@ -4,10 +4,12 @@ description: '{{description}}'
Execute the BMAD '{{name}}' workflow. Execute the BMAD '{{name}}' workflow.
CRITICAL: Use the workflow runner task, not direct workflow-file execution. CRITICAL: You must load and follow the workflow definition exactly.
WORKFLOW INSTRUCTIONS: WORKFLOW INSTRUCTIONS:
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md 1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents 2. READ its entire contents
3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}} 3. FOLLOW every step precisely as specified
4. FOLLOW every runner step exactly as specified 4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}

View File

@ -4,10 +4,12 @@ description: '{{description}}'
Execute the BMAD '{{name}}' workflow. Execute the BMAD '{{name}}' workflow.
CRITICAL: Use the workflow runner task, not direct workflow-file execution. CRITICAL: You must load and follow the workflow definition exactly.
WORKFLOW INSTRUCTIONS: WORKFLOW INSTRUCTIONS:
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md 1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents 2. READ its entire contents
3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}} 3. FOLLOW every step precisely as specified
4. FOLLOW every runner step exactly as specified 4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}

View File

@ -4,7 +4,6 @@
--- ---
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}} Follow all instructions in the workflow file exactly as written.
4. Follow all runner instructions exactly

View File

@ -4,7 +4,6 @@
## Instructions ## Instructions
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}} Follow all instructions in the workflow file exactly as written.
4. Follow all runner instructions exactly

View File

@ -5,7 +5,6 @@ auto_execution_mode: "iterate"
# {{name}} # {{name}}
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md Read the entire workflow file at {project-root}/_bmad/{{workflow_path}}
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}} Follow all instructions in the workflow file exactly as written.
4. Follow all runner instructions exactly

View File

@ -3,8 +3,4 @@ description: '{{description}}'
disable-model-invocation: true disable-model-invocation: true
--- ---
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS: IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{{workflow_path}}, READ its entire contents and follow its directions exactly!
1. LOAD the FULL @{project-root}/_bmad/core/tasks/workflow.md
2. READ its entire contents
3. Execute workflow runner with parameter: workflow-config: {{workflow_path}}
4. FOLLOW the runner instructions exactly as written

View File

@ -813,6 +813,7 @@ class ModuleManager {
const newline = frontmatterMatch[0].includes('\r\n') ? '\r\n' : '\n'; const newline = frontmatterMatch[0].includes('\r\n') ? '\r\n' : '\n';
try { try {
const yaml = require('yaml');
const parsed = yaml.parse(frontmatterMatch[1]); const parsed = yaml.parse(frontmatterMatch[1]);
if (!parsed || typeof parsed !== 'object' || !Object.prototype.hasOwnProperty.call(parsed, 'web_bundle')) { if (!parsed || typeof parsed !== 'object' || !Object.prototype.hasOwnProperty.call(parsed, 'web_bundle')) {
@ -896,6 +897,7 @@ class ModuleManager {
let manifestData = {}; let manifestData = {};
if (await fs.pathExists(manifestPath)) { if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8'); const manifestContent = await fs.readFile(manifestPath, 'utf8');
const yaml = require('yaml');
manifestData = yaml.parse(manifestContent); manifestData = yaml.parse(manifestContent);
} }
if (!manifestData.agentCustomizations) { if (!manifestData.agentCustomizations) {
@ -904,6 +906,7 @@ class ModuleManager {
manifestData.agentCustomizations[path.relative(bmadDir, customizePath)] = originalHash; manifestData.agentCustomizations[path.relative(bmadDir, customizePath)] = originalHash;
// Write back to manifest // Write back to manifest
const yaml = require('yaml');
// Clean the manifest data to remove any non-serializable values // Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData); const cleanManifestData = structuredClone(manifestData);
@ -1238,31 +1241,16 @@ class ModuleManager {
} }
} }
const installerDir = path.join(sourcePath, '_module-installer'); const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
// Prefer .cjs (always CommonJS) then fall back to .js
const cjsPath = path.join(installerDir, 'installer.cjs');
const jsPath = path.join(installerDir, 'installer.js');
const hasCjs = await fs.pathExists(cjsPath);
const installerPath = hasCjs ? cjsPath : jsPath;
// Check if module has a custom installer // Check if module has a custom installer
if (!hasCjs && !(await fs.pathExists(jsPath))) { if (!(await fs.pathExists(installerPath))) {
return; // No custom installer return; // No custom installer
} }
try { try {
// .cjs files are always CommonJS and safe to require(). // Load the module installer
// .js files may be ESM (when the package sets "type":"module"), const moduleInstaller = require(installerPath);
// so use dynamic import() which handles both CJS and ESM.
let moduleInstaller;
if (hasCjs) {
moduleInstaller = require(installerPath);
} else {
const { pathToFileURL } = require('node:url');
const imported = await import(pathToFileURL(installerPath).href);
// CJS module.exports lands on .default; ESM default can be object, function, or class
moduleInstaller = imported.default == null ? imported : imported.default;
}
if (typeof moduleInstaller.install === 'function') { if (typeof moduleInstaller.install === 'function') {
// Get project root (parent of bmad directory) // Get project root (parent of bmad directory)
@ -1288,12 +1276,8 @@ class ModuleManager {
await prompts.log.warn(`Module installer for ${moduleName} returned false`); await prompts.log.warn(`Module installer for ${moduleName} returned false`);
} }
} }
} catch { } catch (error) {
// Post-install scripts are optional; module files are already installed. await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
// TODO: Eliminate post-install scripts entirely by adding a `directories` key
// to module.yaml that declares which config keys are paths to auto-create.
// The main installer can then handle directory creation centrally, removing
// the need for per-module installer.js scripts and their CJS/ESM issues.
} }
} }

View File

@ -274,6 +274,7 @@ class UI {
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds); selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
} }
// After module selection, ask about custom modules // After module selection, ask about custom modules
@ -361,9 +362,6 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules); selectedModules.push(...customModuleResult.selectedCustomModules);
} }
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection // Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options); const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -901,10 +899,107 @@ class UI {
} }
/** /**
* Select all modules (official + community) using grouped multiselect. * Prompt for module selection
* Core is shown as locked but filtered from the result since it's always installed separately. * @param {Array} moduleChoices - Available module choices
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices, defaultSelections = null) {
// If defaultSelections is provided, use it to override checked state
// Otherwise preserve the checked state from moduleChoices (set by getModuleChoices)
const choicesWithDefaults = moduleChoices.map((choice) => ({
...choice,
...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
value: '__NONE__',
label: '\u26A0 None / I changed my mind - skip module installation',
checked: false,
},
];
const selected = await prompts.multiselect({
message: 'Select modules to install (use arrow keys, space to toggle):',
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Get external module choices for selection
* @returns {Array} External module choices for prompt
*/
async getExternalModuleChoices() {
const externalManager = new ExternalModuleManager();
const modules = await externalManager.listAvailable();
return modules.map((mod) => ({
name: mod.name,
value: mod.code, // Use the code (e.g., 'cis') as the value
checked: mod.defaultSelected || false,
hint: mod.description || undefined, // Show description as hint
module: mod, // Store full module info for later use
}));
}
/**
* Prompt for external module selection
* @param {Array} externalModuleChoices - Available external module choices
* @param {Array} defaultSelections - Module codes to pre-select
* @returns {Array} Selected external module codes
*/
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const message = 'Select official BMad modules to install (use arrow keys, space to toggle):';
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
...choice,
checked: defaultSelections.includes(choice.value),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
name: '⚠ None / I changed my mind - skip external module installation',
value: '__NONE__',
checked: false,
},
];
const selected = await prompts.multiselect({
message,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Select all modules (core + official + community) using grouped multiselect
* @param {Set} installedModuleIds - Currently installed module IDs * @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes (excluding core) * @returns {Array} Selected module codes
*/ */
async selectAllModules(installedModuleIds = new Set()) { async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager'); const { ModuleManager } = require('../installers/lib/modules/manager');
@ -973,7 +1068,11 @@ class UI {
} }
} }
} }
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint }))); allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), {
// "None" option at the end
label: '\u26A0 None - Skip module installation',
value: '__NONE__',
});
const selected = await prompts.autocompleteMultiselect({ const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:', message: 'Select modules to install:',
@ -984,7 +1083,14 @@ class UI {
maxItems: allOptions.length, maxItems: allOptions.length,
}); });
const result = selected ? selected.filter((m) => m !== 'core') : []; // If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
const result = selected ? selected.filter((m) => m !== '__NONE__') : [];
// Display selected modules as bulleted list // Display selected modules as bulleted list
if (result.length > 0) { if (result.length > 0) {