Compare commits
7 Commits
1166af05a7
...
867845c8e7
| Author | SHA1 | Date |
|---|---|---|
|
|
867845c8e7 | |
|
|
5e0a8ea2f3 | |
|
|
56a3f267f0 | |
|
|
48a7ec8bff | |
|
|
3da984a491 | |
|
|
815600e4ca | |
|
|
7ee5fa313b |
|
|
@ -18,7 +18,7 @@ Use `npx bmad-method install` to set up BMad in your project. One command handle
|
|||
|
||||
- **Node.js** 20+ (the installer requires it)
|
||||
- **Git** (for cloning external modules)
|
||||
- **An AI tool** such as Claude Code or Cursor — or install without one using `--tools none`
|
||||
- **An AI tool** such as Claude Code or Cursor (run `npx bmad-method install --list-tools` to see all supported tools)
|
||||
|
||||
:::
|
||||
|
||||
|
|
@ -122,7 +122,8 @@ Under `--yes`, patch and minor upgrades apply automatically. Majors stay frozen
|
|||
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
|
||||
| `--directory <path>` | Install into this directory (default: current working dir) |
|
||||
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
|
||||
| `--tools <a,b>` or `--tools none` | IDE/tool selection. `none` skips tool config entirely. |
|
||||
| `--tools <a,b>` | IDE/tool selection. Required for fresh `--yes` installs. Run `--list-tools` for valid IDs. |
|
||||
| `--list-tools` | Print all supported tool/IDE IDs (with target directories) and exit. |
|
||||
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
|
||||
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
|
||||
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
|
||||
|
|
@ -165,17 +166,17 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code
|
|||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,gds \
|
||||
--tools none
|
||||
--modules bmm,bmb,gds
|
||||
```
|
||||
|
||||
`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install.
|
||||
|
||||
**Mix channels — bmb on next, gds on stable:**
|
||||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,cis,gds \
|
||||
--next=bmb \
|
||||
--tools none
|
||||
--next=bmb
|
||||
```
|
||||
|
||||
:::caution[Rate limit on shared IPs]
|
||||
|
|
@ -204,7 +205,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules`
|
|||
|
||||
```bash
|
||||
npx bmad-method install --yes --modules bmb,cis \
|
||||
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools none
|
||||
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools claude-code
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
|
|
|||
|
|
@ -227,37 +227,39 @@ Prepare the content to append to the document:
|
|||
|
||||
### Architecture Completeness Checklist
|
||||
|
||||
**✅ Requirements Analysis**
|
||||
Mark each item `[x]` only if validation confirms it; leave `[ ]` if it is missing, partial, or unverified. Any unchecked item must be reflected in the Gap Analysis above and in the Overall Status below.
|
||||
|
||||
- [x] Project context thoroughly analyzed
|
||||
- [x] Scale and complexity assessed
|
||||
- [x] Technical constraints identified
|
||||
- [x] Cross-cutting concerns mapped
|
||||
**Requirements Analysis**
|
||||
|
||||
**✅ Architectural Decisions**
|
||||
- [ ] Project context thoroughly analyzed
|
||||
- [ ] Scale and complexity assessed
|
||||
- [ ] Technical constraints identified
|
||||
- [ ] Cross-cutting concerns mapped
|
||||
|
||||
- [x] Critical decisions documented with versions
|
||||
- [x] Technology stack fully specified
|
||||
- [x] Integration patterns defined
|
||||
- [x] Performance considerations addressed
|
||||
**Architectural Decisions**
|
||||
|
||||
**✅ Implementation Patterns**
|
||||
- [ ] Critical decisions documented with versions
|
||||
- [ ] Technology stack fully specified
|
||||
- [ ] Integration patterns defined
|
||||
- [ ] Performance considerations addressed
|
||||
|
||||
- [x] Naming conventions established
|
||||
- [x] Structure patterns defined
|
||||
- [x] Communication patterns specified
|
||||
- [x] Process patterns documented
|
||||
**Implementation Patterns**
|
||||
|
||||
**✅ Project Structure**
|
||||
- [ ] Naming conventions established
|
||||
- [ ] Structure patterns defined
|
||||
- [ ] Communication patterns specified
|
||||
- [ ] Process patterns documented
|
||||
|
||||
- [x] Complete directory structure defined
|
||||
- [x] Component boundaries established
|
||||
- [x] Integration points mapped
|
||||
- [x] Requirements to structure mapping complete
|
||||
**Project Structure**
|
||||
|
||||
- [ ] Complete directory structure defined
|
||||
- [ ] Component boundaries established
|
||||
- [ ] Integration points mapped
|
||||
- [ ] Requirements to structure mapping complete
|
||||
|
||||
### Architecture Readiness Assessment
|
||||
|
||||
**Overall Status:** READY FOR IMPLEMENTATION
|
||||
**Overall Status:** {{READY FOR IMPLEMENTATION | READY WITH MINOR GAPS | NOT READY}} (choose READY FOR IMPLEMENTATION only when all 16 checklist items are `[x]` and no Critical Gaps remain; choose NOT READY when any Critical Gap is open or any Requirements Analysis or Architectural Decisions item is unchecked; otherwise READY WITH MINOR GAPS)
|
||||
|
||||
**Confidence Level:** {{high/medium/low}} based on validation results
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
|
||||
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
|
||||
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,anytime,,,false,project-knowledge,*
|
||||
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,anytime,,,false,output_folder,project context
|
||||
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,anytime,,,false,implementation_artifacts,spec and project implementation
|
||||
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,anytime,,,false,planning_artifacts,change proposal
|
||||
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
|
||||
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context
|
||||
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation
|
||||
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
|
||||
BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document
|
||||
BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards
|
||||
BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram
|
||||
BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report
|
||||
BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation
|
||||
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,1-analysis,,,false,planning_artifacts,brainstorming session
|
||||
BMad Method,bmad-market-research,Market Research,MR,"Market analysis competitive landscape customer needs and trends.",,1-analysis,,,false,"planning_artifacts|project-knowledge",research documents
|
||||
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
|
||||
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
|
||||
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
|
||||
BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents
|
||||
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
|
||||
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
|
||||
BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief
|
||||
BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document
|
||||
BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,2-planning,,,true,planning_artifacts,prd
|
||||
BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,,2-planning,,,true,planning_artifacts,prd
|
||||
BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report
|
||||
BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd
|
||||
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,2-planning,bmad-create-prd,,false,planning_artifacts,ux design
|
||||
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,3-solutioning,,,true,planning_artifacts,architecture
|
||||
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
|
||||
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
|
||||
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,4-implementation,,,true,implementation_artifacts,sprint status
|
||||
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,4-implementation,bmad-sprint-planning,,false,,
|
||||
BMad Method,bmad-create-story,Create Story,CS,"Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.",create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
|
||||
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-create-prd,,false,planning_artifacts,ux design
|
||||
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
|
||||
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
|
||||
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
|
||||
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status
|
||||
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
|
||||
BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
|
||||
BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report
|
||||
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,4-implementation,bmad-create-story:validate,,true,,
|
||||
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,4-implementation,bmad-dev-story,,false,,
|
||||
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,4-implementation,,,false,,
|
||||
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
|
||||
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
|
||||
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,,
|
||||
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,,
|
||||
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,,
|
||||
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
|
||||
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -5,15 +5,11 @@ default_selected: true # This module will be selected by default for new install
|
|||
|
||||
# Variables from Core Config inserted:
|
||||
## user_name
|
||||
## project_name
|
||||
## communication_language
|
||||
## document_output_language
|
||||
## output_folder
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
user_skill_level:
|
||||
prompt:
|
||||
- "What is your development experience level?"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
|
||||
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
|
||||
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
|
||||
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,anytime,,,false,,
|
||||
Core,bmad-help,BMad Help,BH,,,anytime,,,false,,
|
||||
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,anytime,,,false,,
|
||||
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,[path],anytime,,,false,,
|
||||
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
|
||||
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,[path],anytime,,,false,report located with target document,
|
||||
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",[path],anytime,,,false,,
|
||||
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,[path],anytime,,,false,,
|
||||
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
|
||||
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,anytime,,,false,{project-root}/_bmad/custom,TOML override files
|
||||
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
|
||||
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
|
||||
Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
|
||||
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
|
||||
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
|
||||
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
|
||||
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document,
|
||||
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,,
|
||||
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
|
||||
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
|
||||
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -11,6 +11,11 @@ user_name:
|
|||
default: "BMad"
|
||||
result: "{value}"
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
communication_language:
|
||||
prompt: "What language should agents use when chatting with you?"
|
||||
scope: user
|
||||
|
|
|
|||
|
|
@ -557,10 +557,39 @@ async function runTests() {
|
|||
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
|
||||
|
||||
assert(copilotInstaller?.target_dir === '.agents/skills', 'GitHub Copilot target_dir uses native skills path');
|
||||
assert(
|
||||
copilotInstaller?.commands_target_dir === '.github/agents',
|
||||
'GitHub Copilot commands_target_dir is configured for the Custom Agents picker',
|
||||
);
|
||||
assert(copilotInstaller?.commands_extension === '.agent.md', 'GitHub Copilot uses .agent.md extension for Custom Agents files');
|
||||
assert(
|
||||
typeof copilotInstaller?.commands_body_template === 'string' && copilotInstaller.commands_body_template.includes('{canonicalId}'),
|
||||
'GitHub Copilot defines a commands_body_template with {canonicalId} placeholder',
|
||||
);
|
||||
assert(
|
||||
copilotInstaller?.commands_filter === 'agents-only',
|
||||
'GitHub Copilot filters Custom Agents picker to persona agents only (agents-only)',
|
||||
);
|
||||
|
||||
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
||||
const installedBmadDir17 = await createTestBmadFixture();
|
||||
|
||||
// Extend the fixture: add a persona-style agent skill (`-agent-` in id)
|
||||
// and the `bmad-tea` non-conventional agent so we can verify that the
|
||||
// agents-only filter routes them to .github/agents/ while leaving the
|
||||
// workflow-style `bmad-master` out.
|
||||
const fixtureCsvPath17 = path.join(installedBmadDir17, '_config', 'skill-manifest.csv');
|
||||
await fs.writeFile(
|
||||
fixtureCsvPath17,
|
||||
[
|
||||
'canonicalId,name,description,module,path',
|
||||
'"bmad-master","bmad-master","Workflow-style fixture (no -agent- in id, should NOT appear in Copilot agents picker)","core","_bmad/core/bmad-master/SKILL.md"',
|
||||
'"bmad-agent-fixture","bmad-agent-fixture","Persona agent fixture (-agent- in id, SHOULD appear in Copilot agents picker)","core","_bmad/core/bmad-agent-fixture/SKILL.md"',
|
||||
'"bmad-tea","bmad-tea","Non-conventional persona agent fixture (Murat-style, SHOULD appear despite no -agent- segment)","core","_bmad/core/bmad-tea/SKILL.md"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
||||
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
|
||||
await fs.writeFile(
|
||||
|
|
@ -596,6 +625,44 @@ async function runTests() {
|
|||
'GitHub Copilot setup preserves user content in copilot-instructions.md',
|
||||
);
|
||||
|
||||
// Custom Agents picker integration: persona agents (and bmad-tea) get
|
||||
// .agent.md files in .github/agents/. Workflows do NOT — the
|
||||
// agents-only filter keeps the picker uncluttered.
|
||||
const agentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
|
||||
const agentFileForPersona17 = path.join(agentsDir17, 'bmad-agent-fixture.agent.md');
|
||||
const agentFileForTea17 = path.join(agentsDir17, 'bmad-tea.agent.md');
|
||||
const agentFileForWorkflow17 = path.join(agentsDir17, 'bmad-master.agent.md');
|
||||
|
||||
assert(await fs.pathExists(agentFileForPersona17), 'Persona agent (-agent- in id) gets a .agent.md file in .github/agents/');
|
||||
assert(await fs.pathExists(agentFileForTea17), 'bmad-tea persona (non-conventional id) is allowlisted into .github/agents/');
|
||||
assert(
|
||||
!(await fs.pathExists(agentFileForWorkflow17)),
|
||||
'Workflow skill (no -agent- in id, not in allowlist) is FILTERED OUT of .github/agents/',
|
||||
);
|
||||
|
||||
// Body content of the persona agent file: frontmatter description +
|
||||
// LOAD pattern referencing the skill's SKILL.md path under target_dir.
|
||||
const personaAgentContent17 = await fs.readFile(agentFileForPersona17, 'utf8');
|
||||
assert(
|
||||
personaAgentContent17.includes('description:'),
|
||||
'Copilot agent pointer carries a description in YAML frontmatter (drives the agents picker label)',
|
||||
);
|
||||
assert(
|
||||
personaAgentContent17.includes('{project-root}/.agents/skills/bmad-agent-fixture/SKILL.md'),
|
||||
'Copilot agent pointer body resolves to the skill via LOAD {project-root}/<target_dir>/<id>/SKILL.md',
|
||||
);
|
||||
|
||||
// Idempotency: re-running setup must not duplicate or rewrite the agent
|
||||
// pointer when the source manifest is unchanged, AND must not start
|
||||
// emitting workflow-skill agent files.
|
||||
const result17b = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
assert(result17b.success === true, 'Second GitHub Copilot install succeeds (idempotent)');
|
||||
assert(await fs.pathExists(agentFileForPersona17), 'Persona agent pointer survives a second install pass');
|
||||
assert(!(await fs.pathExists(agentFileForWorkflow17)), 'Workflow skill remains filtered out of agents picker on second install');
|
||||
|
||||
await fs.remove(tempProjectDir17);
|
||||
await fs.remove(path.dirname(installedBmadDir17));
|
||||
} catch (error) {
|
||||
|
|
@ -1866,12 +1933,12 @@ async function runTests() {
|
|||
const moduleConfigs = {
|
||||
core: {
|
||||
user_name: 'TestUser',
|
||||
project_name: 'demo-project',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
},
|
||||
bmm: {
|
||||
project_name: 'demo-project',
|
||||
user_skill_level: 'expert',
|
||||
planning_artifacts: '{project-root}/_bmad-output/planning-artifacts',
|
||||
implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts',
|
||||
|
|
@ -1879,7 +1946,10 @@ async function runTests() {
|
|||
// Spread-from-core pollution: legacy per-module config.yaml merges
|
||||
// core values into every module; writeCentralConfig must strip these
|
||||
// from [modules.bmm] so core values only live in [core].
|
||||
// project_name is now a core key (#2279), so it joins user_name etc.
|
||||
// as a spread-from-core key that must be stripped.
|
||||
user_name: 'TestUser',
|
||||
project_name: 'stale-bmm-copy',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
@ -1927,6 +1997,7 @@ async function runTests() {
|
|||
assert(teamContent.includes('[core]'), 'config.toml has [core] section');
|
||||
assert(teamContent.includes('document_output_language = "English"'), 'Team-scope core key lands in config.toml');
|
||||
assert(teamContent.includes('output_folder = "_bmad-output"'), 'Team-scope output_folder lands in config.toml');
|
||||
assert(teamContent.includes('project_name = "demo-project"'), 'project_name lands in [core] (core key as of #2279)');
|
||||
assert(!teamContent.includes('user_name'), 'user_name (scope: user) is absent from config.toml');
|
||||
assert(!teamContent.includes('communication_language'), 'communication_language (scope: user) is absent from config.toml');
|
||||
|
||||
|
|
@ -1941,7 +2012,9 @@ async function runTests() {
|
|||
assert(bmmTeamMatch !== null, 'config.toml has [modules.bmm] section');
|
||||
if (bmmTeamMatch) {
|
||||
const bmmTeamBlock = bmmTeamMatch[0];
|
||||
assert(bmmTeamBlock.includes('project_name = "demo-project"'), 'bmm team-scope key lands under [modules.bmm]');
|
||||
assert(bmmTeamBlock.includes('planning_artifacts'), 'bmm-owned team-scope key (planning_artifacts) lands under [modules.bmm]');
|
||||
assert(!bmmTeamBlock.includes('project_name'), 'project_name stripped from [modules.bmm] (now a core key, #2279)');
|
||||
assert(!bmmTeamBlock.includes('stale-bmm-copy'), 'stale bmm-copy of project_name not leaked into config.toml');
|
||||
assert(!bmmTeamBlock.includes('user_name'), 'user_name stripped from [modules.bmm] (core-key pollution)');
|
||||
assert(!bmmTeamBlock.includes('communication_language'), 'communication_language stripped from [modules.bmm]');
|
||||
assert(!bmmTeamBlock.includes('user_skill_level'), 'user_skill_level (scope: user) absent from [modules.bmm] in config.toml');
|
||||
|
|
@ -2933,6 +3006,210 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 42: --tools flag parsing & validation (#2326)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 42: --tools flag parsing & validation${colors.reset}\n`);
|
||||
try {
|
||||
const { UI } = require('../tools/installer/ui');
|
||||
const ui = new UI();
|
||||
const known = new Set(['claude-code', 'cursor', 'windsurf']);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag('claude-code', known)) === JSON.stringify(['claude-code']),
|
||||
'parseToolsFlag returns single ID',
|
||||
);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag('claude-code,cursor', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||
'parseToolsFlag returns multiple IDs',
|
||||
);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag(' claude-code , cursor ', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||
'parseToolsFlag trims whitespace',
|
||||
);
|
||||
|
||||
let emptyErr;
|
||||
try {
|
||||
ui._parseToolsFlag('', known);
|
||||
} catch (error) {
|
||||
emptyErr = error;
|
||||
}
|
||||
assert(
|
||||
emptyErr && emptyErr.expected === true && /empty/i.test(emptyErr.message),
|
||||
'parseToolsFlag rejects empty string with expected=true',
|
||||
);
|
||||
|
||||
let commasOnlyErr;
|
||||
try {
|
||||
ui._parseToolsFlag(' , , ', known);
|
||||
} catch (error) {
|
||||
commasOnlyErr = error;
|
||||
}
|
||||
assert(commasOnlyErr && commasOnlyErr.expected === true, 'parseToolsFlag rejects whitespace/comma-only input');
|
||||
|
||||
let noneErr;
|
||||
try {
|
||||
ui._parseToolsFlag('none', known);
|
||||
} catch (error) {
|
||||
noneErr = error;
|
||||
}
|
||||
assert(noneErr && noneErr.expected === true && /Unknown tool ID/.test(noneErr.message), 'parseToolsFlag rejects "none" as unknown ID');
|
||||
|
||||
let typoErr;
|
||||
try {
|
||||
ui._parseToolsFlag('claude-code,claude-cdoe', known);
|
||||
} catch (error) {
|
||||
typoErr = error;
|
||||
}
|
||||
const typoHeader = typoErr ? typoErr.message.split('\n')[0] : '';
|
||||
assert(
|
||||
typoErr && typoErr.expected === true && /claude-cdoe/.test(typoHeader) && !/claude-code/.test(typoHeader),
|
||||
'parseToolsFlag reports only the unknown ID in error header (valid ones not listed as unknown)',
|
||||
);
|
||||
|
||||
// --list-tools and --tools validation must agree on what counts as a valid ID.
|
||||
const { formatPlatformList } = require('../tools/installer/ide/platform-codes');
|
||||
const { IdeManager } = require('../tools/installer/ide/manager');
|
||||
const ideManager42 = new IdeManager();
|
||||
await ideManager42.ensureInitialized();
|
||||
const validIds = new Set(ideManager42.getAvailableIdes().map((i) => i.value));
|
||||
const listed = await formatPlatformList();
|
||||
// Each entry line starts with ' *' (preferred) or ' ' (other), followed by the ID, then padding.
|
||||
const entryLines = listed.split('\n').filter((l) => /^( \*| {2})[a-z]/.test(l));
|
||||
const listedIds = entryLines.map((l) => l.trim().replace(/^\*/, '').split(/\s+/)[0]);
|
||||
const missingFromList = [...validIds].filter((id) => !listedIds.includes(id));
|
||||
const extraInList = listedIds.filter((id) => !validIds.has(id));
|
||||
assert(
|
||||
missingFromList.length === 0 && extraInList.length === 0,
|
||||
'--list-tools output matches the IDs that --tools accepts',
|
||||
`Missing from list: ${missingFromList.join(',') || '(none)'}; Extra in list: ${extraInList.join(',') || '(none)'}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 42 setup failed: ${error.message}${colors.reset}`);
|
||||
console.log(error.stack);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 43: project_name promoted to core + hoist migration (#2279)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 43: project_name in core + hoist migration${colors.reset}\n`);
|
||||
try {
|
||||
const yamlLib = require('yaml');
|
||||
const coreSchemaPath = path.join(__dirname, '..', 'src', 'core-skills', 'module.yaml');
|
||||
const bmmSchemaPath = path.join(__dirname, '..', 'src', 'bmm-skills', 'module.yaml');
|
||||
const coreSchema = yamlLib.parse(await fs.readFile(coreSchemaPath, 'utf8'));
|
||||
const bmmSchema = yamlLib.parse(await fs.readFile(bmmSchemaPath, 'utf8'));
|
||||
|
||||
assert(
|
||||
coreSchema.project_name && coreSchema.project_name.prompt && coreSchema.project_name.default === '{directory_name}',
|
||||
'core/module.yaml declares project_name with {directory_name} default',
|
||||
);
|
||||
|
||||
assert(coreSchema.project_name.scope === undefined, 'project_name has no user scope (project-scoped, not user-scoped)');
|
||||
|
||||
assert(bmmSchema.project_name === undefined, 'bmm/module.yaml no longer declares project_name (now inherited from core)');
|
||||
|
||||
// Set up a mock existing install: bmm directory has project_name (legacy),
|
||||
// core has user_name but not project_name. After hoist, project_name should
|
||||
// move to core, leaving bmm with only its own keys.
|
||||
const fixtureRoot43 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43-'));
|
||||
const bmadDir43 = path.join(fixtureRoot43, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43, 'bmm'));
|
||||
await fs.writeFile(path.join(bmadDir43, 'core', 'config.yaml'), 'user_name: alice\n', 'utf8');
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir43, 'bmm', 'config.yaml'),
|
||||
'project_name: legacy-from-bmm\nuser_skill_level: intermediate\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const officialModules43 = new OfficialModules();
|
||||
await officialModules43.loadExistingConfig(fixtureRoot43);
|
||||
|
||||
assert(
|
||||
officialModules43.existingConfig.core?.project_name === 'legacy-from-bmm',
|
||||
'loadExistingConfig hoists bmm.project_name to core on existing-install upgrade',
|
||||
);
|
||||
|
||||
assert(
|
||||
!('project_name' in (officialModules43.existingConfig.bmm || {})),
|
||||
'loadExistingConfig removes project_name from bmm after hoisting',
|
||||
);
|
||||
|
||||
assert(
|
||||
officialModules43.existingConfig.bmm?.user_skill_level === 'intermediate',
|
||||
'loadExistingConfig leaves non-core bmm keys (user_skill_level) untouched',
|
||||
);
|
||||
|
||||
assert(officialModules43.existingConfig.core?.user_name === 'alice', 'loadExistingConfig preserves pre-existing core values');
|
||||
|
||||
// Precedence: if core already has the key, hoist must NOT overwrite it.
|
||||
const fixtureRoot43b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43b-'));
|
||||
const bmadDir43b = path.join(fixtureRoot43b, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43b, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43b, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43b, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43b, 'bmm'));
|
||||
await fs.writeFile(path.join(bmadDir43b, 'core', 'config.yaml'), 'project_name: from-core\n', 'utf8');
|
||||
await fs.writeFile(path.join(bmadDir43b, 'bmm', 'config.yaml'), 'project_name: stale-from-bmm\n', 'utf8');
|
||||
|
||||
const officialModules43b = new OfficialModules();
|
||||
await officialModules43b.loadExistingConfig(fixtureRoot43b);
|
||||
|
||||
assert(officialModules43b.existingConfig.core?.project_name === 'from-core', 'hoist does not overwrite an existing core value');
|
||||
|
||||
assert(
|
||||
!('project_name' in (officialModules43b.existingConfig.bmm || {})),
|
||||
'hoist still strips the duplicate from bmm so writeCentralConfig partition stays clean',
|
||||
);
|
||||
|
||||
// Malformed config.yaml (parses to a scalar) must not crash loadExistingConfig
|
||||
// or the hoist pass — they should treat it as "no config for that module"
|
||||
// and continue. Regression for augment review on PR #2348.
|
||||
const fixtureRoot43c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43c-'));
|
||||
const bmadDir43c = path.join(fixtureRoot43c, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43c, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43c, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43c, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43c, 'bmm'));
|
||||
// Scalar YAML — yaml.parse returns the literal 42 (truthy non-object).
|
||||
// Pre-fix this crashed _hoistCoreKeysFromLegacyModuleConfigs with
|
||||
// "Cannot use 'in' operator to search for 'project_name' in 42".
|
||||
await fs.writeFile(path.join(bmadDir43c, 'core', 'config.yaml'), '42\n', 'utf8');
|
||||
await fs.writeFile(path.join(bmadDir43c, 'bmm', 'config.yaml'), 'project_name: rescued\n', 'utf8');
|
||||
|
||||
const officialModules43c = new OfficialModules();
|
||||
let crashErr;
|
||||
try {
|
||||
await officialModules43c.loadExistingConfig(fixtureRoot43c);
|
||||
} catch (error) {
|
||||
crashErr = error;
|
||||
}
|
||||
assert(!crashErr, 'loadExistingConfig does not crash on a scalar core/config.yaml', crashErr?.stack);
|
||||
|
||||
assert(
|
||||
officialModules43c.existingConfig.core?.project_name === 'rescued',
|
||||
'scalar core gets replaced with {} and bmm.project_name still hoists in',
|
||||
);
|
||||
|
||||
await fs.remove(fixtureRoot43).catch(() => {});
|
||||
await fs.remove(fixtureRoot43b).catch(() => {});
|
||||
await fs.remove(fixtureRoot43c).catch(() => {});
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 43 setup failed: ${error.message}${colors.reset}`);
|
||||
console.log(error.stack);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ module.exports = {
|
|||
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
|
||||
[
|
||||
'--tools <tools>',
|
||||
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
|
||||
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Required for fresh non-interactive (--yes) installs. Run with --list-tools to see all valid IDs.',
|
||||
],
|
||||
['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
|
||||
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
||||
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
||||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||
|
|
@ -40,6 +41,12 @@ module.exports = {
|
|||
],
|
||||
action: async (options) => {
|
||||
try {
|
||||
if (options.listTools) {
|
||||
const { formatPlatformList } = require('../ide/platform-codes');
|
||||
process.stdout.write((await formatPlatformList()) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Set debug flag as environment variable for all components
|
||||
if (options.debug) {
|
||||
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
||||
|
|
@ -81,7 +88,7 @@ module.exports = {
|
|||
} else {
|
||||
await prompts.log.error(`Installation failed: ${error.message}`);
|
||||
}
|
||||
if (error.stack) {
|
||||
if (error.stack && !error.expected) {
|
||||
await prompts.log.message(error.stack);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -923,29 +923,15 @@ class Installer {
|
|||
/**
|
||||
* Merge all module-help.csv files into a single bmad-help.csv.
|
||||
* Scans all installed modules for module-help.csv and merges them.
|
||||
* Enriches agent info from the in-memory agent list produced by ManifestGenerator.
|
||||
* Output is written to _bmad/_config/bmad-help.csv.
|
||||
* Output preserves the source schema verbatim — see schema below.
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...)
|
||||
* @param {Array<Object>} _agentEntries - Unused; retained for call-site compatibility
|
||||
*/
|
||||
async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) {
|
||||
async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
|
||||
const allRows = [];
|
||||
const headerRow =
|
||||
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
||||
|
||||
// Build agent lookup from the in-memory list (agent code → command + display fields).
|
||||
const agentInfo = new Map();
|
||||
for (const agent of agentEntries) {
|
||||
if (!agent || !agent.code) continue;
|
||||
const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
|
||||
const displayName = agent.name || agent.code;
|
||||
const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
|
||||
agentInfo.set(agent.code, {
|
||||
command: agentCommand,
|
||||
displayName,
|
||||
title: titleCombined,
|
||||
});
|
||||
}
|
||||
const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
|
||||
const COLUMN_COUNT = 13;
|
||||
const PHASE_INDEX = 7;
|
||||
|
||||
// Get all installed module directories
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
|
|
@ -984,64 +970,19 @@ class Installer {
|
|||
|
||||
// Parse the line - handle quoted fields with commas
|
||||
const columns = this.parseCSVLine(line);
|
||||
if (columns.length >= 12) {
|
||||
// Map old schema to new schema
|
||||
// Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
|
||||
// New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
|
||||
if (columns.length < COLUMN_COUNT - 1) continue;
|
||||
|
||||
const [
|
||||
module,
|
||||
phase,
|
||||
name,
|
||||
code,
|
||||
sequence,
|
||||
workflowFile,
|
||||
command,
|
||||
required,
|
||||
agentName,
|
||||
options,
|
||||
description,
|
||||
outputLocation,
|
||||
outputs,
|
||||
] = columns;
|
||||
// Pad short rows; truncate over-long rows
|
||||
const padded = columns.slice(0, COLUMN_COUNT);
|
||||
while (padded.length < COLUMN_COUNT) padded.push('');
|
||||
|
||||
// Pass through _meta rows as-is (module metadata, not a skill)
|
||||
if (phase === '_meta') {
|
||||
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
||||
const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', ''];
|
||||
allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(','));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
|
||||
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
||||
|
||||
// Lookup agent info
|
||||
const cleanAgentName = agentName ? agentName.trim() : '';
|
||||
const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
|
||||
|
||||
// Build new row with agent info
|
||||
const newRow = [
|
||||
finalModule,
|
||||
phase || '',
|
||||
name || '',
|
||||
code || '',
|
||||
sequence || '',
|
||||
workflowFile || '',
|
||||
command || '',
|
||||
required || 'false',
|
||||
cleanAgentName,
|
||||
agentData.command,
|
||||
agentData.displayName,
|
||||
agentData.title,
|
||||
options || '',
|
||||
description || '',
|
||||
outputLocation || '',
|
||||
outputs || '',
|
||||
];
|
||||
|
||||
allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
|
||||
// If module column is empty, fill with this module's name
|
||||
// (core stays empty so its rows render as universal tools)
|
||||
if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') {
|
||||
padded[0] = moduleName;
|
||||
}
|
||||
|
||||
allRows.push(padded.map((c) => this.escapeCSVField(c)).join(','));
|
||||
}
|
||||
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
|
|
@ -1053,44 +994,34 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort by module, then phase, then sequence
|
||||
allRows.sort((a, b) => {
|
||||
const colsA = this.parseCSVLine(a);
|
||||
const colsB = this.parseCSVLine(b);
|
||||
// Sort by module, then phase. Stable sort preserves authored order within a phase.
|
||||
const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) }));
|
||||
decorated.sort((a, b) => {
|
||||
const moduleA = (a.cols[0] || '').toLowerCase();
|
||||
const moduleB = (b.cols[0] || '').toLowerCase();
|
||||
if (moduleA !== moduleB) return moduleA.localeCompare(moduleB);
|
||||
|
||||
// Module comparison (empty module/universal tools come first)
|
||||
const moduleA = (colsA[0] || '').toLowerCase();
|
||||
const moduleB = (colsB[0] || '').toLowerCase();
|
||||
if (moduleA !== moduleB) {
|
||||
return moduleA.localeCompare(moduleB);
|
||||
}
|
||||
const phaseA = a.cols[PHASE_INDEX] || '';
|
||||
const phaseB = b.cols[PHASE_INDEX] || '';
|
||||
if (phaseA !== phaseB) return phaseA.localeCompare(phaseB);
|
||||
|
||||
// Phase comparison
|
||||
const phaseA = colsA[1] || '';
|
||||
const phaseB = colsB[1] || '';
|
||||
if (phaseA !== phaseB) {
|
||||
return phaseA.localeCompare(phaseB);
|
||||
}
|
||||
|
||||
// Sequence comparison
|
||||
const seqA = parseInt(colsA[4] || '0', 10);
|
||||
const seqB = parseInt(colsB[4] || '0', 10);
|
||||
return seqA - seqB;
|
||||
return a.index - b.index;
|
||||
});
|
||||
const sortedRows = decorated.map((d) => d.row);
|
||||
|
||||
// Write merged catalog
|
||||
const outputDir = path.join(bmadDir, '_config');
|
||||
await fs.ensureDir(outputDir);
|
||||
const outputPath = path.join(outputDir, 'bmad-help.csv');
|
||||
|
||||
const mergedContent = [headerRow, ...allRows].join('\n');
|
||||
const mergedContent = [headerRow, ...sortedRows].join('\n');
|
||||
await fs.writeFile(outputPath, mergedContent, 'utf8');
|
||||
|
||||
// Track the installed file
|
||||
this.installedFiles.add(outputPath);
|
||||
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`);
|
||||
await prompts.log.message(` Generated bmad-help.csv: ${sortedRows.length} workflows`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,11 +52,44 @@ function isSafeCanonicalId(value) {
|
|||
return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value) && !value.includes('..');
|
||||
}
|
||||
|
||||
// Default body template for command pointer files. Used when a platform's
|
||||
// installer config doesn't override `commands_body_template`. Matches
|
||||
// OpenCode's native `@skills/<id>` skill-reference syntax.
|
||||
const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}';
|
||||
|
||||
// Persona-agent id outside the `-agent-` naming convention.
|
||||
// TEA's Murat is the only persona whose canonical id is the bare module code
|
||||
// rather than `<module>-agent-<role>`. Listed here so platforms that filter
|
||||
// for "agents only" (e.g. GitHub Copilot's Custom Agents picker) include it.
|
||||
const NON_CONVENTIONAL_AGENT_IDS = new Set(['bmad-tea']);
|
||||
|
||||
// Is this skill a persona agent (vs. a workflow/tool/standalone skill)?
|
||||
// Used by platforms that surface only persona agents (e.g. Copilot's Custom
|
||||
// Agents picker). Rule: canonical id contains `-agent-` OR is in the
|
||||
// known non-conventional allowlist. Tested against the full installed
|
||||
// manifest — catches all 20 description-confirmed personas across BMM,
|
||||
// CIS, GDS, WDS, TEA without false positives.
|
||||
function isAgentSkill(canonicalId) {
|
||||
if (typeof canonicalId !== 'string') return false;
|
||||
if (NON_CONVENTIONAL_AGENT_IDS.has(canonicalId)) return true;
|
||||
return canonicalId.includes('-agent-');
|
||||
}
|
||||
|
||||
// Resolve placeholders in a body template. Supported placeholders:
|
||||
// {canonicalId} — the skill's canonical id
|
||||
// {target_dir} — the platform's skill install directory (e.g. .agents/skills)
|
||||
// {project-root} — left as a literal placeholder for the model/tool to expand
|
||||
// at runtime; consistent with PR #1769's templates.
|
||||
function expandBodyTemplate(template, { canonicalId, targetDir }) {
|
||||
return template.replaceAll('{canonicalId}', canonicalId).replaceAll('{target_dir}', targetDir);
|
||||
}
|
||||
|
||||
// The exact body the installer would generate for a given description and
|
||||
// canonicalId. Centralised so both the write and the freshness-check paths
|
||||
// agree on the canonical form.
|
||||
function buildCommandPointerBody(description, canonicalId) {
|
||||
return `---\ndescription: ${yamlSafeSingleLine(description)}\n---\n\n@skills/${canonicalId}\n`;
|
||||
// canonicalId, given the platform's body template. Centralised so both the
|
||||
// write and the freshness-check paths agree on the canonical form.
|
||||
function buildCommandPointerBody(description, canonicalId, { template, targetDir }) {
|
||||
const bodyText = expandBodyTemplate(template, { canonicalId, targetDir });
|
||||
return `---\ndescription: ${yamlSafeSingleLine(description)}\n---\n\n${bodyText}\n`;
|
||||
}
|
||||
|
||||
// Heuristic: does an existing pointer file look like our generator's output
|
||||
|
|
@ -64,11 +97,12 @@ function buildCommandPointerBody(description, canonicalId) {
|
|||
// preserve)? We check the body shape rather than full equality so that
|
||||
// description-only edits in the manifest can propagate without trampling
|
||||
// hand edits to the body.
|
||||
function looksLikeGeneratorOutput(content, canonicalId) {
|
||||
function looksLikeGeneratorOutput(content, canonicalId, { template, targetDir }) {
|
||||
if (typeof content !== 'string') return false;
|
||||
const trimmed = content.trim();
|
||||
// Must end with the exact reference line our generator writes.
|
||||
if (!trimmed.endsWith(`@skills/${canonicalId}`)) return false;
|
||||
const expectedTail = expandBodyTemplate(template, { canonicalId, targetDir }).trim();
|
||||
// Must end with the exact body our generator writes (post-expansion).
|
||||
if (!trimmed.endsWith(expectedTail)) return false;
|
||||
// Must start with frontmatter containing exactly one description: line.
|
||||
const fmMatch = trimmed.match(/^---\n([\S\s]*?)\n---\n/);
|
||||
if (!fmMatch) return false;
|
||||
|
|
@ -249,6 +283,7 @@ class ConfigDrivenIdeSetup {
|
|||
skippedExisting: 0,
|
||||
skippedCollision: 0,
|
||||
skippedInvalidId: 0,
|
||||
skippedFiltered: 0,
|
||||
writeFailures: 0,
|
||||
fallbackDescription: 0,
|
||||
};
|
||||
|
|
@ -259,6 +294,12 @@ class ConfigDrivenIdeSetup {
|
|||
const commandsPath = path.join(projectDir, config.commands_target_dir);
|
||||
await fs.ensureDir(commandsPath);
|
||||
|
||||
// Per-platform pointer-file shape, all overrideable in platform-codes.yaml.
|
||||
const extension = config.commands_extension || '.md';
|
||||
const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE;
|
||||
const targetDir = config.target_dir;
|
||||
const filter = config.commands_filter || null;
|
||||
|
||||
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||
|
||||
|
|
@ -274,6 +315,15 @@ class ConfigDrivenIdeSetup {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Optional per-platform filter: surfaces that should only show
|
||||
// persona agents (e.g. Copilot's Custom Agents picker) skip
|
||||
// workflow/tool skills here so the picker isn't cluttered with
|
||||
// 90+ unrelated entries.
|
||||
if (filter === 'agents-only' && !isAgentSkill(canonicalId)) {
|
||||
result.skippedFiltered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserved-name guard is OpenCode-specific. Other adapters that opt
|
||||
// into commands_target_dir later should declare their own reserved
|
||||
// set rather than inheriting OpenCode's.
|
||||
|
|
@ -288,8 +338,8 @@ class ConfigDrivenIdeSetup {
|
|||
result.fallbackDescription++;
|
||||
}
|
||||
|
||||
const body = buildCommandPointerBody(description, canonicalId);
|
||||
const commandFile = path.join(commandsPath, `${canonicalId}.md`);
|
||||
const body = buildCommandPointerBody(description, canonicalId, { template, targetDir });
|
||||
const commandFile = path.join(commandsPath, `${canonicalId}${extension}`);
|
||||
|
||||
// If a pointer file already exists, decide whether to overwrite based
|
||||
// on whether it looks like generator output (description-only diff) or
|
||||
|
|
@ -309,7 +359,7 @@ class ConfigDrivenIdeSetup {
|
|||
result.skippedExisting++;
|
||||
continue;
|
||||
}
|
||||
if (looksLikeGeneratorOutput(existing, canonicalId)) {
|
||||
if (looksLikeGeneratorOutput(existing, canonicalId, { template, targetDir })) {
|
||||
// Description (or other generated bit) has changed; refresh in place.
|
||||
try {
|
||||
await fs.writeFile(commandFile, body, 'utf8');
|
||||
|
|
@ -317,7 +367,7 @@ class ConfigDrivenIdeSetup {
|
|||
} catch (error) {
|
||||
result.writeFailures++;
|
||||
if (!options.silent) {
|
||||
await prompts.log.warn(`Failed to update command pointer ${canonicalId}.md: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to update command pointer ${canonicalId}${extension}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
|
@ -333,7 +383,7 @@ class ConfigDrivenIdeSetup {
|
|||
} catch (error) {
|
||||
result.writeFailures++;
|
||||
if (!options.silent) {
|
||||
await prompts.log.warn(`Failed to write command pointer ${canonicalId}.md: ${error.message}`);
|
||||
await prompts.log.warn(`Failed to write command pointer ${canonicalId}${extension}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -486,7 +536,15 @@ class ConfigDrivenIdeSetup {
|
|||
// so its pointers should go with it.
|
||||
const isInstallFlow = options.previousSkillIds && options.previousSkillIds.size > 0;
|
||||
const activeSkillIds = isInstallFlow ? await this._readActiveSkillIds(resolvedBmadDir) : new Set();
|
||||
await this.cleanupCommandPointers(projectDir, this.installerConfig.commands_target_dir, options, removalSet, activeSkillIds);
|
||||
const extension = this.installerConfig.commands_extension || '.md';
|
||||
await this.cleanupCommandPointers(
|
||||
projectDir,
|
||||
this.installerConfig.commands_target_dir,
|
||||
options,
|
||||
removalSet,
|
||||
activeSkillIds,
|
||||
extension,
|
||||
);
|
||||
}
|
||||
|
||||
// Skip target_dir cleanup when a peer platform owns this directory
|
||||
|
|
@ -590,9 +648,9 @@ class ConfigDrivenIdeSetup {
|
|||
|
||||
/**
|
||||
* Cleanup generated command pointer files for entries in removalSet.
|
||||
* Symmetric counterpart to installCommandPointers — removes <canonicalId>.md
|
||||
* files whose canonicalId is in the set. Removes the commands directory
|
||||
* entirely if it ends up empty.
|
||||
* Symmetric counterpart to installCommandPointers — removes
|
||||
* `<canonicalId><extension>` files whose canonicalId is in the set. Removes
|
||||
* the commands directory entirely if it ends up empty.
|
||||
* @param {string} projectDir
|
||||
* @param {string} commandsTargetDir - Relative dir (e.g. .opencode/commands)
|
||||
* @param {Object} options
|
||||
|
|
@ -603,8 +661,19 @@ class ConfigDrivenIdeSetup {
|
|||
* same skills are about to be re-installed) doesn't wipe hand-edited
|
||||
* pointer files. Pass an empty set or omit to delete every match in
|
||||
* removalSet (uninstall flow).
|
||||
* @param {string} [extension] - Pointer file extension (default '.md');
|
||||
* matches the platform's commands_extension config value so cleanup
|
||||
* correctly identifies pointer files for IDEs whose convention isn't .md
|
||||
* (e.g. Copilot's `.agent.md`).
|
||||
*/
|
||||
async cleanupCommandPointers(projectDir, commandsTargetDir, options = {}, removalSet = new Set(), activeSkillIds = new Set()) {
|
||||
async cleanupCommandPointers(
|
||||
projectDir,
|
||||
commandsTargetDir,
|
||||
options = {},
|
||||
removalSet = new Set(),
|
||||
activeSkillIds = new Set(),
|
||||
extension = '.md',
|
||||
) {
|
||||
if (!removalSet || removalSet.size === 0) return;
|
||||
|
||||
const commandsPath = path.join(projectDir, commandsTargetDir);
|
||||
|
|
@ -618,8 +687,8 @@ class ConfigDrivenIdeSetup {
|
|||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.endsWith('.md')) continue;
|
||||
const canonicalId = entry.slice(0, -3);
|
||||
if (!entry.endsWith(extension)) continue;
|
||||
const canonicalId = entry.slice(0, -extension.length);
|
||||
if (!removalSet.has(canonicalId)) continue;
|
||||
// Spare pointers for skills that are still in the manifest; the
|
||||
// install pass will refresh them in place if their content has gone
|
||||
|
|
|
|||
|
|
@ -31,7 +31,50 @@ function clearCache() {
|
|||
_cachedPlatformCodes = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the installable platform list for human-readable output (used by --list-tools).
|
||||
* Sourced from IdeManager so this view matches what --tools accepts at install time
|
||||
* (suspended platforms excluded).
|
||||
* @returns {Promise<string>} Formatted multi-line string with id, name, target_dir, preferred flag.
|
||||
*/
|
||||
async function formatPlatformList() {
|
||||
const { IdeManager } = require('./manager');
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
|
||||
const entries = ideManager.getAvailableIdes().map((ide) => {
|
||||
const handler = ideManager.handlers.get(ide.value);
|
||||
return {
|
||||
id: ide.value,
|
||||
name: ide.name,
|
||||
targetDir: handler?.installerConfig?.target_dir || '',
|
||||
preferred: ide.preferred,
|
||||
};
|
||||
});
|
||||
|
||||
const idWidth = Math.max(...entries.map((e) => e.id.length), 'ID'.length);
|
||||
const nameWidth = Math.max(...entries.map((e) => e.name.length), 'Name'.length);
|
||||
|
||||
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
||||
const lines = [
|
||||
`Supported tool IDs (pass via --tools <id>[,<id>...]):`,
|
||||
'',
|
||||
` ${pad('ID', idWidth)} ${pad('Name', nameWidth)} Target dir`,
|
||||
` ${pad('-'.repeat(idWidth), idWidth)} ${pad('-'.repeat(nameWidth), nameWidth)} ${'-'.repeat(10)}`,
|
||||
];
|
||||
|
||||
for (const e of entries) {
|
||||
const star = e.preferred ? ' *' : ' ';
|
||||
lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`);
|
||||
}
|
||||
|
||||
lines.push('', '* = recommended / preferred', '', 'Example: bmad-method install --modules bmm --tools claude-code');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadPlatformCodes,
|
||||
clearCache,
|
||||
formatPlatformList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -132,6 +132,17 @@ platforms:
|
|||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
commands_target_dir: .github/agents
|
||||
commands_extension: .agent.md
|
||||
commands_body_template: "LOAD the FULL {project-root}/{target_dir}/{canonicalId}/SKILL.md, READ its entire contents and follow its directions exactly!"
|
||||
# The Custom Agents picker should only show persona agents (not
|
||||
# workflows/tools). BMAD's persona agents are conventionally named
|
||||
# with an `-agent-` segment in their canonical id (e.g.
|
||||
# `bmad-agent-pm`, `gds-agent-game-dev`, `wds-agent-freya-ux`,
|
||||
# `bmad-cis-agent-storyteller`). The one exception is `bmad-tea` —
|
||||
# TEA's Murat persona uses the module code as its id rather than the
|
||||
# `-agent-` convention. This filter keeps the picker uncluttered.
|
||||
commands_filter: agents-only
|
||||
|
||||
goose:
|
||||
name: "Block Goose"
|
||||
|
|
|
|||
|
|
@ -903,7 +903,10 @@ class OfficialModules {
|
|||
try {
|
||||
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const moduleConfig = yaml.parse(content);
|
||||
if (moduleConfig) {
|
||||
// Only keep plain object parses. A corrupt config.yaml that parses
|
||||
// to a scalar or array would crash later code that does `key in cfg`
|
||||
// / `Object.keys(cfg)`; treat it the same as a parse error.
|
||||
if (moduleConfig && typeof moduleConfig === 'object' && !Array.isArray(moduleConfig)) {
|
||||
this._existingConfig[entry.name] = moduleConfig;
|
||||
foundAny = true;
|
||||
}
|
||||
|
|
@ -914,9 +917,58 @@ class OfficialModules {
|
|||
}
|
||||
}
|
||||
|
||||
if (foundAny) {
|
||||
await this._hoistCoreKeysFromLegacyModuleConfigs();
|
||||
}
|
||||
|
||||
return foundAny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate prior answers when a key has moved from a non-core module to core
|
||||
* (e.g. project_name moving from bmm to core in #2279). Without this, the
|
||||
* partition logic in writeCentralConfig drops the value from the bmm bucket
|
||||
* (because it's now a core key) without re-homing it under [core], so the
|
||||
* user's prior answer silently disappears on the next install/quick-update.
|
||||
*/
|
||||
async _hoistCoreKeysFromLegacyModuleConfigs() {
|
||||
const coreSchemaPath = path.join(getSourcePath(), 'core-skills', 'module.yaml');
|
||||
if (!(await fs.pathExists(coreSchemaPath))) return;
|
||||
|
||||
let coreSchema;
|
||||
try {
|
||||
coreSchema = yaml.parse(await fs.readFile(coreSchemaPath, 'utf8'));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!coreSchema || typeof coreSchema !== 'object') return;
|
||||
|
||||
const coreKeys = new Set(
|
||||
Object.entries(coreSchema)
|
||||
.filter(([, v]) => v && typeof v === 'object' && 'prompt' in v)
|
||||
.map(([k]) => k),
|
||||
);
|
||||
if (coreKeys.size === 0) return;
|
||||
|
||||
// Belt-and-suspenders: loadExistingConfig already filters non-object parses,
|
||||
// but anyone calling _hoistCoreKeysFromLegacyModuleConfigs in isolation (or
|
||||
// future code paths populating _existingConfig directly) shouldn't be able
|
||||
// to crash this with a scalar / array.
|
||||
const existingCore = this._existingConfig.core;
|
||||
this._existingConfig.core = existingCore && typeof existingCore === 'object' && !Array.isArray(existingCore) ? existingCore : {};
|
||||
|
||||
for (const [moduleName, cfg] of Object.entries(this._existingConfig)) {
|
||||
if (moduleName === 'core' || !cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue;
|
||||
for (const key of Object.keys(cfg)) {
|
||||
if (!coreKeys.has(key)) continue;
|
||||
if (!(key in this._existingConfig.core)) {
|
||||
this._existingConfig.core[key] = cfg[key];
|
||||
}
|
||||
delete cfg[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-scan module schemas to gather metadata for the configuration gateway prompt.
|
||||
* Returns info about which modules have configurable options.
|
||||
|
|
|
|||
|
|
@ -404,6 +404,37 @@ class UI {
|
|||
* @param {Object} options - Command-line options
|
||||
* @returns {Object} Tool configuration
|
||||
*/
|
||||
_parseToolsFlag(toolsArg, allKnownValues) {
|
||||
const selectedIdes = toolsArg
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (selectedIdes.length === 0) {
|
||||
const err = new Error(
|
||||
'--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.',
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const unknown = selectedIdes.filter((id) => !allKnownValues.has(id));
|
||||
if (unknown.length > 0) {
|
||||
const err = new Error(
|
||||
[
|
||||
`Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`,
|
||||
'',
|
||||
'Run with --list-tools to see all valid IDs.',
|
||||
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||
].join('\n'),
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return selectedIdes;
|
||||
}
|
||||
|
||||
async promptToolSelection(projectDir, options = {}) {
|
||||
const { ExistingInstall } = require('./core/existing-install');
|
||||
const { Installer } = require('./core/installer');
|
||||
|
|
@ -438,15 +469,10 @@ class UI {
|
|||
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);
|
||||
// Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
|
||||
// gets a specific "passed empty" error instead of being silently ignored.
|
||||
if (options.tools !== undefined) {
|
||||
const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
|
|
@ -522,21 +548,13 @@ class UI {
|
|||
|
||||
let selectedIdes = [];
|
||||
|
||||
// Check if tools are provided via command-line
|
||||
if (options.tools) {
|
||||
// Check for explicit "none" value to skip tool installation
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||
return { ides: [], skipIde: true };
|
||||
} else {
|
||||
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 };
|
||||
}
|
||||
// Check if tools are provided via command-line.
|
||||
// Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
|
||||
if (options.tools !== undefined) {
|
||||
selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
} else if (options.yes) {
|
||||
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
|
||||
if (configuredIdes.length > 0) {
|
||||
|
|
@ -544,8 +562,18 @@ class UI {
|
|||
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||
return { ides: configuredIdes, skipIde: false };
|
||||
} else {
|
||||
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
|
||||
return { ides: [], skipIde: true };
|
||||
const err = new Error(
|
||||
[
|
||||
'--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.',
|
||||
'',
|
||||
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||
'See all supported tools: bmad-method install --list-tools',
|
||||
'',
|
||||
'Example: bmad-method install --modules bmm --tools claude-code -y',
|
||||
].join('\n'),
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -730,6 +758,9 @@ class UI {
|
|||
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
|
||||
configCollector.collectedConfig.core = {
|
||||
user_name: defaultUsername,
|
||||
// {directory_name} default per src/core-skills/module.yaml — matches what the
|
||||
// interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
|
||||
project_name: path.basename(directory),
|
||||
communication_language: 'English',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
|
|||
Loading…
Reference in New Issue