Compare commits
13 Commits
ec72336c2e
...
63a03b6945
| Author | SHA1 | Date |
|---|---|---|
|
|
63a03b6945 | |
|
|
c8ca083316 | |
|
|
5b79330f72 | |
|
|
b4d118c897 | |
|
|
3e35057b89 | |
|
|
0659aac02c | |
|
|
0bf8e0edfb | |
|
|
36c21dbada | |
|
|
2754d66042 | |
|
|
60c3477a3a | |
|
|
622b627430 | |
|
|
c563cef0c2 | |
|
|
90ea3cbed7 |
Binary file not shown.
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -1,5 +1,58 @@
|
|||
# Changelog
|
||||
|
||||
## [6.0.0-Beta.8]
|
||||
|
||||
**Release: February 8, 2026**
|
||||
|
||||
### 🌟 Key Highlights
|
||||
|
||||
1. **Non-Interactive Installation** — Full CI/CD support with 10 new CLI flags for automated deployments
|
||||
2. **Complete @clack/prompts Migration** — Unified CLI experience with consolidated installer output
|
||||
3. **CSV File Reference Validation** — Extended Layer 1 validator to catch broken workflow references in CSV files
|
||||
4. **Kiro IDE Support** — Standardized config-driven installation, replacing custom installer
|
||||
|
||||
### 🎁 Features
|
||||
|
||||
* **Non-Interactive Installation** — Added `--directory`, `--modules`, `--tools`, `--custom-content`, `--user-name`, `--communication-language`, `--document-output-language`, `--output-folder`, and `-y/--yes` flags for CI/CD automation (#1520)
|
||||
* **CSV File Reference Validation** — Extended validator to scan `.csv` files for broken workflow references, checking 501 references across 212 files (#1573)
|
||||
* **Kiro IDE Support** — Replaced broken custom installer with config-driven templates using `#[[file:...]]` syntax and `inclusion: manual` frontmatter (#1589)
|
||||
* **OpenCode Template Consolidation** — Combined split templates with `mode: primary` frontmatter for Tab-switching support, fixing agent discovery (#1556)
|
||||
* **Modules Reference Page** — Added official external modules reference documentation (#1540)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **Installer Streamlining** — Removed "None - Skip module installation" option, eliminated ~100 lines of dead code, and added ESM/.cjs support for module installers (#1590)
|
||||
* **CodeRabbit Workflow** — Changed `pull_request` to `pull_request_target` to fix 403 errors and enable reviews on fork PRs (#1583)
|
||||
* **Party Mode Return Protocol** — Added RETURN PROTOCOL to prevent lost-in-the-middle failures after Party Mode completes (#1569)
|
||||
* **Spacebar Toggle** — Fixed SPACE key not working in autocomplete multiselect prompts for tool/IDE selection (#1557)
|
||||
* **OpenCode Agent Routing** — Fixed agents installing to wrong directory by adding `targets` array for routing `.opencode/agent/` vs `.opencode/command/` (#1549)
|
||||
* **Technical Research Workflow** — Fixed step-05 routing to step-06 and corrected `stepsCompleted` values (#1547)
|
||||
* **Forbidden Variable Removal** — Removed `workflow_path` variable from 16 workflow step files (#1546)
|
||||
* **Kilo Installer** — Fixed YAML formatting issues by trimming activation header and converting to yaml.parse/stringify (#1537)
|
||||
* **bmad-help** — Now reads project-specific docs and respects `communication_language` setting (#1535)
|
||||
* **Cache Errors** — Removed `--prefer-offline` npm flag to prevent stale cache errors during installation (#1531)
|
||||
|
||||
### ♻️ Refactoring
|
||||
|
||||
* **Complete @clack/prompts Migration** — Migrated 24 files from legacy libraries (ora, chalk, boxen, figlet, etc.), replaced ~100 console.log+chalk calls, consolidated installer output to single spinner, and removed 5 dependencies (#1586)
|
||||
* **Downloads Page Removal** — Removed downloads page, bundle generation, and archiver dependency in favor of GitHub's native archives (#1577)
|
||||
* **Workflow Verb Standardization** — Replaced "invoke/run" with "load and follow/load" in review workflow prompts (#1570)
|
||||
* **Documentation Language** — Renamed "brownfield" to "established projects" and flattened directory structure for accessibility (#1539)
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* **Comprehensive Site Review** — Fixed broken directory tree diagram, corrected grammar/capitalization, added SEO descriptions, and reordered how-to guides (#1578)
|
||||
* **SEO Metadata** — Added description front matter to 9 documentation pages for search engine optimization (#1566)
|
||||
* **PR Template** — Added pull request template for consistent PR descriptions (#1554)
|
||||
* **Manual Release Cleanup** — Removed broken manual-release workflow and related scripts (#1576)
|
||||
|
||||
### 🔧 Maintenance
|
||||
|
||||
* **Dual-Mode AI Code Review** — Configured Augment Code (audit mode) and CodeRabbit (adversarial mode) for improved code quality (#1511)
|
||||
* **Package-Lock Sync** — Cleaned up 471 lines of orphaned dependencies after archiver removal (#1580)
|
||||
|
||||
---
|
||||
|
||||
## [6.0.0-Beta.7]
|
||||
|
||||
**Release: February 4, 2026**
|
||||
|
|
|
|||
|
|
@ -114,17 +114,6 @@ export default [
|
|||
},
|
||||
},
|
||||
|
||||
// Module installer scripts use CommonJS for compatibility
|
||||
{
|
||||
files: ['**/_module-installer/**/*.js'],
|
||||
rules: {
|
||||
// Allow CommonJS patterns for installer scripts
|
||||
'unicorn/prefer-module': 'off',
|
||||
'n/no-missing-require': 'off',
|
||||
'n/no-unpublished-require': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// ESLint config file should not be checked for publish-related Node rules
|
||||
{
|
||||
files: ['eslint.config.mjs'],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-Beta.7",
|
||||
"version": "6.0.0-Beta.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-Beta.7",
|
||||
"version": "6.0.0-Beta.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-Beta.7",
|
||||
"version": "6.0.0-Beta.8",
|
||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||
"keywords": [
|
||||
"agile",
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const chalk = require('chalk');
|
||||
|
||||
// Directories to create from config
|
||||
const DIRECTORIES = ['output_folder', 'planning_artifacts', 'implementation_artifacts'];
|
||||
|
||||
/**
|
||||
* BMM Module Installer
|
||||
* Creates output directories configured in module config
|
||||
*
|
||||
* @param {Object} options - Installation options
|
||||
* @param {string} options.projectRoot - The root directory of the target project
|
||||
* @param {Object} options.config - Module configuration from module.yaml
|
||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||
* @param {Object} options.logger - Logger instance for output
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async function install(options) {
|
||||
const { projectRoot, config, logger } = options;
|
||||
|
||||
try {
|
||||
logger.log(chalk.blue('🚀 Installing BMM Module...'));
|
||||
|
||||
// Create configured directories
|
||||
for (const configKey of DIRECTORIES) {
|
||||
const configValue = config[configKey];
|
||||
if (!configValue) continue;
|
||||
|
||||
const dirPath = configValue.replace('{project-root}/', '');
|
||||
const fullPath = path.join(projectRoot, dirPath);
|
||||
|
||||
if (!(await fs.pathExists(fullPath))) {
|
||||
const dirName = configKey.replace('_', ' ');
|
||||
logger.log(chalk.yellow(`Creating ${dirName} directory: ${dirPath}`));
|
||||
await fs.ensureDir(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(chalk.green('✓ BMM Module installation complete'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(chalk.red(`Error installing BMM module: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { install };
|
||||
|
|
@ -5,6 +5,7 @@ agent:
|
|||
title: Business Analyst
|
||||
icon: 📊
|
||||
module: bmm
|
||||
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: Architect
|
||||
icon: 🏗️
|
||||
module: bmm
|
||||
capabilities: "distributed systems, cloud infrastructure, API design, scalable patterns"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: Developer Agent
|
||||
icon: 💻
|
||||
module: bmm
|
||||
capabilities: "story execution, test-driven development, code implementation"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ agent:
|
|||
title: Product Manager
|
||||
icon: 📋
|
||||
module: bmm
|
||||
capabilities: "PRD creation, requirements discovery, stakeholder alignment, user interviews"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ agent:
|
|||
title: QA Engineer
|
||||
icon: 🧪
|
||||
module: bmm
|
||||
capabilities: "test automation, API testing, E2E testing, coverage analysis"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: Quick Flow Solo Dev
|
||||
icon: 🚀
|
||||
module: bmm
|
||||
capabilities: "rapid spec creation, lean implementation, minimum ceremony"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: Scrum Master
|
||||
icon: 🏃
|
||||
module: bmm
|
||||
capabilities: "sprint planning, story preparation, agile ceremonies, backlog management"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: Technical Writer
|
||||
icon: 📚
|
||||
module: bmm
|
||||
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
|
||||
hasSidecar: true
|
||||
|
||||
persona:
|
||||
|
|
@ -15,7 +16,7 @@ agent:
|
|||
communication_style: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
|
||||
principles: |
|
||||
- Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy.
|
||||
- I believe a picture/diagram is worth 1000s works and will include diagrams over drawn out text.
|
||||
- I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text.
|
||||
- I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed.
|
||||
- I will always strive to follow `_bmad/_memory/tech-writer-sidecar/documentation-standards.md` best practices.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
title: UX Designer
|
||||
icon: 🎨
|
||||
module: bmm
|
||||
capabilities: "user research, interaction design, UI patterns, experience strategy"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
@ -23,4 +24,4 @@ agent:
|
|||
menu:
|
||||
- trigger: CU or fuzzy match on ux-design
|
||||
exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md"
|
||||
description: "[CU] Create UX: Guidance through realizing the plan for your UX to inform architecture and implementation. PRovides more details that what was discovered in the PRD"
|
||||
description: "[CU] Create UX: Guidance through realizing the plan for your UX to inform architecture and implementation. Provides more details than what was discovered in the PRD"
|
||||
|
|
|
|||
|
|
@ -42,3 +42,9 @@ project_knowledge: # Artifacts from research, document-project output, other lon
|
|||
prompt: "Where should long-term project knowledge be stored? (docs, research, references)"
|
||||
default: "docs"
|
||||
result: "{project-root}/{value}"
|
||||
|
||||
# Directories to create during installation (declarative, no code execution)
|
||||
directories:
|
||||
- "{planning_artifacts}"
|
||||
- "{implementation_artifacts}"
|
||||
- "{project_knowledge}"
|
||||
|
|
|
|||
|
|
@ -70,14 +70,22 @@ This file contains the BMAD PRD philosophy, standards, and validation criteria t
|
|||
**If PRD path provided as invocation parameter:**
|
||||
- Use provided path
|
||||
|
||||
**If no PRD path provided:**
|
||||
"**PRD Validation Workflow**
|
||||
**If no PRD path provided, auto-discover:**
|
||||
- Search `{planning_artifacts}` for files matching `*prd*.md`
|
||||
- Also check for sharded PRDs: `{planning_artifacts}/*prd*/*.md`
|
||||
|
||||
Which PRD would you like to validate?
|
||||
**If exactly ONE PRD found:**
|
||||
- Use it automatically
|
||||
- Inform user: "Found PRD: {discovered_path} — using it for validation."
|
||||
|
||||
Please provide the path to the PRD file you want to validate."
|
||||
**If MULTIPLE PRDs found:**
|
||||
- List all discovered PRDs with numbered options
|
||||
- "I found multiple PRDs. Which one would you like to validate?"
|
||||
- Wait for user selection
|
||||
|
||||
**Wait for user to provide PRD path.**
|
||||
**If NO PRDs found:**
|
||||
- "I couldn't find any PRD files in {planning_artifacts}. Please provide the path to the PRD file you want to validate."
|
||||
- Wait for user to provide PRD path.
|
||||
|
||||
### 3. Validate PRD Exists and Load
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,4 @@ Load and read full config from {main_config} and resolve:
|
|||
|
||||
"**Validate Mode: Validating an existing PRD against BMAD standards.**"
|
||||
|
||||
Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file."
|
||||
|
||||
Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ document_output_language: "{config_source}:document_output_language"
|
|||
date: system-generated
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
output_folder: "{implementation_artifacts}"
|
||||
sprint_status: "{implementation_artifacts}/sprint-status.yaml"
|
||||
|
||||
# Workflow components
|
||||
|
|
@ -21,10 +20,7 @@ instructions: "{installed_path}/instructions.xml"
|
|||
validation: "{installed_path}/checklist.md"
|
||||
template: false
|
||||
|
||||
variables:
|
||||
# Project context
|
||||
project_context: "**/project-context.md"
|
||||
story_dir: "{implementation_artifacts}"
|
||||
project_context: "**/project-context.md"
|
||||
|
||||
# Smart input file references - handles both whole docs and sharded docs
|
||||
# Priority: Whole document first, then sharded version
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<workflow>
|
||||
|
||||
<step n="1" goal="Initialize Change Navigation">
|
||||
<action>Load {project_context} for coding standards and project-wide patterns (if exists)</action>
|
||||
<action>Confirm change trigger and gather user description of the issue</action>
|
||||
<action>Ask: "What specific issue or change has been identified that requires navigation?"</action>
|
||||
<action>Verify access to required project documents:</action>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ date: system-generated
|
|||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
project_knowledge: "{config_source}:project_knowledge"
|
||||
output_folder: "{implementation_artifacts}"
|
||||
sprint_status: "{implementation_artifacts}/sprint-status.yaml"
|
||||
project_context: "**/project-context.md"
|
||||
|
||||
# Smart input file references - handles both whole docs and sharded docs
|
||||
# Priority: Whole document first, then sharded version
|
||||
|
|
@ -51,6 +50,5 @@ input_file_patterns:
|
|||
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/correct-course"
|
||||
template: false
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
checklist: "{installed_path}/checklist.md"
|
||||
default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ This is a COMPETITION to create the **ULTIMATE story context** that makes LLM de
|
|||
### **Required Inputs:**
|
||||
|
||||
- **Story file**: The story file to review and improve
|
||||
- **Workflow variables**: From workflow.yaml (story_dir, output_folder, epics_file, etc.)
|
||||
- **Workflow variables**: From workflow.yaml (implementation_artifacts, epics_file, etc.)
|
||||
- **Source documents**: Epics, architecture, etc. (discovered or provided)
|
||||
- **Validation framework**: `validate-workflow.xml` (handles checklist execution)
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ You will systematically re-do the entire story creation process, but with a crit
|
|||
2. **Load the story file**: `{story_file_path}` (provided by user or discovered)
|
||||
3. **Load validation framework**: `{project-root}/_bmad/core/tasks/validate-workflow.xml`
|
||||
4. **Extract metadata**: epic_num, story_num, story_key, story_title from story file
|
||||
5. **Resolve all workflow variables**: story_dir, output_folder, epics_file, architecture_file, etc.
|
||||
5. **Resolve all workflow variables**: implementation_artifacts, epics_file, architecture_file, etc.
|
||||
6. **Understand current status**: What story implementation guidance is currently provided?
|
||||
|
||||
**Note:** If running in fresh context, user should provide the story file path being reviewed. If running from create-story workflow, the validation framework will automatically discover the checklist and story file.
|
||||
|
|
|
|||
|
|
@ -192,7 +192,8 @@
|
|||
(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 -->
|
||||
<check if="story_num > 1">
|
||||
<action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** -
|
||||
<action>Find {{previous_story_num}}: scan {implementation_artifacts} for the story file in epic {{epic_num}} with the highest story number less than {{story_num}}</action>
|
||||
<action>Load previous story file: {implementation_artifacts}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** -
|
||||
Dev notes and learnings from previous story - 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>Extract
|
||||
all learnings that could impact current story implementation</action>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ author: "BMad"
|
|||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
document_output_language: "{config_source}:document_output_language"
|
||||
user_skill_level: "{config_source}:user_skill_level"
|
||||
date: system-generated
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
output_folder: "{implementation_artifacts}"
|
||||
story_dir: "{implementation_artifacts}"
|
||||
|
||||
# Workflow components
|
||||
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/create-story"
|
||||
|
|
@ -19,18 +19,14 @@ instructions: "{installed_path}/instructions.xml"
|
|||
validation: "{installed_path}/checklist.md"
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
sprint_status: "{implementation_artifacts}/sprint-status.yaml" # Primary source for story tracking
|
||||
epics_file: "{planning_artifacts}/epics.md" # Enhanced epics+stories with BDD and source hints
|
||||
prd_file: "{planning_artifacts}/prd.md" # Fallback for requirements (if not in epics file)
|
||||
architecture_file: "{planning_artifacts}/architecture.md" # Fallback for constraints (if not in epics file)
|
||||
ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file)
|
||||
story_title: "" # Will be elicited if not derivable
|
||||
|
||||
# Project context
|
||||
sprint_status: "{implementation_artifacts}/sprint-status.yaml" # Primary source for story tracking
|
||||
epics_file: "{planning_artifacts}/epics.md" # Enhanced epics+stories with BDD and source hints
|
||||
prd_file: "{planning_artifacts}/prd.md" # Fallback for requirements (if not in epics file)
|
||||
architecture_file: "{planning_artifacts}/architecture.md" # Fallback for constraints (if not in epics file)
|
||||
ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file)
|
||||
story_title: "" # Will be elicited if not derivable
|
||||
project_context: "**/project-context.md"
|
||||
|
||||
default_output_file: "{story_dir}/{{story_key}}.md"
|
||||
default_output_file: "{implementation_artifacts}/{{story_key}}.md"
|
||||
|
||||
# Smart input file references - Simplified for enhanced approach
|
||||
# The epics+stories file should contain everything needed with source hints
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
|
||||
<!-- Non-sprint story discovery -->
|
||||
<check if="{{sprint_status}} file does NOT exist">
|
||||
<action>Search {story_dir} for stories directly</action>
|
||||
<action>Search {implementation_artifacts} for stories directly</action>
|
||||
<action>Find stories with "ready-for-dev" status in files</action>
|
||||
<action>Look for story files matching pattern: *-*-*.md</action>
|
||||
<action>Read each candidate story file to check Status section</action>
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
</check>
|
||||
|
||||
<action>Store the found story_key (e.g., "1-2-user-authentication") for later status updates</action>
|
||||
<action>Find matching story file in {story_dir} using story_key pattern: {{story_key}}.md</action>
|
||||
<action>Find matching story file in {implementation_artifacts} using story_key pattern: {{story_key}}.md</action>
|
||||
<action>Read COMPLETE story file from discovered path</action>
|
||||
|
||||
<anchor id="task_check" />
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ author: "BMad"
|
|||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
user_skill_level: "{config_source}:user_skill_level"
|
||||
document_output_language: "{config_source}:document_output_language"
|
||||
story_dir: "{config_source}:implementation_artifacts"
|
||||
date: system-generated
|
||||
|
||||
# Workflow components
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ PARTY MODE PROTOCOL:
|
|||
|
||||
<step n="1" goal="Epic Discovery - Find Completed Epic with Priority Logic">
|
||||
|
||||
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
|
||||
<action>Explain to {user_name} the epic discovery process using natural dialogue</action>
|
||||
|
||||
<output>
|
||||
|
|
@ -80,7 +81,7 @@ Bob (Scrum Master): "I'm having trouble detecting the completed epic from {sprin
|
|||
<check if="{{epic_number}} still not determined">
|
||||
<action>PRIORITY 3: Fallback to stories folder</action>
|
||||
|
||||
<action>Scan {story_directory} for highest numbered story files</action>
|
||||
<action>Scan {implementation_artifacts} for highest numbered story files</action>
|
||||
<action>Extract epic numbers from story filenames (pattern: epic-X-Y-story-name.md)</action>
|
||||
<action>Set {{detected_epic}} = highest epic number found</action>
|
||||
|
||||
|
|
@ -170,7 +171,7 @@ Bob (Scrum Master): "Before we start the team discussion, let me review all the
|
|||
Charlie (Senior Dev): "Good idea - those dev notes always have gold in them."
|
||||
</output>
|
||||
|
||||
<action>For each story in epic {{epic_number}}, read the complete story file from {story_directory}/{{epic_number}}-{{story_num}}-\*.md</action>
|
||||
<action>For each story in epic {{epic_number}}, read the complete story file from {implementation_artifacts}/{{epic_number}}-{{story_num}}-*.md</action>
|
||||
|
||||
<action>Extract and analyze from each story:</action>
|
||||
|
||||
|
|
@ -261,14 +262,14 @@ Bob (Scrum Master): "We'll get to all of it. But first, let me load the previous
|
|||
<action>Calculate previous epic number: {{prev_epic_num}} = {{epic_number}} - 1</action>
|
||||
|
||||
<check if="{{prev_epic_num}} >= 1">
|
||||
<action>Search for previous retrospective using pattern: {retrospectives_folder}/epic-{{prev_epic_num}}-retro-*.md</action>
|
||||
<action>Search for previous retrospectives using pattern: {implementation_artifacts}/epic-{{prev_epic_num}}-retro-*.md</action>
|
||||
|
||||
<check if="previous retro found">
|
||||
<check if="previous retrospectives found">
|
||||
<output>
|
||||
Bob (Scrum Master): "I found our retrospective from Epic {{prev_epic_num}}. Let me see what we committed to back then..."
|
||||
Bob (Scrum Master): "I found our retrospectives from Epic {{prev_epic_num}}. Let me see what we committed to back then..."
|
||||
</output>
|
||||
|
||||
<action>Read the complete previous retrospective file</action>
|
||||
<action>Read the previous retrospectives</action>
|
||||
|
||||
<action>Extract key elements:</action>
|
||||
- **Action items committed**: What did the team agree to improve?
|
||||
|
|
@ -365,7 +366,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
|
|||
<action>Attempt to load next epic using selective loading strategy:</action>
|
||||
|
||||
**Try sharded first (more specific):**
|
||||
<action>Check if file exists: {planning_artifacts}/epic\*/epic-{{next_epic_num}}.md</action>
|
||||
<action>Check if file exists: {planning_artifacts}/epic*/epic-{{next_epic_num}}.md</action>
|
||||
|
||||
<check if="sharded epic file found">
|
||||
<action>Load {planning_artifacts}/*epic*/epic-{{next_epic_num}}.md</action>
|
||||
|
|
@ -374,7 +375,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
|
|||
|
||||
**Fallback to whole document:**
|
||||
<check if="sharded epic not found">
|
||||
<action>Check if file exists: {planning_artifacts}/epic\*.md</action>
|
||||
<action>Check if file exists: {planning_artifacts}/epic*.md</action>
|
||||
|
||||
<check if="whole epic file found">
|
||||
<action>Load entire epics document</action>
|
||||
|
|
@ -1302,7 +1303,7 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
|
|||
|
||||
<step n="11" goal="Save Retrospective and Update Sprint Status">
|
||||
|
||||
<action>Ensure retrospectives folder exists: {retrospectives_folder}</action>
|
||||
<action>Ensure retrospectives folder exists: {implementation_artifacts}</action>
|
||||
<action>Create folder if it doesn't exist</action>
|
||||
|
||||
<action>Generate comprehensive retrospective summary document including:</action>
|
||||
|
|
@ -1322,11 +1323,11 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
|
|||
- Commitments and next steps
|
||||
|
||||
<action>Format retrospective document as readable markdown with clear sections</action>
|
||||
<action>Set filename: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md</action>
|
||||
<action>Set filename: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md</action>
|
||||
<action>Save retrospective document</action>
|
||||
|
||||
<output>
|
||||
✅ Retrospective document saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
|
||||
✅ Retrospective document saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
|
||||
</output>
|
||||
|
||||
<action>Update {sprint_status_file} to mark retrospective as completed</action>
|
||||
|
|
@ -1365,7 +1366,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
|
|||
|
||||
- Epic {{epic_number}}: {{epic_title}} reviewed
|
||||
- Retrospective Status: completed
|
||||
- Retrospective saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
|
||||
- Retrospective saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
|
||||
|
||||
**Commitments Made:**
|
||||
|
||||
|
|
@ -1375,7 +1376,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
|
|||
|
||||
**Next Steps:**
|
||||
|
||||
1. **Review retrospective summary**: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
|
||||
1. **Review retrospective summary**: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
|
||||
|
||||
2. **Execute preparation sprint** (Est: {{prep_days}} days)
|
||||
- Complete {{critical_count}} critical path items
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ description: "Run after epic completion to review overall success, extract lesso
|
|||
author: "BMad"
|
||||
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:implementation_artifacts}"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
user_skill_level: "{config_source}:user_skill_level"
|
||||
|
|
@ -12,6 +11,7 @@ document_output_language: "{config_source}:document_output_language"
|
|||
date: system-generated
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
project_context: "**/project-context.md"
|
||||
|
||||
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/retrospective"
|
||||
template: false
|
||||
|
|
@ -51,5 +51,3 @@ input_file_patterns:
|
|||
|
||||
# Required files
|
||||
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||
story_directory: "{implementation_artifacts}"
|
||||
retrospectives_folder: "{implementation_artifacts}"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
<workflow>
|
||||
|
||||
<step n="1" goal="Parse epic files and extract all work items">
|
||||
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
|
||||
<action>Communicate in {communication_language} with {user_name}</action>
|
||||
<action>Look for all files matching `{epics_pattern}` in {epics_location}</action>
|
||||
<action>Could be a single `epics.md` file or multiple `epic-1.md`, `epic-2.md` files</action>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ communication_language: "{config_source}:communication_language"
|
|||
date: system-generated
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
output_folder: "{implementation_artifacts}"
|
||||
|
||||
# Workflow components
|
||||
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning"
|
||||
|
|
@ -18,24 +17,21 @@ template: "{installed_path}/sprint-status-template.yaml"
|
|||
validation: "{installed_path}/checklist.md"
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
# Project context
|
||||
project_context: "**/project-context.md"
|
||||
# Project identification
|
||||
project_name: "{config_source}:project_name"
|
||||
project_context: "**/project-context.md"
|
||||
project_name: "{config_source}:project_name"
|
||||
|
||||
# Tracking system configuration
|
||||
tracking_system: "file-system" # Options: file-system, Future will support other options from config of mcp such as jira, linear, trello
|
||||
project_key: "NOKEY" # Placeholder for tracker integrations; file-system uses a no-op key
|
||||
story_location: "{config_source}:implementation_artifacts" # Relative path for file-system, Future will support URL for Jira/Linear/Trello
|
||||
story_location_absolute: "{config_source}:implementation_artifacts" # Absolute path for file operations
|
||||
# Tracking system configuration
|
||||
tracking_system: "file-system" # Options: file-system, Future will support other options from config of mcp such as jira, linear, trello
|
||||
project_key: "NOKEY" # Placeholder for tracker integrations; file-system uses a no-op key
|
||||
story_location: "{implementation_artifacts}" # Relative path for file-system, Future will support URL for Jira/Linear/Trello
|
||||
story_location_absolute: "{implementation_artifacts}" # Absolute path for file operations
|
||||
|
||||
# Source files (file-system only)
|
||||
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
|
||||
epics_pattern: "epic*.md" # Pattern to find epic files
|
||||
# Source files (file-system only)
|
||||
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
|
||||
epics_pattern: "epic*.md" # Pattern to find epic files
|
||||
|
||||
# Output configuration
|
||||
status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||
# Output configuration
|
||||
status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||
|
||||
# Smart input file references - handles both whole docs and sharded docs
|
||||
# Priority: Whole document first, then sharded version
|
||||
|
|
@ -43,8 +39,8 @@ variables:
|
|||
input_file_patterns:
|
||||
epics:
|
||||
description: "All epics with user stories"
|
||||
whole: "{output_folder}/*epic*.md"
|
||||
sharded: "{output_folder}/*epic*/*.md"
|
||||
whole: "{planning_artifacts}/*epic*.md"
|
||||
sharded: "{planning_artifacts}/*epic*/*.md"
|
||||
load_strategy: "FULL_LOAD"
|
||||
|
||||
# Output configuration
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
</step>
|
||||
|
||||
<step n="1" goal="Locate sprint status file">
|
||||
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
|
||||
<action>Try {sprint_status_file}</action>
|
||||
<check if="file not found">
|
||||
<output>❌ sprint-status.yaml not found.
|
||||
|
|
|
|||
|
|
@ -5,22 +5,17 @@ author: "BMad"
|
|||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
document_output_language: "{config_source}:document_output_language"
|
||||
date: system-generated
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
planning_artifacts: "{config_source}:planning_artifacts"
|
||||
|
||||
# Workflow components
|
||||
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-status"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
|
||||
# Inputs
|
||||
variables:
|
||||
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||
tracking_system: "file-system"
|
||||
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||
|
||||
# Smart input file references
|
||||
input_file_patterns:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ This uses **step-file architecture** for focused execution:
|
|||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
|
||||
- `user_name`, `communication_language`, `user_skill_level`
|
||||
- `output_folder`, `planning_artifacts`, `implementation_artifacts`
|
||||
- `planning_artifacts`, `implementation_artifacts`
|
||||
- `date` as system-generated current datetime
|
||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ a) **Before asking detailed questions, do a rapid scan to understand the landsca
|
|||
|
||||
b) **Check for existing context docs:**
|
||||
|
||||
- Check `{output_folder}` and `{planning_artifacts}`for planning documents (PRD, architecture, epics, research)
|
||||
- Check `{implementation_artifacts}` and `{planning_artifacts}`for planning documents (PRD, architecture, epics, research)
|
||||
- Check for `**/project-context.md` - if it exists, skim for patterns and conventions
|
||||
- Check for any existing stories or specs related to user's request
|
||||
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ This uses **step-file architecture** for disciplined execution:
|
|||
|
||||
Load and read full config from `{main_config}` and resolve:
|
||||
|
||||
- `project_name`, `output_folder`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||
- `date` as system-generated current datetime
|
||||
- `project_context` = `**/project-context.md` (load if exists)
|
||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||
|
||||
### 2. First Step Execution
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<step n="2" goal="Check for resumability and determine workflow mode">
|
||||
<critical>SMART LOADING STRATEGY: Check state file FIRST before loading any CSV files</critical>
|
||||
|
||||
<action>Check for existing state file at: {output_folder}/project-scan-report.json</action>
|
||||
<action>Check for existing state file at: {project_knowledge}/project-scan-report.json</action>
|
||||
|
||||
<check if="project-scan-report.json exists">
|
||||
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
|
||||
|
|
@ -107,8 +107,8 @@ Your choice [1/2/3]:
|
|||
</check>
|
||||
|
||||
<check if="user selects 2">
|
||||
<action>Create archive directory: {output_folder}/.archive/</action>
|
||||
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
|
||||
<action>Create archive directory: {project_knowledge}/.archive/</action>
|
||||
<action>Move old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
|
||||
<action>Set resume_mode = false</action>
|
||||
<action>Continue to Step 0.5</action>
|
||||
</check>
|
||||
|
|
@ -120,7 +120,7 @@ Your choice [1/2/3]:
|
|||
|
||||
<check if="state file age >= 24 hours">
|
||||
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
|
||||
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
|
||||
<action>Archive old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
|
||||
<action>Set resume_mode = false</action>
|
||||
<action>Continue to Step 0.5</action>
|
||||
</check>
|
||||
|
|
@ -128,7 +128,7 @@ Your choice [1/2/3]:
|
|||
</step>
|
||||
|
||||
<step n="3" goal="Check for existing documentation and determine workflow mode" if="resume_mode == false">
|
||||
<action>Check if {output_folder}/index.md exists</action>
|
||||
<action>Check if {project_knowledge}/index.md exists</action>
|
||||
|
||||
<check if="index.md exists">
|
||||
<action>Read existing index.md to extract metadata (date, project structure, parts count)</action>
|
||||
|
|
@ -195,7 +195,7 @@ Your choice [1/2/3]:
|
|||
|
||||
- Mode: {{workflow_mode}}
|
||||
- Scan Level: {{scan_level}}
|
||||
- Output: {output_folder}/index.md and related files
|
||||
- Output: {project_knowledge}/index.md and related files
|
||||
|
||||
{{#if status_file_found}}
|
||||
**Status Updated:**
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@
|
|||
"type": "string",
|
||||
"description": "Absolute path to project root directory"
|
||||
},
|
||||
"output_folder": {
|
||||
"project_knowledge": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to output folder"
|
||||
"description": "Absolute path to project knowledge folder"
|
||||
},
|
||||
"completed_steps": {
|
||||
"type": "array",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ author: "BMad"
|
|||
|
||||
# Critical variables
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:project_knowledge"
|
||||
project_knowledge: "{config_source}:project_knowledge"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
document_output_language: "{config_source}:document_output_language"
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ This will read EVERY file in this area. Proceed? [y/n]
|
|||
|
||||
<action>Load complete deep-dive template from: {installed_path}/templates/deep-dive-template.md</action>
|
||||
<action>Fill template with all collected data from steps 13b-13d</action>
|
||||
<action>Write filled template to: {output_folder}/deep-dive-{{sanitized_target_name}}.md</action>
|
||||
<action>Write filled template to: {project_knowledge}/deep-dive-{{sanitized_target_name}}.md</action>
|
||||
<action>Validate deep-dive document completeness</action>
|
||||
|
||||
<template-output>deep_dive_documentation</template-output>
|
||||
|
|
@ -241,7 +241,7 @@ Detailed exhaustive analysis of specific areas:
|
|||
|
||||
## Deep-Dive Documentation Complete! ✓
|
||||
|
||||
**Generated:** {output_folder}/deep-dive-{{target_name}}.md
|
||||
**Generated:** {project_knowledge}/deep-dive-{{target_name}}.md
|
||||
**Files Analyzed:** {{file_count}}
|
||||
**Lines of Code Scanned:** {{total_loc}}
|
||||
**Time Taken:** ~{{duration}}
|
||||
|
|
@ -255,7 +255,7 @@ Detailed exhaustive analysis of specific areas:
|
|||
- Related code and reuse opportunities
|
||||
- Implementation guidance
|
||||
|
||||
**Index Updated:** {output_folder}/index.md now includes link to this deep-dive
|
||||
**Index Updated:** {project_knowledge}/index.md now includes link to this deep-dive
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
</action>
|
||||
|
|
@ -278,7 +278,7 @@ Your choice [1/2]:
|
|||
|
||||
All deep-dive documentation complete!
|
||||
|
||||
**Master Index:** {output_folder}/index.md
|
||||
**Master Index:** {project_knowledge}/index.md
|
||||
**Deep-Dives Generated:** {{deep_dive_count}}
|
||||
|
||||
These comprehensive docs are now ready for:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ parent_workflow: "{project-root}/_bmad/bmm/workflows/document-project/workflow.y
|
|||
|
||||
# Critical variables inherited from parent
|
||||
config_source: "{project-root}/_bmad/bmb/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
project_knowledge: "{config_source}:project_knowledge"
|
||||
user_name: "{config_source}:user_name"
|
||||
date: system-generated
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ This workflow uses a single comprehensive CSV file to intelligently document you
|
|||
</step>
|
||||
|
||||
<step n="0.6" goal="Check for existing documentation and determine workflow mode">
|
||||
<action>Check if {output_folder}/index.md exists</action>
|
||||
<action>Check if {project_knowledge}/index.md exists</action>
|
||||
|
||||
<check if="index.md exists">
|
||||
<action>Read existing index.md to extract metadata (date, project structure, parts count)</action>
|
||||
|
|
@ -127,7 +127,7 @@ Your choice [1/2/3] (default: 1):
|
|||
<action>Display: "Using Exhaustive Scan (reading all source files)"</action>
|
||||
</action>
|
||||
|
||||
<action>Initialize state file: {output_folder}/project-scan-report.json</action>
|
||||
<action>Initialize state file: {project_knowledge}/project-scan-report.json</action>
|
||||
<critical>Every time you touch the state file, record: step id, human-readable summary (what you actually did), precise timestamp, and any outputs written. Vague phrases are unacceptable.</critical>
|
||||
<action>Write initial state:
|
||||
{
|
||||
|
|
@ -136,7 +136,7 @@ Your choice [1/2/3] (default: 1):
|
|||
"mode": "{{workflow_mode}}",
|
||||
"scan_level": "{{scan_level}}",
|
||||
"project_root": "{{project_root_path}}",
|
||||
"output_folder": "{{output_folder}}",
|
||||
"project_knowledge": "{{project_knowledge}}",
|
||||
"completed_steps": [],
|
||||
"current_step": "step_1",
|
||||
"findings": {},
|
||||
|
|
@ -325,7 +325,7 @@ findings.batches_completed: [
|
|||
</check>
|
||||
|
||||
<action>Build API contracts catalog</action>
|
||||
<action>IMMEDIATELY write to: {output_folder}/api-contracts-{part_id}.md</action>
|
||||
<action>IMMEDIATELY write to: {project_knowledge}/api-contracts-{part_id}.md</action>
|
||||
<action>Validate document has all required sections</action>
|
||||
<action>Update state file with output generated</action>
|
||||
<action>PURGE detailed API data, keep only: "{{api_count}} endpoints documented"</action>
|
||||
|
|
@ -346,7 +346,7 @@ findings.batches_completed: [
|
|||
</check>
|
||||
|
||||
<action>Build database schema documentation</action>
|
||||
<action>IMMEDIATELY write to: {output_folder}/data-models-{part_id}.md</action>
|
||||
<action>IMMEDIATELY write to: {project_knowledge}/data-models-{part_id}.md</action>
|
||||
<action>Validate document completeness</action>
|
||||
<action>Update state file with output generated</action>
|
||||
<action>PURGE detailed schema data, keep only: "{{table_count}} tables documented"</action>
|
||||
|
|
@ -805,7 +805,7 @@ When a document SHOULD be generated but wasn't (due to quick scan, missing data,
|
|||
|
||||
<step n="11" goal="Validate and review generated documentation" if="workflow_mode != deep_dive">
|
||||
<action>Show summary of all generated files:
|
||||
Generated in {{output_folder}}/:
|
||||
Generated in {{project_knowledge}}/:
|
||||
{{file_list_with_sizes}}
|
||||
</action>
|
||||
|
||||
|
|
@ -823,7 +823,7 @@ Generated in {{output_folder}}/:
|
|||
3. Extract document metadata from each match for user selection
|
||||
</critical>
|
||||
|
||||
<action>Read {output_folder}/index.md</action>
|
||||
<action>Read {project_knowledge}/index.md</action>
|
||||
|
||||
<action>Scan for incomplete documentation markers:
|
||||
Step 1: Search for exact pattern "_(To be generated)_" (case-sensitive)
|
||||
|
|
@ -1065,9 +1065,9 @@ Enter number(s) separated by commas (e.g., "1,3,5"), or type 'all':
|
|||
|
||||
## Project Documentation Complete! ✓
|
||||
|
||||
**Location:** {{output_folder}}/
|
||||
**Location:** {{project_knowledge}}/
|
||||
|
||||
**Master Index:** {{output_folder}}/index.md
|
||||
**Master Index:** {{project_knowledge}}/index.md
|
||||
👆 This is your primary entry point for AI-assisted development
|
||||
|
||||
**Generated Documentation:**
|
||||
|
|
@ -1076,9 +1076,9 @@ Enter number(s) separated by commas (e.g., "1,3,5"), or type 'all':
|
|||
**Next Steps:**
|
||||
|
||||
1. Review the index.md to familiarize yourself with the documentation structure
|
||||
2. When creating a brownfield PRD, point the PRD workflow to: {{output_folder}}/index.md
|
||||
3. For UI-only features: Reference {{output_folder}}/architecture-{{ui_part_id}}.md
|
||||
4. For API-only features: Reference {{output_folder}}/architecture-{{api_part_id}}.md
|
||||
2. When creating a brownfield PRD, point the PRD workflow to: {{project_knowledge}}/index.md
|
||||
3. For UI-only features: Reference {{project_knowledge}}/architecture-{{ui_part_id}}.md
|
||||
4. For API-only features: Reference {{project_knowledge}}/architecture-{{api_part_id}}.md
|
||||
5. For full-stack features: Reference both part architectures + integration-architecture.md
|
||||
|
||||
**Verification Recap:**
|
||||
|
|
@ -1101,6 +1101,6 @@ When ready to plan new features, run the PRD workflow and provide this index as
|
|||
- Write final state file
|
||||
</action>
|
||||
|
||||
<action>Display: "State file saved: {{output_folder}}/project-scan-report.json"</action>
|
||||
<action>Display: "State file saved: {{project_knowledge}}/project-scan-report.json"</action>
|
||||
|
||||
</workflow>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ parent_workflow: "{project-root}/_bmad/bmm/workflows/document-project/workflow.y
|
|||
|
||||
# Critical variables inherited from parent
|
||||
config_source: "{project-root}/_bmad/bmb/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
project_knowledge: "{config_source}:project_knowledge"
|
||||
user_name: "{config_source}:user_name"
|
||||
date: system-generated
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ author: "BMad"
|
|||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
implementation_artifacts: "{config_source}:implementation_artifacts"
|
||||
user_name: "{config_source}:user_name"
|
||||
communication_language: "{config_source}:communication_language"
|
||||
|
|
@ -19,10 +18,8 @@ validation: "{installed_path}/checklist.md"
|
|||
template: false
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
# Directory paths
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
source_dir: "{project-root}" # Source code directory
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
source_dir: "{project-root}" # Source code directory
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{implementation_artifacts}/tests/test-summary.md"
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Core Module Installer
|
||||
* Standard module installer function that executes after IDE installations
|
||||
*
|
||||
* @param {Object} options - Installation options
|
||||
* @param {string} options.projectRoot - The root directory of the target project
|
||||
* @param {Object} options.config - Module configuration from module.yaml
|
||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||
* @param {Object} options.logger - Logger instance for output
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async function install(options) {
|
||||
const { projectRoot, config, installedIDEs, logger } = options;
|
||||
|
||||
try {
|
||||
logger.log(chalk.blue('🏗️ Installing Core Module...'));
|
||||
|
||||
// Core agent configs are created by the main installer's createAgentConfigs method
|
||||
// No need to create them here - they'll be handled along with all other agents
|
||||
|
||||
// Handle IDE-specific configurations if needed
|
||||
if (installedIDEs && installedIDEs.length > 0) {
|
||||
logger.log(chalk.cyan(`Configuring Core for IDEs: ${installedIDEs.join(', ')}`));
|
||||
|
||||
// Add any IDE-specific Core configurations here
|
||||
for (const ide of installedIDEs) {
|
||||
await configureForIDE(ide, projectRoot, config, logger);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(chalk.green('✓ Core Module installation complete'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(chalk.red(`Error installing Core module: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Core module for specific IDE
|
||||
* @private
|
||||
*/
|
||||
async function configureForIDE(ide) {
|
||||
// Add IDE-specific configurations here
|
||||
switch (ide) {
|
||||
case 'claude-code': {
|
||||
// Claude Code specific Core configurations
|
||||
break;
|
||||
}
|
||||
// Add more IDEs as needed
|
||||
default: {
|
||||
// No specific configuration needed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { install };
|
||||
|
|
@ -7,6 +7,7 @@ agent:
|
|||
name: "BMad Master"
|
||||
title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
||||
icon: "🧙"
|
||||
capabilities: "runtime resource management, workflow orchestration, task execution, knowledge custodian"
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
|
|
@ -14,7 +15,7 @@ agent:
|
|||
identity: "Master-level expert in the BMAD Core Platform and all loaded modules with comprehensive knowledge of all resources, tasks, and workflows. Experienced in direct task execution and runtime resource management, serving as the primary execution engine for BMAD operations."
|
||||
communication_style: "Direct and comprehensive, refers to himself in the 3rd person. Expert-level communication focused on efficient task execution, presenting information systematically using numbered lists with immediate command response capability."
|
||||
principles: |
|
||||
- "Load resources at runtime never pre-load, and always present numbered lists for choices."
|
||||
- Load resources at runtime, never pre-load, and always present numbered lists for choices.
|
||||
|
||||
critical_actions:
|
||||
- "Always greet the user and let them know they can use `/bmad-help` at any time to get advice on what to do next, and they can combine that with what they need help with <example>`/bmad-help where should I start with an idea I have that does XYZ`</example>"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ module.exports = {
|
|||
if (config.actionType === 'cancel') {
|
||||
await prompts.log.warn('Installation cancelled.');
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle quick update separately
|
||||
|
|
@ -47,23 +46,14 @@ module.exports = {
|
|||
const result = await installer.quickUpdate(config);
|
||||
await prompts.log.success('Quick update complete!');
|
||||
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
|
||||
|
||||
// Display version-specific end message
|
||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
||||
const messageLoader = new MessageLoader();
|
||||
await messageLoader.displayEndMessage();
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle compile agents separately
|
||||
if (config.actionType === 'compile-agents') {
|
||||
const result = await installer.compileAgents(config);
|
||||
await prompts.log.success('Agent recompilation complete!');
|
||||
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular install/update flow
|
||||
|
|
@ -72,16 +62,10 @@ module.exports = {
|
|||
// Check if installation was cancelled
|
||||
if (result && result.cancelled) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if installation succeeded
|
||||
if (result && result.success) {
|
||||
// Display version-specific end message from install-messages.yaml
|
||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
||||
const messageLoader = new MessageLoader();
|
||||
await messageLoader.displayEndMessage();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -42,13 +42,12 @@ modules:
|
|||
type: bmad-org
|
||||
npmPackage: bmad-method-test-architecture-enterprise
|
||||
|
||||
# TODO: Enable once fixes applied:
|
||||
|
||||
# whiteport-design-system:
|
||||
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
||||
# module-definition: src/module.yaml
|
||||
# code: WDS
|
||||
# name: "Whiteport UX Design System"
|
||||
# description: "UX design framework with Figma integration"
|
||||
# defaultSelected: false
|
||||
# type: community
|
||||
# whiteport-design-system:
|
||||
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
||||
# module-definition: src/module.yaml
|
||||
# code: wds
|
||||
# name: "Whiteport UX Design System"
|
||||
# description: "UX design framework with Figma integration"
|
||||
# defaultSelected: false
|
||||
# type: community
|
||||
# npmPackage: bmad-method-wds-expansion
|
||||
|
|
|
|||
|
|
@ -14,28 +14,10 @@ startMessage: |
|
|||
but anticipate no massive breaking changes
|
||||
- Groundwork in place for customization and community modules
|
||||
|
||||
📚 New Docs Site: http://docs.bmad-method.org/
|
||||
- High quality tutorials, guided walkthrough, and articles coming soon!
|
||||
- Everything is free. No paywalls. No gated content.
|
||||
- Knowledge should be shared, not sold.
|
||||
|
||||
💡 Love BMad? Please star us on GitHub & subscribe on YouTube!
|
||||
- GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||
- YouTube: https://www.youtube.com/@BMadCode
|
||||
|
||||
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# Display at the END of installation (after all setup completes)
|
||||
endMessage: |
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✨ BMAD V6 BETA IS INSTALLED! Thank you for being part of this journey!
|
||||
|
||||
🌟 BMad is 100% free and open source.
|
||||
- No gated Discord. No paywalls.
|
||||
- No gated Discord. No paywalls. No gated content.
|
||||
- We believe in empowering everyone, not just those who can pay.
|
||||
- Knowledge should be shared, not sold.
|
||||
|
||||
🙏 SUPPORT BMAD DEVELOPMENT:
|
||||
- During the Beta, please give us feedback and raise issues on GitHub!
|
||||
|
|
@ -47,13 +29,14 @@ endMessage: |
|
|||
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
||||
|
||||
📚 RESOURCES:
|
||||
- Docs: http://docs.bmad-method.org/ (bookmark it!)
|
||||
- Changelog: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
||||
|
||||
⭐⭐⭐ HELP US GROW:
|
||||
⭐ HELP US GROW:
|
||||
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
||||
- Every star & sub helps us reach more developers!
|
||||
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# No end message - install summary and next steps are rendered by the installer
|
||||
endMessage: ""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ class ConfigCollector {
|
|||
this.collectedConfig = {};
|
||||
this.existingConfig = null;
|
||||
this.currentProjectDir = null;
|
||||
this._moduleManagerInstance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a cached ModuleManager instance (lazy initialization)
|
||||
* @returns {Object} ModuleManager instance
|
||||
*/
|
||||
_getModuleManager() {
|
||||
if (!this._moduleManagerInstance) {
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
this._moduleManagerInstance = new ModuleManager();
|
||||
}
|
||||
return this._moduleManagerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,6 +142,70 @@ class ConfigCollector {
|
|||
return foundAny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-scan module schemas to gather metadata for the configuration gateway prompt.
|
||||
* Returns info about which modules have configurable options.
|
||||
* @param {Array} modules - List of non-core module names
|
||||
* @returns {Promise<Array>} Array of {moduleName, displayName, questionCount, hasFieldsWithoutDefaults}
|
||||
*/
|
||||
async scanModuleSchemas(modules) {
|
||||
const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
|
||||
const results = [];
|
||||
|
||||
for (const moduleName of modules) {
|
||||
// Resolve module.yaml path - custom paths first, then standard location, then ModuleManager search
|
||||
let moduleConfigPath = null;
|
||||
const customPath = this.customModulePaths?.get(moduleName);
|
||||
if (customPath) {
|
||||
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||
} else {
|
||||
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
if (await fs.pathExists(standardPath)) {
|
||||
moduleConfigPath = standardPath;
|
||||
} else {
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
if (moduleSourcePath) {
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!moduleConfigPath || !(await fs.pathExists(moduleConfigPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const moduleConfig = yaml.parse(content);
|
||||
if (!moduleConfig) continue;
|
||||
|
||||
const displayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
|
||||
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
|
||||
const questionKeys = configKeys.filter((key) => {
|
||||
if (metadataFields.has(key)) return false;
|
||||
const item = moduleConfig[key];
|
||||
return item && typeof item === 'object' && item.prompt;
|
||||
});
|
||||
|
||||
const hasFieldsWithoutDefaults = questionKeys.some((key) => {
|
||||
const item = moduleConfig[key];
|
||||
return item.default === undefined || item.default === null || item.default === '';
|
||||
});
|
||||
|
||||
results.push({
|
||||
moduleName,
|
||||
displayName,
|
||||
questionCount: questionKeys.length,
|
||||
hasFieldsWithoutDefaults,
|
||||
});
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Could not read schema for module "${moduleName}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration for all modules
|
||||
* @param {Array} modules - List of modules to configure (including 'core')
|
||||
|
|
@ -141,6 +218,7 @@ class ConfigCollector {
|
|||
// Store custom module paths for use in collectModuleConfig
|
||||
this.customModulePaths = options.customModulePaths || new Map();
|
||||
this.skipPrompts = options.skipPrompts || false;
|
||||
this.modulesToCustomize = undefined;
|
||||
await this.loadExistingConfig(projectDir);
|
||||
|
||||
// Check if core was already collected (e.g., in early collection phase)
|
||||
|
|
@ -154,10 +232,95 @@ class ConfigCollector {
|
|||
this.allAnswers = {};
|
||||
}
|
||||
|
||||
for (const moduleName of allModules) {
|
||||
// Split processing: core first, then gateway, then remaining modules
|
||||
const coreModules = allModules.filter((m) => m === 'core');
|
||||
const nonCoreModules = allModules.filter((m) => m !== 'core');
|
||||
|
||||
// Collect core config first (always fully prompted)
|
||||
for (const moduleName of coreModules) {
|
||||
await this.collectModuleConfig(moduleName, projectDir);
|
||||
}
|
||||
|
||||
// Show batch configuration gateway for non-core modules
|
||||
// Scan all non-core module schemas for display names and config metadata
|
||||
let scannedModules = [];
|
||||
if (!this.skipPrompts && nonCoreModules.length > 0) {
|
||||
scannedModules = await this.scanModuleSchemas(nonCoreModules);
|
||||
const customizableModules = scannedModules.filter((m) => m.questionCount > 0);
|
||||
|
||||
if (customizableModules.length > 0) {
|
||||
const configMode = await prompts.select({
|
||||
message: 'Module configuration',
|
||||
choices: [
|
||||
{ name: 'Express Setup', value: 'express', hint: 'accept all defaults (recommended)' },
|
||||
{ name: 'Customize', value: 'customize', hint: 'choose modules to configure' },
|
||||
],
|
||||
default: 'express',
|
||||
});
|
||||
|
||||
if (configMode === 'customize') {
|
||||
const choices = customizableModules.map((m) => ({
|
||||
name: `${m.displayName} (${m.questionCount} option${m.questionCount === 1 ? '' : 's'})`,
|
||||
value: m.moduleName,
|
||||
hint: m.hasFieldsWithoutDefaults ? 'has fields without defaults' : undefined,
|
||||
checked: m.hasFieldsWithoutDefaults,
|
||||
}));
|
||||
const selected = await prompts.multiselect({
|
||||
message: 'Select modules to customize:',
|
||||
choices,
|
||||
required: false,
|
||||
});
|
||||
this.modulesToCustomize = new Set(selected);
|
||||
} else {
|
||||
// Express mode: no modules to customize
|
||||
this.modulesToCustomize = new Set();
|
||||
}
|
||||
} else {
|
||||
// All non-core modules have zero config - no gateway needed
|
||||
this.modulesToCustomize = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
// Collect remaining non-core modules
|
||||
if (this.modulesToCustomize === undefined) {
|
||||
// No gateway was shown (skipPrompts, no non-core modules, or direct call) - process all normally
|
||||
for (const moduleName of nonCoreModules) {
|
||||
await this.collectModuleConfig(moduleName, projectDir);
|
||||
}
|
||||
} else {
|
||||
// Split into default modules (tasks progress) and customized modules (interactive)
|
||||
const defaultModules = nonCoreModules.filter((m) => !this.modulesToCustomize.has(m));
|
||||
const customizeModules = nonCoreModules.filter((m) => this.modulesToCustomize.has(m));
|
||||
|
||||
// Run default modules with a single spinner
|
||||
if (defaultModules.length > 0) {
|
||||
// Build display name map from all scanned modules for pre-call spinner messages
|
||||
const displayNameMap = new Map();
|
||||
for (const m of scannedModules) {
|
||||
displayNameMap.set(m.moduleName, m.displayName);
|
||||
}
|
||||
|
||||
const configSpinner = await prompts.spinner();
|
||||
configSpinner.start('Configuring modules...');
|
||||
for (const moduleName of defaultModules) {
|
||||
const displayName = displayNameMap.get(moduleName) || moduleName.toUpperCase();
|
||||
configSpinner.message(`Configuring ${displayName}...`);
|
||||
try {
|
||||
this._silentConfig = true;
|
||||
await this.collectModuleConfig(moduleName, projectDir);
|
||||
} finally {
|
||||
this._silentConfig = false;
|
||||
}
|
||||
}
|
||||
configSpinner.stop('Module configuration complete');
|
||||
}
|
||||
|
||||
// Run customized modules individually (may show interactive prompts)
|
||||
for (const moduleName of customizeModules) {
|
||||
await this.collectModuleConfig(moduleName, projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
this.collectedConfig._meta = {
|
||||
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
||||
|
|
@ -188,20 +351,15 @@ class ConfigCollector {
|
|||
this.allAnswers = {};
|
||||
}
|
||||
|
||||
// Load module's install config schema
|
||||
// Load module's config schema from module.yaml
|
||||
// First, try the standard src/modules location
|
||||
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
|
||||
// If not found in src/modules, we need to find it by searching the project
|
||||
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||
// Use the module manager to find the module source
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
if (!(await fs.pathExists(moduleConfigPath))) {
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
}
|
||||
}
|
||||
|
|
@ -211,19 +369,14 @@ class ConfigCollector {
|
|||
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
configPath = moduleConfigPath;
|
||||
} else if (await fs.pathExists(installerConfigPath)) {
|
||||
configPath = installerConfigPath;
|
||||
} else {
|
||||
// Check if this is a custom module with custom.yaml
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
|
||||
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
|
||||
|
||||
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) {
|
||||
if (await fs.pathExists(rootCustomConfigPath)) {
|
||||
isCustomModule = true;
|
||||
// For custom modules, we don't have an install-config schema, so just use existing values
|
||||
// The custom.yaml values will be loaded and merged during installation
|
||||
|
|
@ -500,28 +653,21 @@ class ConfigCollector {
|
|||
}
|
||||
// Load module's config
|
||||
// First, check if we have a custom module path for this module
|
||||
let installerConfigPath = null;
|
||||
let moduleConfigPath = null;
|
||||
|
||||
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
|
||||
const customPath = this.customModulePaths.get(moduleName);
|
||||
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||
} else {
|
||||
// Try the standard src/modules location
|
||||
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
}
|
||||
|
||||
// If not found in src/modules or custom paths, search the project
|
||||
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||
// Use the module manager to find the module source
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
if (!(await fs.pathExists(moduleConfigPath))) {
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
}
|
||||
}
|
||||
|
|
@ -529,8 +675,6 @@ class ConfigCollector {
|
|||
let configPath = null;
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
configPath = moduleConfigPath;
|
||||
} else if (await fs.pathExists(installerConfigPath)) {
|
||||
configPath = installerConfigPath;
|
||||
} else {
|
||||
// No config for this module
|
||||
return;
|
||||
|
|
@ -590,12 +734,12 @@ class ConfigCollector {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
let customize = true;
|
||||
if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
|
||||
let useDefaults = true;
|
||||
if (moduleName === 'core') {
|
||||
// Core module: no confirm prompt, continues directly
|
||||
} else {
|
||||
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
|
||||
useDefaults = false; // Core: always show all questions
|
||||
} else if (this.modulesToCustomize === undefined) {
|
||||
// Fallback: original per-module confirm (backward compat for direct calls)
|
||||
const customizeAnswer = await prompts.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
|
|
@ -604,10 +748,13 @@ class ConfigCollector {
|
|||
default: true,
|
||||
},
|
||||
]);
|
||||
customize = customizeAnswer.customize;
|
||||
useDefaults = customizeAnswer.customize;
|
||||
} else {
|
||||
// Batch mode: use defaults unless module was selected for customization
|
||||
useDefaults = !this.modulesToCustomize.has(moduleName);
|
||||
}
|
||||
|
||||
if (customize && moduleName !== 'core') {
|
||||
if (useDefaults && moduleName !== 'core') {
|
||||
// Accept defaults - only ask questions that have NO default value
|
||||
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
|
||||
|
||||
|
|
@ -737,16 +884,18 @@ class ConfigCollector {
|
|||
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
|
||||
const hasNoConfig = actualConfigKeys.length === 0;
|
||||
|
||||
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
if (moduleConfig.subheader) {
|
||||
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
||||
if (!this._silentConfig) {
|
||||
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
if (moduleConfig.subheader) {
|
||||
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
||||
} else {
|
||||
await prompts.log.message(` \u2713 No custom configuration required`);
|
||||
}
|
||||
} else {
|
||||
await prompts.log.message(` \u2713 No custom configuration required`);
|
||||
// Module has config but just no questions to ask
|
||||
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
||||
}
|
||||
} else {
|
||||
// Module has config but just no questions to ask
|
||||
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
class CustomModuleCache {
|
||||
constructor(bmadDir) {
|
||||
|
|
@ -195,7 +196,7 @@ class CustomModuleCache {
|
|||
// Verify cache integrity
|
||||
const currentCacheHash = await this.calculateHash(cacheDir);
|
||||
if (currentCacheHash !== cached.cacheHash) {
|
||||
console.warn(`Warning: Cache integrity check failed for ${moduleId}`);
|
||||
await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Manages IDE configuration persistence
|
||||
|
|
@ -93,7 +94,7 @@ class IdeConfigManager {
|
|||
const config = yaml.parse(content);
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Failed to load IDE config for ${ideName}:`, error.message);
|
||||
await prompts.log.warn(`Failed to load IDE config for ${ideName}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +124,7 @@ class IdeConfigManager {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Warning: Failed to load IDE configs:', error.message);
|
||||
await prompts.log.warn(`Failed to load IDE configs: ${error.message}`);
|
||||
}
|
||||
|
||||
return configs;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@ const yaml = require('yaml');
|
|||
const crypto = require('node:crypto');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
// Load package.json for version info
|
||||
const packageJson = require('../../../../../package.json');
|
||||
|
|
@ -241,7 +242,7 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Failed to parse workflow at ${fullPath}: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to parse workflow at ${fullPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -321,6 +322,7 @@ class ManifestGenerator {
|
|||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
||||
const capabilitiesMatch = content.match(/capabilities="([^"]+)"/);
|
||||
|
||||
// Extract persona fields
|
||||
const roleMatch = content.match(/<role>([^<]+)<\/role>/);
|
||||
|
|
@ -342,6 +344,7 @@ class ManifestGenerator {
|
|||
displayName: nameMatch ? nameMatch[1] : agentName,
|
||||
title: titleMatch ? titleMatch[1] : '',
|
||||
icon: iconMatch ? iconMatch[1] : '',
|
||||
capabilities: capabilitiesMatch ? this.cleanForCSV(capabilitiesMatch[1]) : '',
|
||||
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
||||
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
||||
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
||||
|
|
@ -691,7 +694,7 @@ class ManifestGenerator {
|
|||
|
||||
return preservedRows;
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Failed to read existing CSV ${csvPath}:`, error.message);
|
||||
await prompts.log.warn(`Failed to read existing CSV ${csvPath}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -784,7 +787,7 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
// Create CSV header with persona fields
|
||||
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
||||
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
|
||||
|
||||
// Combine existing and new agents, preferring new data for duplicates
|
||||
const allAgents = new Map();
|
||||
|
|
@ -802,6 +805,7 @@ class ManifestGenerator {
|
|||
displayName: agent.displayName,
|
||||
title: agent.title,
|
||||
icon: agent.icon,
|
||||
capabilities: agent.capabilities,
|
||||
role: agent.role,
|
||||
identity: agent.identity,
|
||||
communicationStyle: agent.communicationStyle,
|
||||
|
|
@ -818,6 +822,7 @@ class ManifestGenerator {
|
|||
escapeCsv(record.displayName),
|
||||
escapeCsv(record.title),
|
||||
escapeCsv(record.icon),
|
||||
escapeCsv(record.capabilities),
|
||||
escapeCsv(record.role),
|
||||
escapeCsv(record.identity),
|
||||
escapeCsv(record.communicationStyle),
|
||||
|
|
@ -1068,7 +1073,7 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not scan for installed modules: ${error.message}`);
|
||||
await prompts.log.warn(`Could not scan for installed modules: ${error.message}`);
|
||||
}
|
||||
|
||||
return modules;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
|||
const fs = require('fs-extra');
|
||||
const crypto = require('node:crypto');
|
||||
const { getProjectRoot } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
class Manifest {
|
||||
/**
|
||||
|
|
@ -100,7 +101,7 @@ class Manifest {
|
|||
ides: manifestData.ides || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to read YAML manifest:', error.message);
|
||||
await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +231,7 @@ class Manifest {
|
|||
const content = await fs.readFile(yamlPath, 'utf8');
|
||||
return yaml.parse(content);
|
||||
} catch (error) {
|
||||
console.error('Failed to read YAML manifest:', error.message);
|
||||
await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -472,7 +473,7 @@ class Manifest {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not parse ${filePath}:`, error.message);
|
||||
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// Handle other file types (CSV, JSON, YAML, etc.)
|
||||
|
|
@ -774,7 +775,7 @@ class Manifest {
|
|||
configs[moduleName] = yaml.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not load config for module ${moduleName}:`, error.message);
|
||||
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -876,7 +877,7 @@ class Manifest {
|
|||
const pkg = require(packageJsonPath);
|
||||
version = pkg.version;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -904,7 +905,7 @@ class Manifest {
|
|||
repoUrl: moduleConfig.repoUrl || null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class CustomHandler {
|
|||
// Found a custom.yaml file
|
||||
customPaths.push(fullPath);
|
||||
} else if (
|
||||
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
|
||||
entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
|
||||
// Skip if it's in src/modules (those are standard modules)
|
||||
!fullPath.includes(path.join('src', 'modules'))
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ class CodexSetup extends BaseIdeSetup {
|
|||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
// Non-interactive mode: use default (global)
|
||||
if (options.skipPrompts) {
|
||||
return { installLocation: 'global' };
|
||||
}
|
||||
|
||||
let confirmed = false;
|
||||
let installLocation = 'global';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,655 @@
|
|||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
const csv = require('csv-parse/sync');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* GitHub Copilot setup handler
|
||||
* Creates agents in .github/agents/, prompts in .github/prompts/,
|
||||
* copilot-instructions.md, and configures VS Code settings
|
||||
*/
|
||||
class GitHubCopilotSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('github-copilot', 'GitHub Copilot', false);
|
||||
// Don't set configDir to '.github' — nearly every GitHub repo has that directory,
|
||||
// which would cause the base detect() to false-positive. Use detectionPaths instead.
|
||||
this.configDir = null;
|
||||
this.githubDir = '.github';
|
||||
this.agentsDir = 'agents';
|
||||
this.promptsDir = 'prompts';
|
||||
this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup GitHub Copilot configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Create .github/agents and .github/prompts directories
|
||||
const githubDir = path.join(projectDir, this.githubDir);
|
||||
const agentsDir = path.join(githubDir, this.agentsDir);
|
||||
const promptsDir = path.join(githubDir, this.promptsDir);
|
||||
await this.ensureDir(agentsDir);
|
||||
await this.ensureDir(promptsDir);
|
||||
|
||||
// Preserve any customised tool permissions from existing files before cleanup
|
||||
this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Load agent manifest for enriched descriptions
|
||||
const agentManifest = await this.loadAgentManifest(bmadDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Create agent .agent.md files
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const agentMeta = agentManifest.get(artifact.name);
|
||||
|
||||
// Compute fileName first so we can look up any existing tool permissions
|
||||
const dashName = toDashPath(artifact.relativePath);
|
||||
const fileName = dashName.replace(/\.md$/, '.agent.md');
|
||||
const toolsStr = this.getToolsForFile(fileName);
|
||||
const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
|
||||
const targetPath = path.join(agentsDir, fileName);
|
||||
await this.writeFile(targetPath, agentContent);
|
||||
agentCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Created agent: ${fileName}`));
|
||||
}
|
||||
|
||||
// Generate prompt files from bmad-help.csv
|
||||
const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest);
|
||||
|
||||
// Generate copilot-instructions.md
|
||||
await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest);
|
||||
|
||||
console.log(chalk.green(`\n✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents created in .github/agents/`));
|
||||
console.log(chalk.dim(` - ${promptCount} prompts created in .github/prompts/`));
|
||||
console.log(chalk.dim(` - copilot-instructions.md generated`));
|
||||
console.log(chalk.dim(` - Destination: .github/`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results: {
|
||||
agents: agentCount,
|
||||
workflows: promptCount,
|
||||
tasks: 0,
|
||||
tools: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load agent manifest CSV into a Map keyed by agent name
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @returns {Map} Agent metadata keyed by name
|
||||
*/
|
||||
async loadAgentManifest(bmadDir) {
|
||||
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
||||
const agents = new Map();
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return agents;
|
||||
}
|
||||
|
||||
try {
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const records = csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
for (const record of records) {
|
||||
agents.set(record.name, record);
|
||||
}
|
||||
} catch {
|
||||
// Gracefully degrade if manifest is unreadable/malformed
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bmad-help.csv to drive prompt generation
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @returns {Array|null} Parsed CSV rows
|
||||
*/
|
||||
async loadBmadHelp(bmadDir) {
|
||||
const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv');
|
||||
|
||||
if (!(await fs.pathExists(helpPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const csvContent = await fs.readFile(helpPath, 'utf8');
|
||||
return csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
} catch {
|
||||
// Gracefully degrade if help CSV is unreadable/malformed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent .agent.md content with enriched description
|
||||
* @param {Object} artifact - Agent artifact from AgentCommandGenerator
|
||||
* @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
|
||||
* @returns {string} Agent file content
|
||||
*/
|
||||
createAgentContent(artifact, manifestEntry, toolsStr) {
|
||||
// Build enriched description from manifest metadata
|
||||
let description;
|
||||
if (manifestEntry) {
|
||||
const persona = manifestEntry.displayName || artifact.name;
|
||||
const title = manifestEntry.title || this.formatTitle(artifact.name);
|
||||
const capabilities = manifestEntry.capabilities || 'agent capabilities';
|
||||
description = `${persona} — ${title}: ${capabilities}`;
|
||||
} else {
|
||||
description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
|
||||
}
|
||||
|
||||
// Build the agent file path for the activation block
|
||||
const agentPath = artifact.agentPath || artifact.relativePath;
|
||||
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
|
||||
|
||||
return `---
|
||||
description: '${description.replaceAll("'", "''")}'
|
||||
tools: ${toolsStr}
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from ${agentFilePath}
|
||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||
3. FOLLOW every step in the <activation> section precisely
|
||||
4. DISPLAY the welcome/greeting as instructed
|
||||
5. PRESENT the numbered menu
|
||||
6. WAIT for user input before proceeding
|
||||
</agent-activation>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array} agentArtifacts - Agent artifacts for activator generation
|
||||
* @param {Map} agentManifest - Agent manifest data
|
||||
* @returns {number} Count of prompts generated
|
||||
*/
|
||||
async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) {
|
||||
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
|
||||
let promptCount = 0;
|
||||
|
||||
// Load bmad-help.csv to drive workflow/task prompt generation
|
||||
const helpEntries = await this.loadBmadHelp(bmadDir);
|
||||
|
||||
if (helpEntries) {
|
||||
for (const entry of helpEntries) {
|
||||
const command = entry.command;
|
||||
if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
|
||||
|
||||
const workflowFile = entry['workflow-file'];
|
||||
if (!workflowFile) continue; // Skip entries with no workflow file path
|
||||
const promptFileName = `${command}.prompt.md`;
|
||||
const toolsStr = this.getToolsForFile(promptFileName);
|
||||
const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
|
||||
const promptPath = path.join(promptsDir, promptFileName);
|
||||
await this.writeFile(promptPath, promptContent);
|
||||
promptCount++;
|
||||
}
|
||||
|
||||
// Generate tech-writer command prompts (entries with no command column)
|
||||
for (const entry of helpEntries) {
|
||||
if (entry.command) continue; // Already handled above
|
||||
const techWriterPrompt = this.createTechWriterPromptContent(entry);
|
||||
if (techWriterPrompt) {
|
||||
const promptFileName = `${techWriterPrompt.fileName}.prompt.md`;
|
||||
const promptPath = path.join(promptsDir, promptFileName);
|
||||
await this.writeFile(promptPath, techWriterPrompt.content);
|
||||
promptCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate agent activator prompts (Pattern D)
|
||||
for (const artifact of agentArtifacts) {
|
||||
const agentMeta = agentManifest.get(artifact.name);
|
||||
const fileName = `bmad-${artifact.name}.prompt.md`;
|
||||
const toolsStr = this.getToolsForFile(fileName);
|
||||
const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
|
||||
const promptPath = path.join(promptsDir, fileName);
|
||||
await this.writeFile(promptPath, promptContent);
|
||||
promptCount++;
|
||||
}
|
||||
|
||||
return promptCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prompt content for a workflow/task entry from bmad-help.csv
|
||||
* Determines the pattern (A, B, or A for .xml tasks) based on file extension
|
||||
* @param {Object} entry - bmad-help.csv row
|
||||
* @param {string} workflowFile - Workflow file path
|
||||
* @returns {string} Prompt file content
|
||||
*/
|
||||
createWorkflowPromptContent(entry, workflowFile, toolsStr) {
|
||||
const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
|
||||
// bmm/config.yaml is safe to hardcode here: these prompts are only generated when
|
||||
// bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed.
|
||||
const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`;
|
||||
|
||||
let body;
|
||||
if (workflowFile.endsWith('.yaml')) {
|
||||
// Pattern B: YAML-based workflows — use workflow engine
|
||||
body = `${configLine}
|
||||
2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
|
||||
3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`;
|
||||
} else if (workflowFile.endsWith('.xml')) {
|
||||
// Pattern A variant: XML tasks — load and execute directly
|
||||
body = `${configLine}
|
||||
2. Load and execute the task at {project-root}/${workflowFile}`;
|
||||
} else {
|
||||
// Pattern A: MD workflows — load and follow directly
|
||||
body = `${configLine}
|
||||
2. Load and follow the workflow at {project-root}/${workflowFile}`;
|
||||
}
|
||||
|
||||
return `---
|
||||
description: '${description}'
|
||||
agent: 'agent'
|
||||
tools: ${toolsStr}
|
||||
---
|
||||
|
||||
${body}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a short 2-5 word description for a prompt from the entry name
|
||||
* @param {string} name - Entry name from bmad-help.csv
|
||||
* @returns {string} Short description
|
||||
*/
|
||||
createPromptDescription(name) {
|
||||
const descriptionMap = {
|
||||
'Brainstorm Project': 'Brainstorm ideas',
|
||||
'Market Research': 'Market research',
|
||||
'Domain Research': 'Domain research',
|
||||
'Technical Research': 'Technical research',
|
||||
'Create Brief': 'Create product brief',
|
||||
'Create PRD': 'Create PRD',
|
||||
'Validate PRD': 'Validate PRD',
|
||||
'Edit PRD': 'Edit PRD',
|
||||
'Create UX': 'Create UX design',
|
||||
'Create Architecture': 'Create architecture',
|
||||
'Create Epics and Stories': 'Create epics and stories',
|
||||
'Check Implementation Readiness': 'Check implementation readiness',
|
||||
'Sprint Planning': 'Sprint planning',
|
||||
'Sprint Status': 'Sprint status',
|
||||
'Create Story': 'Create story',
|
||||
'Validate Story': 'Validate story',
|
||||
'Dev Story': 'Dev story',
|
||||
'QA Automation Test': 'QA automation',
|
||||
'Code Review': 'Code review',
|
||||
Retrospective: 'Retrospective',
|
||||
'Document Project': 'Document project',
|
||||
'Generate Project Context': 'Generate project context',
|
||||
'Quick Spec': 'Quick spec',
|
||||
'Quick Dev': 'Quick dev',
|
||||
'Correct Course': 'Correct course',
|
||||
Brainstorming: 'Brainstorm ideas',
|
||||
'Party Mode': 'Party mode',
|
||||
'bmad-help': 'BMAD help',
|
||||
'Index Docs': 'Index documents',
|
||||
'Shard Document': 'Shard document',
|
||||
'Editorial Review - Prose': 'Editorial review prose',
|
||||
'Editorial Review - Structure': 'Editorial review structure',
|
||||
'Adversarial Review (General)': 'Adversarial review',
|
||||
};
|
||||
|
||||
return descriptionMap[name] || name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prompt content for tech-writer agent-only commands (Pattern C)
|
||||
* @param {Object} entry - bmad-help.csv row
|
||||
* @returns {Object|null} { fileName, content } or null if not a tech-writer command
|
||||
*/
|
||||
createTechWriterPromptContent(entry) {
|
||||
if (entry['agent-name'] !== 'tech-writer') return null;
|
||||
|
||||
const techWriterCommands = {
|
||||
'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
|
||||
'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' },
|
||||
'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' },
|
||||
'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' },
|
||||
'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' },
|
||||
};
|
||||
|
||||
const cmd = techWriterCommands[entry.name];
|
||||
if (!cmd) return null;
|
||||
|
||||
const safeDescription = this.escapeYamlSingleQuote(cmd.description);
|
||||
const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
|
||||
|
||||
const content = `---
|
||||
description: '${safeDescription}'
|
||||
agent: 'agent'
|
||||
tools: ${toolsStr}
|
||||
---
|
||||
|
||||
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
|
||||
2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona
|
||||
3. Execute the ${entry.name} menu command (${cmd.code})
|
||||
`;
|
||||
|
||||
return { fileName: cmd.file, content };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent activator prompt content (Pattern D)
|
||||
* @param {Object} artifact - Agent artifact
|
||||
* @param {Object|undefined} manifestEntry - Agent manifest entry
|
||||
* @returns {string} Prompt file content
|
||||
*/
|
||||
createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
|
||||
let description;
|
||||
if (manifestEntry) {
|
||||
description = manifestEntry.title || this.formatTitle(artifact.name);
|
||||
} else {
|
||||
description = this.formatTitle(artifact.name);
|
||||
}
|
||||
|
||||
const safeDescription = this.escapeYamlSingleQuote(description);
|
||||
const agentPath = artifact.agentPath || artifact.relativePath;
|
||||
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
|
||||
|
||||
// bmm/config.yaml is safe to hardcode: agent activators are only generated from
|
||||
// bmm agent artifacts, so bmm is guaranteed to be installed.
|
||||
return `---
|
||||
description: '${safeDescription}'
|
||||
agent: 'agent'
|
||||
tools: ${toolsStr}
|
||||
---
|
||||
|
||||
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
|
||||
2. Load the full agent file from ${agentFilePath}
|
||||
3. Follow ALL activation instructions in the agent file
|
||||
4. Display the welcome/greeting as instructed
|
||||
5. Present the numbered menu
|
||||
6. Wait for user input before proceeding
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate copilot-instructions.md from module config
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Map} agentManifest - Agent manifest data
|
||||
*/
|
||||
async generateCopilotInstructions(projectDir, bmadDir, agentManifest) {
|
||||
const configVars = await this.loadModuleConfig(bmadDir);
|
||||
|
||||
// Build the agents table from the manifest
|
||||
let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
|
||||
const agentOrder = [
|
||||
'bmad-master',
|
||||
'analyst',
|
||||
'architect',
|
||||
'dev',
|
||||
'pm',
|
||||
'qa',
|
||||
'quick-flow-solo-dev',
|
||||
'sm',
|
||||
'tech-writer',
|
||||
'ux-designer',
|
||||
];
|
||||
|
||||
for (const agentName of agentOrder) {
|
||||
const meta = agentManifest.get(agentName);
|
||||
if (meta) {
|
||||
const capabilities = meta.capabilities || 'agent capabilities';
|
||||
const cleanTitle = (meta.title || '').replaceAll('""', '"');
|
||||
agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const bmad = this.bmadFolderName;
|
||||
const bmadSection = `# BMAD Method — Project Instructions
|
||||
|
||||
## Project Configuration
|
||||
|
||||
- **Project**: ${configVars.project_name || '{{project_name}}'}
|
||||
- **User**: ${configVars.user_name || '{{user_name}}'}
|
||||
- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'}
|
||||
- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'}
|
||||
- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'}
|
||||
- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'}
|
||||
- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'}
|
||||
- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'}
|
||||
- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'}
|
||||
|
||||
## BMAD Runtime Structure
|
||||
|
||||
- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core)
|
||||
- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase)
|
||||
- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
|
||||
- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
|
||||
- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
|
||||
- **Module configuration**: \`${bmad}/bmm/config.yaml\`
|
||||
- **Core configuration**: \`${bmad}/core/config.yaml\`
|
||||
- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
|
||||
- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
|
||||
- **Help manifest**: \`${bmad}/_config/bmad-help.csv\`
|
||||
- **Agent memory**: \`${bmad}/_memory/\`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution
|
||||
- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\`
|
||||
- MD-based workflows execute directly — load and follow the \`.md\` file
|
||||
- YAML-based workflows require the workflow engine — load \`workflow.xml\` first, then pass the \`.yaml\` config
|
||||
- Follow step-based workflow execution: load steps JIT, never multiple at once
|
||||
- Save outputs after EACH step when using the workflow engine
|
||||
- The \`{project-root}\` variable resolves to the workspace root at runtime
|
||||
|
||||
## Available Agents
|
||||
|
||||
${agentsTable}
|
||||
## Slash Commands
|
||||
|
||||
Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`;
|
||||
|
||||
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
||||
const markerStart = '<!-- BMAD:START -->';
|
||||
const markerEnd = '<!-- BMAD:END -->';
|
||||
const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`;
|
||||
|
||||
if (await fs.pathExists(instructionsPath)) {
|
||||
const existing = await fs.readFile(instructionsPath, 'utf8');
|
||||
const startIdx = existing.indexOf(markerStart);
|
||||
const endIdx = existing.indexOf(markerEnd);
|
||||
|
||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||
// Replace only the BMAD section between markers
|
||||
const before = existing.slice(0, startIdx);
|
||||
const after = existing.slice(endIdx + markerEnd.length);
|
||||
const merged = `${before}${markedContent}${after}`;
|
||||
await this.writeFile(instructionsPath, merged);
|
||||
console.log(chalk.green(' ✓ Updated BMAD section in copilot-instructions.md'));
|
||||
} else {
|
||||
// Existing file without markers — back it up before overwriting
|
||||
const backupPath = `${instructionsPath}.bak`;
|
||||
await fs.copy(instructionsPath, backupPath);
|
||||
console.log(chalk.yellow(` ⚠ Backed up existing copilot-instructions.md → copilot-instructions.md.bak`));
|
||||
await this.writeFile(instructionsPath, `${markedContent}\n`);
|
||||
console.log(chalk.green(' ✓ Generated copilot-instructions.md (with BMAD markers)'));
|
||||
}
|
||||
} else {
|
||||
// No existing file — create fresh with markers
|
||||
await this.writeFile(instructionsPath, `${markedContent}\n`);
|
||||
console.log(chalk.green(' ✓ Generated copilot-instructions.md'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load module config.yaml for template variables
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @returns {Object} Config variables
|
||||
*/
|
||||
async loadModuleConfig(bmadDir) {
|
||||
const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
|
||||
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
||||
|
||||
for (const configPath of [bmmConfigPath, coreConfigPath]) {
|
||||
if (await fs.pathExists(configPath)) {
|
||||
try {
|
||||
const content = await fs.readFile(configPath, 'utf8');
|
||||
return yaml.parse(content) || {};
|
||||
} catch {
|
||||
// Fall through to next config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use inside YAML single-quoted values.
|
||||
* In YAML, the only escape inside single quotes is '' for a literal '.
|
||||
* @param {string} value - Raw string
|
||||
* @returns {string} Escaped string safe for YAML single-quoted context
|
||||
*/
|
||||
escapeYamlSingleQuote(value) {
|
||||
return (value || '').replaceAll("'", "''");
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan existing agent and prompt files for customised tool permissions before cleanup.
|
||||
* Returns a Map<filename, toolsArray> so permissions can be preserved across reinstalls.
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Map} Existing tool permissions keyed by filename
|
||||
*/
|
||||
async collectExistingToolPermissions(projectDir) {
|
||||
const permissions = new Map();
|
||||
const dirs = [
|
||||
[path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/],
|
||||
[path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/],
|
||||
];
|
||||
|
||||
for (const [dir, pattern] of dirs) {
|
||||
if (!(await fs.pathExists(dir))) continue;
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!pattern.test(file)) continue;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(path.join(dir, file), 'utf8');
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!fmMatch) continue;
|
||||
|
||||
const frontmatter = yaml.parse(fmMatch[1]);
|
||||
if (frontmatter && Array.isArray(frontmatter.tools)) {
|
||||
permissions.set(file, frontmatter.tools);
|
||||
}
|
||||
} catch {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools array string for a file, preserving any existing customisation.
|
||||
* Falls back to the default tools if no prior customisation exists.
|
||||
* @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md')
|
||||
* @returns {string} YAML inline array string
|
||||
*/
|
||||
getToolsForFile(fileName) {
|
||||
const defaultTools = ['read', 'edit', 'search', 'execute'];
|
||||
const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools;
|
||||
return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format name as title
|
||||
*/
|
||||
formatTitle(name) {
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup GitHub Copilot configuration - surgically remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
// Clean up agents directory
|
||||
const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir);
|
||||
if (await fs.pathExists(agentsDir)) {
|
||||
const files = await fs.readdir(agentsDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) {
|
||||
await fs.remove(path.join(agentsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up prompts directory
|
||||
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
|
||||
if (await fs.pathExists(promptsDir)) {
|
||||
const files = await fs.readdir(promptsDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) {
|
||||
await fs.remove(path.join(promptsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: copilot-instructions.md is NOT cleaned up here.
|
||||
// generateCopilotInstructions() handles marker-based replacement in a single
|
||||
// read-modify-write pass, which correctly preserves user content outside the markers.
|
||||
// Stripping markers here would cause generation to treat the file as legacy (no markers)
|
||||
// and overwrite user content.
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GitHubCopilotSetup };
|
||||
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
|||
* Dynamically discovers and loads IDE handlers
|
||||
*
|
||||
* Loading strategy:
|
||||
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic
|
||||
* 1. Custom installer files (codex.js, github-copilot.js, kilo.js) - for platforms with unique installation logic
|
||||
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||
*/
|
||||
class IdeManager {
|
||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
|||
|
||||
/**
|
||||
* Dynamically load all IDE handlers
|
||||
* 1. Load custom installer files first (codex.js, kilo.js)
|
||||
* 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js)
|
||||
* 2. Load config-driven handlers from platform-codes.yaml
|
||||
*/
|
||||
async loadHandlers() {
|
||||
|
|
@ -61,7 +61,7 @@ class IdeManager {
|
|||
*/
|
||||
async loadCustomInstallerFiles() {
|
||||
const ideDir = __dirname;
|
||||
const customFiles = ['codex.js', 'kilo.js'];
|
||||
const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js'];
|
||||
|
||||
for (const file of customFiles) {
|
||||
const filePath = path.join(ideDir, file);
|
||||
|
|
|
|||
|
|
@ -89,11 +89,7 @@ platforms:
|
|||
preferred: false
|
||||
category: ide
|
||||
description: "GitHub's AI pair programmer"
|
||||
installer:
|
||||
targets:
|
||||
- target_dir: .github/agents
|
||||
template_type: copilot_agents
|
||||
artifact_types: [agents]
|
||||
# No installer config - uses custom github-copilot.js
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Manages external official modules defined in external-official-modules.yaml
|
||||
|
|
@ -29,7 +30,7 @@ class ExternalModuleManager {
|
|||
this.cachedModules = config;
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load external modules config: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
|
||||
return { modules: {} };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,17 +236,11 @@ class ModuleManager {
|
|||
async getModuleInfo(modulePath, defaultName, sourceDescription) {
|
||||
// Check for module structure (module.yaml OR custom.yaml)
|
||||
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
||||
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
||||
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
||||
let configPath = null;
|
||||
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
configPath = moduleConfigPath;
|
||||
} else if (await fs.pathExists(installerConfigPath)) {
|
||||
configPath = installerConfigPath;
|
||||
} else if (await fs.pathExists(customConfigPath)) {
|
||||
configPath = customConfigPath;
|
||||
} else if (await fs.pathExists(rootCustomConfigPath)) {
|
||||
configPath = rootCustomConfigPath;
|
||||
}
|
||||
|
|
@ -268,7 +262,7 @@ class ModuleManager {
|
|||
description: 'BMAD Module',
|
||||
version: '5.0.0',
|
||||
source: sourceDescription,
|
||||
isCustom: configPath === customConfigPath || configPath === rootCustomConfigPath || isCustomSource,
|
||||
isCustom: configPath === rootCustomConfigPath || isCustomSource,
|
||||
};
|
||||
|
||||
// Read module config for metadata
|
||||
|
|
@ -458,7 +452,7 @@ class ModuleManager {
|
|||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||
} catch (error) {
|
||||
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
||||
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
|
||||
if (!silent) await prompts.log.warn(` ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
// Check if package.json is newer than node_modules
|
||||
|
|
@ -484,7 +478,7 @@ class ModuleManager {
|
|||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||
} catch (error) {
|
||||
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
||||
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
|
||||
if (!silent) await prompts.log.warn(` ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -541,21 +535,13 @@ class ModuleManager {
|
|||
// Check if this is a custom module and read its custom.yaml values
|
||||
let customConfig = null;
|
||||
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
|
||||
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
|
||||
|
||||
if (await fs.pathExists(rootCustomConfigPath)) {
|
||||
try {
|
||||
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
|
||||
customConfig = yaml.parse(customContent);
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
|
||||
try {
|
||||
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
|
||||
customConfig = yaml.parse(customContent);
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +549,7 @@ class ModuleManager {
|
|||
if (customConfig) {
|
||||
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
||||
if (options.logger) {
|
||||
options.logger.log(` Merged custom configuration for ${moduleName}`);
|
||||
await options.logger.log(` Merged custom configuration for ${moduleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -585,9 +571,9 @@ class ModuleManager {
|
|||
// Process agent files to inject activation block
|
||||
await this.processAgentFiles(targetPath, moduleName);
|
||||
|
||||
// Call module-specific installer if it exists (unless explicitly skipped)
|
||||
// Create directories declared in module.yaml (unless explicitly skipped)
|
||||
if (!options.skipModuleInstaller) {
|
||||
await this.runModuleInstaller(moduleName, bmadDir, options);
|
||||
await this.createModuleDirectories(moduleName, bmadDir, options);
|
||||
}
|
||||
|
||||
// Capture version info for manifest
|
||||
|
|
@ -743,8 +729,8 @@ class ModuleManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Skip _module-installer directory - it's only needed at install time
|
||||
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
|
||||
// Skip module.yaml at root - it's only needed at install time
|
||||
if (file === 'module.yaml') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -871,7 +857,7 @@ class ModuleManager {
|
|||
await fs.writeFile(targetFile, strippedYaml, 'utf8');
|
||||
} catch {
|
||||
// If anything fails, just copy the file as-is
|
||||
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`);
|
||||
await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
|
||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
||||
}
|
||||
}
|
||||
|
|
@ -1026,7 +1012,7 @@ class ModuleManager {
|
|||
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
|
||||
}
|
||||
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`);
|
||||
await prompts.log.warn(` Agent marked as having sidecar but ${sidecarDirName} directory not found`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1259,62 +1245,175 @@ class ModuleManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Run module-specific installer if it exists
|
||||
* Create directories declared in module.yaml's `directories` key
|
||||
* This replaces the security-risky module installer pattern with declarative config
|
||||
* During updates, if a directory path changed, moves the old directory to the new path
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @param {string} bmadDir - Target bmad directory
|
||||
* @param {Object} options - Installation options
|
||||
* @param {Object} options.moduleConfig - Module configuration from config collector
|
||||
* @param {Object} options.existingModuleConfig - Previous module config (for detecting path changes during updates)
|
||||
* @param {Object} options.coreConfig - Core configuration
|
||||
* @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
|
||||
*/
|
||||
async runModuleInstaller(moduleName, bmadDir, options = {}) {
|
||||
async createModuleDirectories(moduleName, bmadDir, options = {}) {
|
||||
const moduleConfig = options.moduleConfig || {};
|
||||
const existingModuleConfig = options.existingModuleConfig || {};
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||
|
||||
// Special handling for core module - it's in src/core not src/modules
|
||||
let sourcePath;
|
||||
if (moduleName === 'core') {
|
||||
sourcePath = getSourcePath('core');
|
||||
} else {
|
||||
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
|
||||
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
||||
if (!sourcePath) {
|
||||
// No source found, skip module installer
|
||||
return;
|
||||
return emptyResult; // No source found, skip
|
||||
}
|
||||
}
|
||||
|
||||
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
|
||||
|
||||
// Check if module has a custom installer
|
||||
if (!(await fs.pathExists(installerPath))) {
|
||||
return; // No custom installer
|
||||
// Read module.yaml to find the `directories` key
|
||||
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
||||
return emptyResult; // No module.yaml, skip
|
||||
}
|
||||
|
||||
let moduleYaml;
|
||||
try {
|
||||
// Load the module installer
|
||||
const moduleInstaller = require(installerPath);
|
||||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
moduleYaml = yaml.parse(yamlContent);
|
||||
} catch {
|
||||
return emptyResult; // Invalid YAML, skip
|
||||
}
|
||||
|
||||
if (typeof moduleInstaller.install === 'function') {
|
||||
// Get project root (parent of bmad directory)
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
if (!moduleYaml || !moduleYaml.directories) {
|
||||
return emptyResult; // No directories declared, skip
|
||||
}
|
||||
|
||||
// Prepare logger (use console if not provided)
|
||||
const logger = options.logger || {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
};
|
||||
const directories = moduleYaml.directories;
|
||||
const wdsFolders = moduleYaml.wds_folders || [];
|
||||
const createdDirs = [];
|
||||
const movedDirs = [];
|
||||
const createdWdsFolders = [];
|
||||
|
||||
// Call the module installer
|
||||
const result = await moduleInstaller.install({
|
||||
projectRoot,
|
||||
config: options.moduleConfig || {},
|
||||
coreConfig: options.coreConfig || {},
|
||||
installedIDEs: options.installedIDEs || [],
|
||||
logger,
|
||||
});
|
||||
for (const dirRef of directories) {
|
||||
// Parse variable reference like "{design_artifacts}"
|
||||
const varMatch = dirRef.match(/^\{([^}]+)\}$/);
|
||||
if (!varMatch) {
|
||||
// Not a variable reference, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
await prompts.log.warn(`Module installer for ${moduleName} returned false`);
|
||||
const configKey = varMatch[1];
|
||||
const dirValue = moduleConfig[configKey];
|
||||
if (!dirValue || typeof dirValue !== 'string') {
|
||||
continue; // No value or not a string, skip
|
||||
}
|
||||
|
||||
// Strip {project-root}/ prefix if present
|
||||
let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
|
||||
|
||||
// Handle remaining {project-root} anywhere in the path
|
||||
dirPath = dirPath.replaceAll('{project-root}', '');
|
||||
|
||||
// Resolve to absolute path
|
||||
const fullPath = path.join(projectRoot, dirPath);
|
||||
|
||||
// Validate path is within project root (prevent directory traversal)
|
||||
const normalizedPath = path.normalize(fullPath);
|
||||
const normalizedRoot = path.normalize(projectRoot);
|
||||
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
|
||||
const color = await prompts.getColor();
|
||||
await prompts.log.warn(color.yellow(`${configKey} path escapes project root, skipping: ${dirPath}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if directory path changed from previous config (update/modify scenario)
|
||||
const oldDirValue = existingModuleConfig[configKey];
|
||||
let oldFullPath = null;
|
||||
let oldDirPath = null;
|
||||
if (oldDirValue && typeof oldDirValue === 'string') {
|
||||
// F3: Normalize both values before comparing to avoid false negatives
|
||||
// from trailing slashes, separator differences, or prefix format variations
|
||||
let normalizedOld = oldDirValue.replace(/^\{project-root\}\/?/, '');
|
||||
normalizedOld = path.normalize(normalizedOld.replaceAll('{project-root}', ''));
|
||||
const normalizedNew = path.normalize(dirPath);
|
||||
|
||||
if (normalizedOld !== normalizedNew) {
|
||||
oldDirPath = normalizedOld;
|
||||
oldFullPath = path.join(projectRoot, oldDirPath);
|
||||
const normalizedOldAbsolute = path.normalize(oldFullPath);
|
||||
if (!normalizedOldAbsolute.startsWith(normalizedRoot + path.sep) && normalizedOldAbsolute !== normalizedRoot) {
|
||||
oldFullPath = null; // Old path escapes project root, ignore it
|
||||
}
|
||||
|
||||
// F13: Prevent parent/child move (e.g. docs/planning → docs/planning/v2)
|
||||
if (oldFullPath) {
|
||||
const normalizedNewAbsolute = path.normalize(fullPath);
|
||||
if (
|
||||
normalizedOldAbsolute.startsWith(normalizedNewAbsolute + path.sep) ||
|
||||
normalizedNewAbsolute.startsWith(normalizedOldAbsolute + path.sep)
|
||||
) {
|
||||
const color = await prompts.getColor();
|
||||
await prompts.log.warn(
|
||||
color.yellow(
|
||||
`${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}), creating new directory instead`,
|
||||
),
|
||||
);
|
||||
oldFullPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dirName = configKey.replaceAll('_', ' ');
|
||||
|
||||
if (oldFullPath && (await fs.pathExists(oldFullPath)) && !(await fs.pathExists(fullPath))) {
|
||||
// Path changed and old dir exists → move old to new location
|
||||
// F1: Use fs.move() instead of fs.rename() for cross-device/volume support
|
||||
// F2: Wrap in try/catch — fallback to creating new dir on failure
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(fullPath));
|
||||
await fs.move(oldFullPath, fullPath);
|
||||
movedDirs.push(`${dirName}: ${oldDirPath} → ${dirPath}`);
|
||||
} catch (moveError) {
|
||||
const color = await prompts.getColor();
|
||||
await prompts.log.warn(
|
||||
color.yellow(
|
||||
`Failed to move ${oldDirPath} → ${dirPath}: ${moveError.message}\n Creating new directory instead. Please move contents from the old directory manually.`,
|
||||
),
|
||||
);
|
||||
await fs.ensureDir(fullPath);
|
||||
createdDirs.push(`${dirName}: ${dirPath}`);
|
||||
}
|
||||
} else if (oldFullPath && (await fs.pathExists(oldFullPath)) && (await fs.pathExists(fullPath))) {
|
||||
// F5: Both old and new directories exist — warn user about potential orphaned documents
|
||||
const color = await prompts.getColor();
|
||||
await prompts.log.warn(
|
||||
color.yellow(
|
||||
`${dirName}: path changed but both directories exist:\n Old: ${oldDirPath}\n New: ${dirPath}\n Old directory may contain orphaned documents — please review and merge manually.`,
|
||||
),
|
||||
);
|
||||
} else if (!(await fs.pathExists(fullPath))) {
|
||||
// New directory doesn't exist yet → create it
|
||||
createdDirs.push(`${dirName}: ${dirPath}`);
|
||||
await fs.ensureDir(fullPath);
|
||||
}
|
||||
|
||||
// Create WDS subfolders if this is the design_artifacts directory
|
||||
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
|
||||
for (const subfolder of wdsFolders) {
|
||||
const subPath = path.join(fullPath, subfolder);
|
||||
if (!(await fs.pathExists(subPath))) {
|
||||
await fs.ensureDir(subPath);
|
||||
createdWdsFolders.push(subfolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
|
||||
return { createdDirs, movedDirs, createdWdsFolders };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1383,10 +1482,6 @@ class ModuleManager {
|
|||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip _module-installer directories
|
||||
if (entry.name === '_module-installer') {
|
||||
continue;
|
||||
}
|
||||
const subFiles = await this.getFileList(fullPath, baseDir);
|
||||
files.push(...subFiles);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -279,6 +279,9 @@ async function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
|||
`title="${meta.title || ''}"`,
|
||||
`icon="${meta.icon || '🤖'}"`,
|
||||
];
|
||||
if (meta.capabilities) {
|
||||
agentAttrs.push(`capabilities="${escapeXml(meta.capabilities)}"`);
|
||||
}
|
||||
|
||||
xml += `<agent ${agentAttrs.join(' ')}>\n`;
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class UI {
|
|||
const installedVersion = existingInstall.version || 'unknown';
|
||||
|
||||
// Check if version is pre beta
|
||||
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir));
|
||||
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir), options);
|
||||
|
||||
// If user chose to cancel, exit the installer
|
||||
if (!shouldProceed) {
|
||||
|
|
@ -227,6 +227,14 @@ class UI {
|
|||
}
|
||||
actionType = options.action;
|
||||
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
||||
} else if (options.yes) {
|
||||
// Default to quick-update if available, otherwise first available choice
|
||||
if (choices.length === 0) {
|
||||
throw new Error('No valid actions available for this installation');
|
||||
}
|
||||
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
|
||||
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
|
||||
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
|
||||
} else {
|
||||
actionType = await prompts.select({
|
||||
message: 'How would you like to proceed?',
|
||||
|
|
@ -242,6 +250,7 @@ class UI {
|
|||
actionType: 'quick-update',
|
||||
directory: confirmedDirectory,
|
||||
customContent: { hasCustomContent: false },
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -252,6 +261,7 @@ class UI {
|
|||
actionType: 'compile-agents',
|
||||
directory: confirmedDirectory,
|
||||
customContent: { hasCustomContent: false },
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -272,9 +282,13 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||
} else if (options.yes) {
|
||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
await prompts.log.info(
|
||||
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
selectedModules = selectedModules.filter((m) => m !== 'core');
|
||||
}
|
||||
|
||||
// After module selection, ask about custom modules
|
||||
|
|
@ -331,6 +345,22 @@ class UI {
|
|||
},
|
||||
};
|
||||
}
|
||||
} else if (options.yes) {
|
||||
// Non-interactive mode: preserve existing custom modules (matches default: false)
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
customModuleResult.selectedCustomModules.push(entry.name);
|
||||
}
|
||||
}
|
||||
await prompts.log.info(
|
||||
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
|
||||
);
|
||||
} else {
|
||||
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
|
||||
}
|
||||
} else {
|
||||
const changeCustomModules = await prompts.confirm({
|
||||
message: 'Modify custom modules, agents, or workflows?',
|
||||
|
|
@ -362,6 +392,9 @@ class UI {
|
|||
selectedModules.push(...customModuleResult.selectedCustomModules);
|
||||
}
|
||||
|
||||
// Filter out core - it's always installed via installCore flag
|
||||
selectedModules = selectedModules.filter((m) => m !== 'core');
|
||||
|
||||
// Get tool selection
|
||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
||||
|
||||
|
|
@ -376,6 +409,7 @@ class UI {
|
|||
skipIde: toolSelection.skipIde,
|
||||
coreConfig: coreConfig,
|
||||
customContent: customModuleResult.customContentConfig,
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -527,6 +561,27 @@ class UI {
|
|||
if (configuredIdes.length > 0) {
|
||||
const allTools = [...preferredIdes, ...otherIdes];
|
||||
|
||||
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
||||
if (options.tools) {
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||
return { ides: [], skipIde: true };
|
||||
}
|
||||
const selectedIdes = options.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
}
|
||||
|
||||
if (options.yes) {
|
||||
await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||
return { ides: configuredIdes, skipIde: false };
|
||||
}
|
||||
|
||||
// Sort: configured tools first, then preferred, then others
|
||||
const sortedTools = [
|
||||
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
|
||||
|
|
@ -689,18 +744,6 @@ class UI {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display installation summary
|
||||
* @param {Object} result - Installation result
|
||||
*/
|
||||
async showInstallSummary(result) {
|
||||
let summary = `Installed to: ${result.path}`;
|
||||
if (result.modules && result.modules.length > 0) {
|
||||
summary += `\nModules: ${result.modules.join(', ')}`;
|
||||
}
|
||||
await prompts.note(summary, 'BMAD is ready to use!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get confirmed directory from user
|
||||
* @returns {string} Confirmed directory path
|
||||
|
|
@ -899,107 +942,10 @@ class UI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Prompt for module selection
|
||||
* @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
|
||||
* Select all modules (official + community) using grouped multiselect.
|
||||
* Core is shown as locked but filtered from the result since it's always installed separately.
|
||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||
* @returns {Array} Selected module codes
|
||||
* @returns {Array} Selected module codes (excluding core)
|
||||
*/
|
||||
async selectAllModules(installedModuleIds = new Set()) {
|
||||
const { ModuleManager } = require('../installers/lib/modules/manager');
|
||||
|
|
@ -1068,11 +1014,7 @@ class UI {
|
|||
}
|
||||
}
|
||||
}
|
||||
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), {
|
||||
// "None" option at the end
|
||||
label: '\u26A0 None - Skip module installation',
|
||||
value: '__NONE__',
|
||||
});
|
||||
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
||||
|
||||
const selected = await prompts.autocompleteMultiselect({
|
||||
message: 'Select modules to install:',
|
||||
|
|
@ -1083,14 +1025,7 @@ class UI {
|
|||
maxItems: allOptions.length,
|
||||
});
|
||||
|
||||
// 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__') : [];
|
||||
const result = selected ? selected.filter((m) => m !== 'core') : [];
|
||||
|
||||
// Display selected modules as bulleted list
|
||||
if (result.length > 0) {
|
||||
|
|
@ -1748,7 +1683,7 @@ class UI {
|
|||
* @param {string} bmadFolderName - Name of the BMAD folder
|
||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||
*/
|
||||
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName, options = {}) {
|
||||
if (!this.isLegacyVersion(installedVersion)) {
|
||||
return true; // Not legacy, proceed
|
||||
}
|
||||
|
|
@ -1774,6 +1709,11 @@ class UI {
|
|||
await prompts.log.warn('VERSION WARNING');
|
||||
await prompts.note(warningContent, 'Version Warning');
|
||||
|
||||
if (options.yes) {
|
||||
await prompts.log.warn('Non-interactive mode (--yes): auto-proceeding with legacy update');
|
||||
return true;
|
||||
}
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'How would you like to proceed?',
|
||||
choices: [
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ function buildMetadataSchema(expectedModule) {
|
|||
title: createNonEmptyString('agent.metadata.title'),
|
||||
icon: createNonEmptyString('agent.metadata.icon'),
|
||||
module: createNonEmptyString('agent.metadata.module').optional(),
|
||||
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
|
||||
hasSidecar: z.boolean(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const STRICT = process.argv.includes('--strict');
|
|||
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']);
|
||||
|
||||
// Skip directories
|
||||
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
|
||||
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
||||
|
||||
// Pattern: {project-root}/_bmad/ references
|
||||
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;
|
||||
|
|
|
|||
Loading…
Reference in New Issue