mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
Compare commits
6 Commits
v6.0.0-Bet
...
0d2b8c3429
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d2b8c3429 | ||
|
|
984bd9e558 | ||
|
|
6a282f86b4 | ||
|
|
6c5381b6dc | ||
|
|
27c18e0020 | ||
|
|
9ebc4ce9c0 |
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [6.0.0-Beta.0]
|
||||
## [6.0.0-Beta.2]
|
||||
|
||||
- Fix installer so commands match what is installed, centralize most ide into a central file instead of separate files for each ide.
|
||||
- Specific IDEs may still need udpates, but all is config driven now and should be easier to maintain
|
||||
- Kiro still needs updates, but its been in this state since contributed, will investigate soon
|
||||
- Any version older than Beta.0 will recommend removal and reinstall to project. From later alphas though its sufficient to quick update if still desired, but best is just start fresh with Beta.
|
||||
|
||||
## [6.0.0-Beta.1]
|
||||
|
||||
**Release: January 2026 - Alpha to Beta Transition**
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ Products, platforms, complex features — structured planning then build:
|
||||
5. `/sprint-planning` — initialize sprint tracking
|
||||
6. **Repeat per story:** `/create-story` → `/dev-story` → `/code-review`
|
||||
|
||||
Every step tells you what's next. Optional phases (brainstorming, research, UX design) are available when you need them — ask `/bmad-help` anytime. For a detailed walkthrough, see the [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/getting-started-bmadv6/).
|
||||
Every step tells you what's next. Optional phases (brainstorming, research, UX design) are available when you need them — ask `/bmad-help` anytime. For a detailed walkthrough, see the [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/).
|
||||
|
||||
## Modules
|
||||
|
||||
@@ -78,8 +78,8 @@ BMad Method extends with official modules for specialized domains. Modules are a
|
||||
|
||||
**[Full Documentation](http://docs.bmad-method.org)** — Tutorials, how-to guides, concepts, and reference
|
||||
|
||||
- [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/getting-started-bmadv6/)
|
||||
- [Upgrading from Previous Versions](http://docs.bmad-method.org/how-to/installation/upgrade-to-v6/)
|
||||
- [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/)
|
||||
- [Upgrading from Previous Versions](http://docs.bmad-method.org/how-to/upgrade-to-v6/)
|
||||
|
||||
### For v4 Users
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||
"keywords": [
|
||||
"agile",
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
|
||||
bmm,anytime,Document Project,DP,10,_bmad/bmm/workflows/document-project/workflow.yaml,bmad_bmm_document-project,false,analyst,Create Mode,"Analyze an existing project to produce useful documentation",project-knowledge,*,
|
||||
bmm,anytime,Quick Spec,TS,20,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad_bmm_quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps utilities without extensive planning",planning_artifacts,"tech spec",
|
||||
bmm,anytime,Quick Dev,QD,30,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad_bmm_quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
||||
bmm,anytime,Correct Course,CC,40,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad_bmm_correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
||||
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad_brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session",
|
||||
bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad_bmm_create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief",
|
||||
bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad_bmm_validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report",
|
||||
bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad_bmm_prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd,
|
||||
bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad_bmm_prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report",
|
||||
bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad_bmm_create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design",
|
||||
bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad_bmm_create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report",
|
||||
,anytime,Create Dataflow,CDF,50,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad_bmm_create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram",
|
||||
,anytime,Create Diagram,CED,51,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad_bmm_create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram",
|
||||
,anytime,Create Flowchart,CFC,52,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad_bmm_create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart",
|
||||
,anytime,Create Wireframe,CEW,53,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad_bmm_create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe",
|
||||
bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad_bmm_create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture,
|
||||
bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad_bmm_create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report",
|
||||
bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad_bmm_create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories",
|
||||
bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad_bmm_create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report",
|
||||
bmm,3-solutioning,Test Design,TD,50,_bmad/bmm/workflows/testarch/test-design/workflow.yaml,bmad_bmm_testarch-test-design,false,tea,Create Mode,"Create comprehensive test scenarios ahead of development, recommended if string test compliance or assurance is needed. Very critical for distributed applications with separate front ends and backends outside of a monorepo.",planning_artifacts,"test design",
|
||||
bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad_bmm_check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report",
|
||||
bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad_bmm_sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status",
|
||||
bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad_bmm_sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,,
|
||||
bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad_bmm_create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story,
|
||||
bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad_bmm_create-story,false,sm,Validate Mode,"Validates story readiness and completeness before development work begins",implementation_artifacts,"story validation report",
|
||||
bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad_bmm_dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,,
|
||||
bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad_bmm_code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,,
|
||||
bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad_bmm_retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective,
|
||||
bmm,anytime,Document Project,DP,10,_bmad/bmm/workflows/document-project/workflow.yaml,bmad-bmm-document-project,false,analyst,Create Mode,"Analyze an existing project to produce useful documentation",project-knowledge,*,
|
||||
bmm,anytime,Quick Spec,TS,20,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps utilities without extensive planning",planning_artifacts,"tech spec",
|
||||
bmm,anytime,Quick Dev,QD,30,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
||||
bmm,anytime,Correct Course,CC,40,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
||||
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session",
|
||||
bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief",
|
||||
bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report",
|
||||
bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd,
|
||||
bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report",
|
||||
bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design",
|
||||
bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report",
|
||||
,anytime,Create Dataflow,CDF,50,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad-bmm-create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram",
|
||||
,anytime,Create Diagram,CED,51,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad-bmm-create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram",
|
||||
,anytime,Create Flowchart,CFC,52,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad-bmm-create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart",
|
||||
,anytime,Create Wireframe,CEW,53,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad-bmm-create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe",
|
||||
bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture,
|
||||
bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report",
|
||||
bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories",
|
||||
bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report",
|
||||
bmm,3-solutioning,Test Design,TD,50,_bmad/bmm/workflows/testarch/test-design/workflow.yaml,bmad-bmm-testarch-test-design,false,tea,Create Mode,"Create comprehensive test scenarios ahead of development, recommended if string test compliance or assurance is needed. Very critical for distributed applications with separate front ends and backends outside of a monorepo.",planning_artifacts,"test design",
|
||||
bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad-bmm-check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report",
|
||||
bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad-bmm-sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status",
|
||||
bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad-bmm-sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,,
|
||||
bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story,
|
||||
bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,false,sm,Validate Mode,"Validates story readiness and completeness before development work begins",implementation_artifacts,"story validation report",
|
||||
bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad-bmm-dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,,
|
||||
bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad-bmm-code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,,
|
||||
bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad-bmm-retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective,
|
||||
|
||||
|
Can't render this file because it has a wrong number of fields in line 7.
|
@@ -139,7 +139,7 @@ b) **HALT and wait for user selection.**
|
||||
#### Menu Handling Logic:
|
||||
|
||||
- IF A: Read fully and follow: `{advanced_elicitation}` with current spec content, process enhanced insights, ask user "Accept improvements? (y/n)", if yes update spec then redisplay menu, if no keep original then redisplay menu
|
||||
- IF B: Load and execute `{quick_dev_workflow}` with the final spec file (warn: fresh context is better)
|
||||
- IF B: Read the entire workflow file at `{quick_dev_workflow}` and follow the instructions with the final spec file (warn: fresh context is better)
|
||||
- IF D: Exit workflow - display final confirmation and path to spec
|
||||
- IF P: Read fully and follow: `{party_mode_exec}` with current spec content, process collaborative insights, ask user "Accept changes? (y/n)", if yes update spec then redisplay menu, if no keep original then redisplay menu
|
||||
- IF R: Execute Adversarial Review (see below)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
|
||||
core,,Brainstorming,BS,20,_bmad/core/workflows/brainstorming/workflow.md,bmad_brainstorming,false,analyst,,Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods,{output_folder}/brainstorming/brainstorming-session-{{date}}.md,,
|
||||
core,,Party Mode,PM,30,_bmad/core/workflows/party-mode/workflow.md,bmad_party-mode,false,party-mode facilitator,,Orchestrates group discussions between all installed BMAD agents enabling natural multi-agent conversations,,
|
||||
core,,bmad-help,BH,40,_bmad/core/tasks/help.md,bmad_help,false,system,,Get unstuck by showing what workflow steps come next or answering questions about what to do in the BMad Method,,
|
||||
core,,Index Docs,ID,50,_bmad/core/tasks/index-docs.xml,bmad_index-docs,false,llm,,Generates or updates an index.md of all documents in the specified directory,,
|
||||
core,,Shard Document,SD,70,_bmad/core/tasks/shard-doc.xml,bmad_shard-doc,false,llm,,Splits large markdown documents into smaller organized files based on level 2 sections,,
|
||||
core,,Editorial Review - Prose,EP,80,_bmad/core/tasks/editorial-review-prose.xml,bmad_editorial-review-prose,false,llm,reader_type,Clinical copy-editor that reviews text for communication issues,,"three-column markdown table with suggested fixes",
|
||||
core,,Editorial Review - Structure,ES,90,_bmad/core/tasks/editorial-review-structure.xml,bmad_editorial-review-structure,false,llm,,Structural editor that proposes cuts reorganization and simplification while preserving comprehension,,
|
||||
core,,Adversarial Review (General),AR,100,_bmad/core/tasks/review-adversarial-general.xml,bmad_review-adversarial-general,false,llm,,Cynically review content and produce findings,,
|
||||
core,,Brainstorming,BS,20,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,,Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods,{output_folder}/brainstorming/brainstorming-session-{{date}}.md,,
|
||||
core,,Party Mode,PM,30,_bmad/core/workflows/party-mode/workflow.md,bmad-party-mode,false,party-mode facilitator,,Orchestrates group discussions between all installed BMAD agents enabling natural multi-agent conversations,,
|
||||
core,,bmad-help,BH,40,_bmad/core/tasks/help.md,bmad-help,false,,,Get unstuck by showing what workflow steps come next or answering questions about what to do in the BMad Method,,
|
||||
core,,Index Docs,ID,50,_bmad/core/tasks/index-docs.xml,bmad-index-docs,false,,,Generates or updates an index.md of all documents in the specified directory,,
|
||||
core,,Shard Document,SD,70,_bmad/core/tasks/shard-doc.xml,bmad-shard-doc,false,,,Splits large markdown documents into smaller organized files based on level 2 sections,,
|
||||
core,,Editorial Review - Prose,EP,80,_bmad/core/tasks/editorial-review-prose.xml,bmad-editorial-review-prose,false,,,Clinical copy-editor that reviews text for communication issues,,"three-column markdown table with suggested fixes",
|
||||
core,,Editorial Review - Structure,ES,90,_bmad/core/tasks/editorial-review-structure.xml,bmad-editorial-review-structure,false,,,Structural editor that proposes cuts reorganization and simplification while preserving comprehension,,
|
||||
core,,Adversarial Review (General),AR,100,_bmad/core/tasks/review-adversarial-general.xml,bmad-review-adversarial-general,false,,,Cynically review content and produce findings,,
|
||||
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
@@ -161,56 +161,39 @@ class Installer {
|
||||
}
|
||||
|
||||
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
||||
// Ensure IDE manager is initialized
|
||||
await this.ideManager.ensureInitialized();
|
||||
|
||||
// Determine which IDEs are newly selected (not previously configured)
|
||||
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
||||
|
||||
if (newlySelectedIdes.length > 0) {
|
||||
console.log('\n'); // Add spacing before IDE questions
|
||||
|
||||
// Collect configuration for IDEs that support it
|
||||
for (const ide of newlySelectedIdes) {
|
||||
// List of IDEs that have interactive prompts
|
||||
//TODO: Why is this here, hardcoding this list here is bad, fix me!
|
||||
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes(
|
||||
ide,
|
||||
);
|
||||
try {
|
||||
const handler = this.ideManager.handlers.get(ide);
|
||||
|
||||
if (needsPrompts) {
|
||||
// Get IDE handler and collect configuration
|
||||
try {
|
||||
// Dynamically load the IDE setup module
|
||||
const ideModule = require(`../ide/${ide}`);
|
||||
|
||||
// Get the setup class (handle different export formats)
|
||||
let SetupClass;
|
||||
const className =
|
||||
ide
|
||||
.split('-')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('') + 'Setup';
|
||||
|
||||
if (ideModule[className]) {
|
||||
SetupClass = ideModule[className];
|
||||
} else if (ideModule.default) {
|
||||
SetupClass = ideModule.default;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ideSetup = new SetupClass();
|
||||
|
||||
// Check if this IDE has a collectConfiguration method
|
||||
if (typeof ideSetup.collectConfiguration === 'function') {
|
||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||
ideConfigurations[ide] = await ideSetup.collectConfiguration({
|
||||
selectedModules: selectedModules || [],
|
||||
projectDir,
|
||||
bmadDir,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// IDE doesn't have a setup file or collectConfiguration method
|
||||
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
|
||||
if (!handler) {
|
||||
console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this IDE handler has a collectConfiguration method
|
||||
// (custom installers like Codex, Kilo, Kiro-cli may have this)
|
||||
if (typeof handler.collectConfiguration === 'function') {
|
||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||
ideConfigurations[ide] = await handler.collectConfiguration({
|
||||
selectedModules: selectedModules || [],
|
||||
projectDir,
|
||||
bmadDir,
|
||||
});
|
||||
}
|
||||
// Most config-driven IDEs don't need configuration - silently skip
|
||||
} catch (error) {
|
||||
// IDE doesn't support configuration or has an error
|
||||
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1016,6 +999,9 @@ class Installer {
|
||||
|
||||
// Configure IDEs and copy documentation
|
||||
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
||||
// Ensure IDE manager is initialized (handlers may not be loaded in quick update flow)
|
||||
await this.ideManager.ensureInitialized();
|
||||
|
||||
// Filter out any undefined/null values from the IDE list
|
||||
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
# IDE Installer Standardization Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Standardize IDE installers to use **flat file naming** with **underscores** (Windows-compatible) and centralize duplicated code in shared utilities.
|
||||
|
||||
**Key Rule: All IDEs use underscore format for Windows compatibility (colons don't work on Windows).**
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### File Structure Patterns
|
||||
|
||||
| IDE | Current Pattern | Path Format |
|
||||
|-----|-----------------|-------------|
|
||||
| **claude-code** | Hierarchical | `.claude/commands/bmad/{module}/agents/{name}.md` |
|
||||
| **cursor** | Hierarchical | `.cursor/commands/bmad/{module}/agents/{name}.md` |
|
||||
| **crush** | Hierarchical | `.crush/commands/bmad/{module}/agents/{name}.md` |
|
||||
| **antigravity** | Flattened (underscores) | `.agent/workflows/bmad_module_agents_name.md` |
|
||||
| **codex** | Flattened (underscores) | `~/.codex/prompts/bmad_module_agents_name.md` |
|
||||
| **cline** | Flattened (underscores) | `.clinerules/workflows/bmad_module_type_name.md` |
|
||||
| **roo** | Flattened (underscores) | `.roo/commands/bmad_module_agent_name.md` |
|
||||
| **auggie** | Hybrid | `.augment/commands/bmad/agents/{module}-{name}.md` |
|
||||
| **iflow** | Hybrid | `.iflow/commands/bmad/agents/{module}-{name}.md` |
|
||||
| **trae** | Different (rules) | `.trae/rules/bmad-agent-{module}-{name}.md` |
|
||||
| **github-copilot** | Different (agents) | `.github/agents/bmd-custom-{module}-{name}.agent.md` |
|
||||
|
||||
### Shared Generators (in `/shared`)
|
||||
|
||||
1. `agent-command-generator.js` - generates agent launchers
|
||||
2. `task-tool-command-generator.js` - generates task/tool commands
|
||||
3. `workflow-command-generator.js` - generates workflow commands
|
||||
|
||||
All currently create artifacts with **nested relative paths** like `{module}/agents/{name}.md`
|
||||
|
||||
### Code Duplication Issues
|
||||
|
||||
1. **Flattening logic** duplicated in multiple IDEs
|
||||
2. **Agent launcher content creation** duplicated
|
||||
3. **Path transformation** duplicated
|
||||
|
||||
## Target Standardization
|
||||
|
||||
### For All IDEs (underscore format - Windows-compatible)
|
||||
|
||||
**IDEs affected:** claude-code, cursor, crush, antigravity, codex, cline, roo
|
||||
|
||||
```
|
||||
Format: bmad_{module}_{type}_{name}.md
|
||||
|
||||
Examples:
|
||||
- Agent: bmad_bmm_agents_pm.md
|
||||
- Agent: bmad_core_agents_dev.md
|
||||
- Workflow: bmad_bmm_workflows_correct-course.md
|
||||
- Task: bmad_bmm_tasks_bmad-help.md
|
||||
- Tool: bmad_core_tools_code-review.md
|
||||
- Custom: bmad_custom_agents_fred-commit-poet.md
|
||||
```
|
||||
|
||||
**Note:** Type segments (agents, workflows, tasks, tools) are filtered out from names:
|
||||
- `bmm/agents/pm.md` → `bmad_bmm_pm.md` (not `bmad_bmm_agents_pm.md`)
|
||||
|
||||
### For Hybrid IDEs (keep as-is)
|
||||
|
||||
**IDEs affected:** auggie, iflow
|
||||
|
||||
These use `{module}-{name}.md` format within subdirectories - keep as-is.
|
||||
|
||||
### Skip (drastically different)
|
||||
|
||||
**IDEs affected:** trae, github-copilot
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Create Shared Utility
|
||||
|
||||
**File:** `shared/path-utils.js`
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Convert hierarchical path to flat underscore-separated name (Windows-compatible)
|
||||
* @param {string} module - Module name (e.g., 'bmm', 'core')
|
||||
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') - filtered out
|
||||
* @param {string} name - Artifact name (e.g., 'pm', 'correct-course')
|
||||
* @returns {string} Flat filename like 'bmad_bmm_pm.md'
|
||||
*/
|
||||
function toUnderscoreName(module, type, name) {
|
||||
return `bmad_${module}_${name}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to flat underscore-separated name (Windows-compatible)
|
||||
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
||||
* @returns {string} Flat filename like 'bmad_bmm_pm.md'
|
||||
*/
|
||||
function toUnderscorePath(relativePath) {
|
||||
const withoutExt = relativePath.replace('.md', '');
|
||||
const parts = withoutExt.split(/[\/\\]/);
|
||||
// Filter out type segments (agents, workflows, tasks, tools)
|
||||
const filtered = parts.filter((p) => !TYPE_SEGMENTS.includes(p));
|
||||
return `bmad_${filtered.join('_')}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom agent underscore name
|
||||
* @param {string} agentName - Custom agent name
|
||||
* @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md'
|
||||
*/
|
||||
function customAgentUnderscoreName(agentName) {
|
||||
return `bmad_custom_${agentName}.md`;
|
||||
}
|
||||
|
||||
// Backward compatibility aliases
|
||||
const toColonName = toUnderscoreName;
|
||||
const toColonPath = toUnderscorePath;
|
||||
const toDashPath = toUnderscorePath;
|
||||
const customAgentColonName = customAgentUnderscoreName;
|
||||
const customAgentDashName = customAgentUnderscoreName;
|
||||
|
||||
module.exports = {
|
||||
toUnderscoreName,
|
||||
toUnderscorePath,
|
||||
customAgentUnderscoreName,
|
||||
// Backward compatibility
|
||||
toColonName,
|
||||
toColonPath,
|
||||
toDashPath,
|
||||
customAgentColonName,
|
||||
customAgentDashName,
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 2: Update Shared Generators
|
||||
|
||||
**Files to modify:**
|
||||
- `shared/agent-command-generator.js`
|
||||
- `shared/task-tool-command-generator.js`
|
||||
- `shared/workflow-command-generator.js`
|
||||
|
||||
**Changes:**
|
||||
1. Import path utilities
|
||||
2. Change `relativePath` to use flat format
|
||||
3. Add method `writeColonArtifacts()` for folder-based IDEs (uses underscore)
|
||||
4. Add method `writeDashArtifacts()` for flat IDEs (uses underscore)
|
||||
|
||||
### Phase 3: Update All IDEs
|
||||
|
||||
**Files to modify:**
|
||||
- `claude-code.js`
|
||||
- `cursor.js`
|
||||
- `crush.js`
|
||||
- `antigravity.js`
|
||||
- `codex.js`
|
||||
- `cline.js`
|
||||
- `roo.js`
|
||||
|
||||
**Changes:**
|
||||
1. Import utilities from path-utils
|
||||
2. Change from hierarchical to flat underscore naming
|
||||
3. Update cleanup to handle flat structure (`startsWith('bmad')`)
|
||||
|
||||
### Phase 4: Update Base Class
|
||||
|
||||
**File:** `_base-ide.js`
|
||||
|
||||
**Changes:**
|
||||
1. Mark `flattenFilename()` as `@deprecated`
|
||||
2. Add comment pointing to new path-utils
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### New Files
|
||||
- [x] Create `shared/path-utils.js`
|
||||
|
||||
### All IDEs (convert to underscore format)
|
||||
- [x] Update `shared/agent-command-generator.js` - update for underscore
|
||||
- [x] Update `shared/task-tool-command-generator.js` - update for underscore
|
||||
- [x] Update `shared/workflow-command-generator.js` - update for underscore
|
||||
- [x] Update `claude-code.js` - convert to underscore format
|
||||
- [x] Update `cursor.js` - convert to underscore format
|
||||
- [x] Update `crush.js` - convert to underscore format
|
||||
- [ ] Update `antigravity.js` - use underscore format
|
||||
- [ ] Update `codex.js` - use underscore format
|
||||
- [ ] Update `cline.js` - use underscore format
|
||||
- [ ] Update `roo.js` - use underscore format
|
||||
|
||||
### CSV Command Files
|
||||
- [x] Update `src/core/module-help.csv` - change colons to underscores
|
||||
- [x] Update `src/bmm/module-help.csv` - change colons to underscores
|
||||
|
||||
### Base Class
|
||||
- [ ] Update `_base-ide.js` - add deprecation notice
|
||||
|
||||
### Testing
|
||||
- [ ] Test claude-code installation
|
||||
- [ ] Test cursor installation
|
||||
- [ ] Test crush installation
|
||||
- [ ] Test antigravity installation
|
||||
- [ ] Test codex installation
|
||||
- [ ] Test cline installation
|
||||
- [ ] Test roo installation
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Filter type segments**: agents, workflows, tasks, tools are filtered out from flat names
|
||||
2. **Underscore format**: Universal underscore format for Windows compatibility
|
||||
3. **Custom agents**: Follow the same pattern as regular agents
|
||||
4. **Backward compatibility**: Old function names kept as aliases
|
||||
5. **Cleanup**: Will remove old `bmad:` format files on next install
|
||||
423
tools/cli/installers/lib/ide/_config-driven.js
Normal file
423
tools/cli/installers/lib/ide/_config-driven.js
Normal file
@@ -0,0 +1,423 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
|
||||
/**
|
||||
* Config-driven IDE setup handler
|
||||
*
|
||||
* This class provides a standardized way to install BMAD artifacts to IDEs
|
||||
* based on configuration in platform-codes.yaml. It eliminates the need for
|
||||
* individual installer files for each IDE.
|
||||
*
|
||||
* Features:
|
||||
* - Config-driven from platform-codes.yaml
|
||||
* - Template-based content generation
|
||||
* - Multi-target installation support (e.g., GitHub Copilot)
|
||||
* - Artifact type filtering (agents, workflows, tasks, tools)
|
||||
*/
|
||||
class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||
constructor(platformCode, platformConfig) {
|
||||
super(platformCode, platformConfig.name, platformConfig.preferred);
|
||||
this.platformConfig = platformConfig;
|
||||
this.installerConfig = platformConfig.installer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main setup method - called by IdeManager
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
* @returns {Promise<Object>} Setup result
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Clean up any old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
if (!this.installerConfig) {
|
||||
return { success: false, reason: 'no-config' };
|
||||
}
|
||||
|
||||
// Handle multi-target installations (e.g., GitHub Copilot)
|
||||
if (this.installerConfig.targets) {
|
||||
return this.installToMultipleTargets(projectDir, bmadDir, this.installerConfig.targets, options);
|
||||
}
|
||||
|
||||
// Handle single-target installations
|
||||
if (this.installerConfig.target_dir) {
|
||||
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
|
||||
}
|
||||
|
||||
return { success: false, reason: 'invalid-config' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install to a single target directory
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} config - Installation configuration
|
||||
* @param {Object} options - Setup options
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async installToTarget(projectDir, bmadDir, config, options) {
|
||||
const { target_dir, template_type, artifact_types } = config;
|
||||
const targetPath = path.join(projectDir, target_dir);
|
||||
await this.ensureDir(targetPath);
|
||||
|
||||
const selectedModules = options.selectedModules || [];
|
||||
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||
|
||||
// Install agents
|
||||
if (!artifact_types || artifact_types.includes('agents')) {
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config);
|
||||
}
|
||||
|
||||
// Install workflows
|
||||
if (!artifact_types || artifact_types.includes('workflows')) {
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
||||
}
|
||||
|
||||
// Install tasks and tools
|
||||
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath);
|
||||
results.tasks = taskToolResult.tasks || 0;
|
||||
results.tools = taskToolResult.tools || 0;
|
||||
}
|
||||
|
||||
this.printSummary(results, target_dir);
|
||||
return { success: true, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install to multiple target directories
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array} targets - Array of target configurations
|
||||
* @param {Object} options - Setup options
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||
|
||||
for (const target of targets) {
|
||||
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||
if (result.success) {
|
||||
allResults.agents += result.results.agents || 0;
|
||||
allResults.workflows += result.results.workflows || 0;
|
||||
allResults.tasks += result.results.tasks || 0;
|
||||
allResults.tools += result.results.tools || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, results: allResults };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write agent artifacts to target directory
|
||||
* @param {string} targetPath - Target directory path
|
||||
* @param {Array} artifacts - Agent artifacts
|
||||
* @param {string} templateType - Template type to use
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<number>} Count of artifacts written
|
||||
*/
|
||||
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
// Try to load platform-specific template, fall back to default-agent
|
||||
const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||
let count = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'agent');
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write workflow artifacts to target directory
|
||||
* @param {string} targetPath - Target directory path
|
||||
* @param {Array} artifacts - Workflow artifacts
|
||||
* @param {string} templateType - Template type to use
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<number>} Count of artifacts written
|
||||
*/
|
||||
async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
let count = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
// Use different template based on workflow type (YAML vs MD)
|
||||
// Default to 'default' template type, but allow override via config
|
||||
const workflowTemplateType = artifact.isYamlWorkflow
|
||||
? config.yaml_workflow_template || `${templateType}-workflow-yaml`
|
||||
: config.md_workflow_template || `${templateType}-workflow`;
|
||||
|
||||
// Fall back to default templates if specific ones don't exist
|
||||
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
|
||||
const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType);
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'workflow');
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load template based on type and configuration
|
||||
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||
* @param {Object} config - Installation configuration
|
||||
* @param {string} fallbackTemplateType - Fallback template type if requested template not found
|
||||
* @returns {Promise<string>} Template content
|
||||
*/
|
||||
async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
|
||||
const { header_template, body_template } = config;
|
||||
|
||||
// Check for separate header/body templates
|
||||
if (header_template || body_template) {
|
||||
return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
||||
}
|
||||
|
||||
// Load combined template
|
||||
const templateName = `${templateType}-${artifactType}.md`;
|
||||
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
||||
|
||||
if (await fs.pathExists(templatePath)) {
|
||||
return await fs.readFile(templatePath, 'utf8');
|
||||
}
|
||||
|
||||
// Fall back to default template (if provided)
|
||||
if (fallbackTemplateType) {
|
||||
const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`);
|
||||
if (await fs.pathExists(fallbackPath)) {
|
||||
return await fs.readFile(fallbackPath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback - minimal template
|
||||
return this.getDefaultTemplate(artifactType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load split templates (header + body)
|
||||
* @param {string} templateType - Template type
|
||||
* @param {string} artifactType - Artifact type
|
||||
* @param {string} headerTpl - Header template name
|
||||
* @param {string} bodyTpl - Body template name
|
||||
* @returns {Promise<string>} Combined template content
|
||||
*/
|
||||
async loadSplitTemplates(templateType, artifactType, headerTpl, bodyTpl) {
|
||||
let header = '';
|
||||
let body = '';
|
||||
|
||||
// Load header template
|
||||
if (headerTpl) {
|
||||
const headerPath = path.join(__dirname, 'templates', 'split', headerTpl);
|
||||
if (await fs.pathExists(headerPath)) {
|
||||
header = await fs.readFile(headerPath, 'utf8');
|
||||
}
|
||||
} else {
|
||||
// Use default header for template type
|
||||
const defaultHeaderPath = path.join(__dirname, 'templates', 'split', templateType, 'header.md');
|
||||
if (await fs.pathExists(defaultHeaderPath)) {
|
||||
header = await fs.readFile(defaultHeaderPath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
// Load body template
|
||||
if (bodyTpl) {
|
||||
const bodyPath = path.join(__dirname, 'templates', 'split', bodyTpl);
|
||||
if (await fs.pathExists(bodyPath)) {
|
||||
body = await fs.readFile(bodyPath, 'utf8');
|
||||
}
|
||||
} else {
|
||||
// Use default body for template type
|
||||
const defaultBodyPath = path.join(__dirname, 'templates', 'split', templateType, 'body.md');
|
||||
if (await fs.pathExists(defaultBodyPath)) {
|
||||
body = await fs.readFile(defaultBodyPath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
// Combine header and body
|
||||
return `${header}\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default minimal template
|
||||
* @param {string} artifactType - Artifact type
|
||||
* @returns {string} Default template
|
||||
*/
|
||||
getDefaultTemplate(artifactType) {
|
||||
if (artifactType === 'agent') {
|
||||
return `---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
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 {project-root}/{{bmadFolderName}}/{{path}}
|
||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||
3. FOLLOW every step in the <activation> section precisely
|
||||
</agent-activation>
|
||||
`;
|
||||
}
|
||||
return `---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render template with artifact data
|
||||
* @param {string} template - Template content
|
||||
* @param {Object} artifact - Artifact data
|
||||
* @returns {string} Rendered content
|
||||
*/
|
||||
renderTemplate(template, artifact) {
|
||||
// Use the appropriate path property based on artifact type
|
||||
let pathToUse = artifact.relativePath || '';
|
||||
if (artifact.type === 'agent-launcher') {
|
||||
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
||||
} else if (artifact.type === 'workflow-command') {
|
||||
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
||||
}
|
||||
|
||||
let rendered = template
|
||||
.replaceAll('{{name}}', artifact.name || '')
|
||||
.replaceAll('{{module}}', artifact.module || 'core')
|
||||
.replaceAll('{{path}}', pathToUse)
|
||||
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
|
||||
.replaceAll('{{workflow_path}}', pathToUse);
|
||||
|
||||
// Replace _bmad placeholder with actual folder name
|
||||
rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
|
||||
|
||||
// Replace {{bmadFolderName}} placeholder if present
|
||||
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate filename for artifact
|
||||
* @param {Object} artifact - Artifact data
|
||||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||
* @returns {string} Generated filename
|
||||
*/
|
||||
generateFilename(artifact, artifactType) {
|
||||
const { toDashPath } = require('./shared/path-utils');
|
||||
// toDashPath already handles the .agent.md suffix for agents correctly
|
||||
// No need to add it again here
|
||||
return toDashPath(artifact.relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print installation summary
|
||||
* @param {Object} results - Installation results
|
||||
* @param {string} targetDir - Target directory (relative)
|
||||
*/
|
||||
printSummary(results, targetDir) {
|
||||
console.log(chalk.green(`\n✓ ${this.name} configured:`));
|
||||
if (results.agents > 0) {
|
||||
console.log(chalk.dim(` - ${results.agents} agents installed`));
|
||||
}
|
||||
if (results.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
|
||||
}
|
||||
if (results.tasks > 0 || results.tools > 0) {
|
||||
console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
|
||||
}
|
||||
console.log(chalk.dim(` - Destination: ${targetDir}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup IDE configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
// Clean all target directories
|
||||
if (this.installerConfig?.targets) {
|
||||
for (const target of this.installerConfig.targets) {
|
||||
await this.cleanupTarget(projectDir, target.target_dir);
|
||||
}
|
||||
} else if (this.installerConfig?.target_dir) {
|
||||
await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup a specific target directory
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} targetDir - Target directory to clean
|
||||
*/
|
||||
async cleanupTarget(projectDir, targetDir) {
|
||||
const targetPath = path.join(projectDir, targetDir);
|
||||
|
||||
if (!(await fs.pathExists(targetPath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all bmad* files
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(targetPath);
|
||||
} catch {
|
||||
// Directory exists but can't be read - skip cleanup
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entries || !Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let removedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip non-strings or undefined entries
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (entry.startsWith('bmad')) {
|
||||
const entryPath = path.join(targetPath, entry);
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (stat.isFile()) {
|
||||
await fs.remove(entryPath);
|
||||
removedCount++;
|
||||
} else if (stat.isDirectory()) {
|
||||
await fs.remove(entryPath);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ConfigDrivenIdeSetup };
|
||||
@@ -1,474 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const {
|
||||
loadModuleInjectionConfig,
|
||||
shouldApplyInjection,
|
||||
filterAgentInstructions,
|
||||
resolveSubagentFiles,
|
||||
} = require('./shared/module-injections');
|
||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Google Antigravity IDE setup handler
|
||||
*
|
||||
* Uses .agent/workflows/ directory for slash commands
|
||||
*/
|
||||
class AntigravitySetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('antigravity', 'Google Antigravity', true);
|
||||
this.configDir = '.agent';
|
||||
this.workflowsDir = 'workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for subagent installation location
|
||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||
*/
|
||||
async _promptInstallLocation() {
|
||||
return prompts.select({
|
||||
message: 'Where would you like to install Antigravity subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration choices before installation
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
// const config = {
|
||||
// subagentChoices: null,
|
||||
// installLocation: null,
|
||||
// };
|
||||
|
||||
// const sourceModulesPath = getSourcePath('modules');
|
||||
// const modules = options.selectedModules || [];
|
||||
|
||||
// for (const moduleName of modules) {
|
||||
// // Check for Antigravity sub-module injection config in SOURCE directory
|
||||
// const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'antigravity', 'injections.yaml');
|
||||
|
||||
// if (await this.exists(injectionConfigPath)) {
|
||||
// const yaml = require('yaml');
|
||||
|
||||
// try {
|
||||
// // Load injection configuration
|
||||
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
||||
// const injectionConfig = yaml.parse(configContent);
|
||||
|
||||
// // Ask about subagents if they exist and we haven't asked yet
|
||||
// if (injectionConfig.subagents && !config.subagentChoices) {
|
||||
// config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||
|
||||
// if (config.subagentChoices.install !== 'none') {
|
||||
// config.installLocation = await this._promptInstallLocation();
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad');
|
||||
|
||||
if (await fs.pathExists(bmadWorkflowsDir)) {
|
||||
await fs.remove(bmadWorkflowsDir);
|
||||
console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Antigravity IDE configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
// Store project directory for use in processContent
|
||||
this.projectDir = projectDir;
|
||||
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .agent/workflows directory structure
|
||||
const agentDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(agentDir, this.workflowsDir);
|
||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
||||
|
||||
await this.ensureDir(bmadWorkflowsDir);
|
||||
|
||||
// Generate agent launchers using AgentCommandGenerator
|
||||
// This creates small launcher files that reference the actual agents in _bmad/
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Write agent launcher files with FLATTENED naming using shared utility
|
||||
// Antigravity ignores directory structure, so we flatten to: bmad_module_name.md
|
||||
// This creates slash commands like /bmad_bmm_dev instead of /dev
|
||||
const agentCount = await agentGen.writeDashArtifacts(bmadWorkflowsDir, agentArtifacts);
|
||||
|
||||
// Process Antigravity specific injections for installed modules
|
||||
// Use pre-collected configuration if available, or skip if already configured
|
||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
||||
// IDE is already configured from previous installation, skip prompting
|
||||
// Just process with default/existing configuration
|
||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
|
||||
} else if (options.preCollectedConfig) {
|
||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
||||
} else {
|
||||
await this.processModuleInjections(projectDir, bmadDir, options);
|
||||
}
|
||||
|
||||
// Generate workflow commands from manifest (if it exists)
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Write workflow-command artifacts with FLATTENED naming using shared utility
|
||||
const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts);
|
||||
|
||||
// Generate task and tool commands from manifests (if they exist)
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||
if (workflowCommandCount > 0) {
|
||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
||||
}
|
||||
if (taskToolResult.generated > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`));
|
||||
console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and process file content
|
||||
*/
|
||||
async readAndProcess(filePath, metadata) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return this.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override processContent to keep {project-root} placeholder
|
||||
*/
|
||||
processContent(content, metadata = {}) {
|
||||
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
|
||||
return super.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agents from source modules (not installed location)
|
||||
*/
|
||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
||||
const agents = [];
|
||||
|
||||
// Add core agents
|
||||
const corePath = getModulePath('core');
|
||||
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
||||
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
||||
agents.push(...coreAgents);
|
||||
}
|
||||
|
||||
// Add module agents
|
||||
for (const moduleName of selectedModules) {
|
||||
const modulePath = path.join(sourceDir, moduleName);
|
||||
const agentsPath = path.join(modulePath, 'agents');
|
||||
|
||||
if (await fs.pathExists(agentsPath)) {
|
||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
||||
agents.push(...moduleAgents);
|
||||
}
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process module injections with pre-collected configuration
|
||||
*/
|
||||
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
||||
// Get list of installed modules
|
||||
const modules = options.selectedModules || [];
|
||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
||||
|
||||
// Get the actual source directory (not the installation directory)
|
||||
await this.processModuleInjectionsInternal({
|
||||
projectDir,
|
||||
modules,
|
||||
handler: 'antigravity',
|
||||
subagentChoices,
|
||||
installLocation,
|
||||
interactive: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Antigravity specific injections for installed modules
|
||||
* Looks for injections.yaml in each module's antigravity sub-module
|
||||
*/
|
||||
async processModuleInjections(projectDir, bmadDir, options) {
|
||||
// Get list of installed modules
|
||||
const modules = options.selectedModules || [];
|
||||
let subagentChoices = null;
|
||||
let installLocation = null;
|
||||
|
||||
// Get the actual source directory (not the installation directory)
|
||||
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
||||
projectDir,
|
||||
modules,
|
||||
handler: 'antigravity',
|
||||
subagentChoices,
|
||||
installLocation,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
if (updatedChoices) {
|
||||
subagentChoices = updatedChoices;
|
||||
}
|
||||
if (updatedLocation) {
|
||||
installLocation = updatedLocation;
|
||||
}
|
||||
}
|
||||
|
||||
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
||||
let choices = subagentChoices;
|
||||
let location = installLocation;
|
||||
|
||||
for (const moduleName of modules) {
|
||||
const configData = await loadModuleInjectionConfig(handler, moduleName);
|
||||
|
||||
if (!configData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { config, handlerBaseDir } = configData;
|
||||
|
||||
if (interactive) {
|
||||
console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler} features...`));
|
||||
}
|
||||
|
||||
// if (interactive && config.subagents && !choices) {
|
||||
// choices = await this.promptSubagentInstallation(config.subagents);
|
||||
|
||||
// if (choices.install !== 'none') {
|
||||
// location = await this._promptInstallLocation();
|
||||
// }
|
||||
// }
|
||||
|
||||
if (config.injections && choices && choices.install !== 'none') {
|
||||
for (const injection of config.injections) {
|
||||
if (shouldApplyInjection(injection, choices)) {
|
||||
await this.injectContent(projectDir, injection, choices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.subagents && choices && choices.install !== 'none') {
|
||||
await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project');
|
||||
}
|
||||
}
|
||||
|
||||
return { subagentChoices: choices, installLocation: location };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for subagent installation preferences
|
||||
*/
|
||||
async promptSubagentInstallation(subagentConfig) {
|
||||
// First ask if they want to install subagents
|
||||
const install = await prompts.select({
|
||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
});
|
||||
|
||||
if (install === 'selective') {
|
||||
// Show list of available subagents with descriptions
|
||||
const subagentInfo = {
|
||||
'market-researcher.md': 'Market research and competitive analysis',
|
||||
'requirements-analyst.md': 'Requirements extraction and validation',
|
||||
'technical-evaluator.md': 'Technology stack evaluation',
|
||||
'epic-optimizer.md': 'Epic and story breakdown optimization',
|
||||
'document-reviewer.md': 'Document quality review',
|
||||
};
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||
choices: subagentConfig.files.map((file) => ({
|
||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
checked: true,
|
||||
})),
|
||||
});
|
||||
|
||||
return { install: 'selective', selected };
|
||||
}
|
||||
|
||||
return { install };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject content at specified point in file
|
||||
*/
|
||||
async injectContent(projectDir, injection, subagentChoices = null) {
|
||||
const targetPath = path.join(projectDir, injection.file);
|
||||
|
||||
if (await this.exists(targetPath)) {
|
||||
let content = await fs.readFile(targetPath, 'utf8');
|
||||
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
|
||||
|
||||
if (content.includes(marker)) {
|
||||
let injectionContent = injection.content;
|
||||
|
||||
// Filter content if selective subagents chosen
|
||||
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
||||
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
||||
}
|
||||
|
||||
content = content.replace(marker, injectionContent);
|
||||
await fs.writeFile(targetPath, content);
|
||||
console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy selected subagents to appropriate Antigravity agents directory
|
||||
*/
|
||||
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
||||
const os = require('node:os');
|
||||
|
||||
// Determine target directory based on user choice
|
||||
let targetDir;
|
||||
if (location === 'user') {
|
||||
targetDir = path.join(os.homedir(), '.agent', 'agents');
|
||||
console.log(chalk.dim(` Installing subagents globally to: ~/.agent/agents/`));
|
||||
} else {
|
||||
targetDir = path.join(projectDir, '.agent', 'agents');
|
||||
console.log(chalk.dim(` Installing subagents to project: .agent/agents/`));
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
await this.ensureDir(targetDir);
|
||||
|
||||
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
||||
|
||||
let copiedCount = 0;
|
||||
for (const resolved of resolvedFiles) {
|
||||
try {
|
||||
const sourcePath = resolved.absolutePath;
|
||||
|
||||
const subFolder = path.dirname(resolved.relativePath);
|
||||
let targetPath;
|
||||
if (subFolder && subFolder !== '.') {
|
||||
const targetSubDir = path.join(targetDir, subFolder);
|
||||
await this.ensureDir(targetSubDir);
|
||||
targetPath = path.join(targetSubDir, path.basename(resolved.file));
|
||||
} else {
|
||||
targetPath = path.join(targetDir, path.basename(resolved.file));
|
||||
}
|
||||
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`));
|
||||
copiedCount++;
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (copiedCount > 0) {
|
||||
console.log(chalk.dim(` Total subagents installed: ${copiedCount}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Antigravity
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
// Create .agent/workflows/bmad directory structure (same as regular agents)
|
||||
const agentDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(agentDir, this.workflowsDir);
|
||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
||||
|
||||
await fs.ensureDir(bmadWorkflowsDir);
|
||||
|
||||
// Create custom agent launcher with same pattern as regular agents
|
||||
const launcherContent = `name: '${agentName}'
|
||||
description: '${agentName} agent'
|
||||
usage: |
|
||||
Custom BMAD agent: ${agentName}
|
||||
|
||||
Launch with: /${agentName}
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||
3. EXECUTE as ${agentName} with full persona adoption
|
||||
</agent-activation>
|
||||
|
||||
---
|
||||
|
||||
⚠️ **IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
const fileName = customAgentDashName(agentName);
|
||||
const launcherPath = path.join(bmadWorkflowsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'antigravity',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: `/${fileName.replace('.md', '')}`,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AntigravitySetup };
|
||||
@@ -1,244 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
|
||||
/**
|
||||
* Auggie CLI setup handler
|
||||
* Installs to project directory (.augment/commands)
|
||||
*/
|
||||
class AuggieSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('auggie', 'Auggie CLI');
|
||||
this.detectionPaths = ['.augment'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Auggie CLI 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}...`));
|
||||
|
||||
// Always use project directory
|
||||
const location = path.join(projectDir, '.augment', 'commands');
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Get tasks, tools, and workflows (ALL workflows now generate commands)
|
||||
const tasks = await this.getTasks(bmadDir, true);
|
||||
const tools = await this.getTools(bmadDir, true);
|
||||
|
||||
// Get ALL workflows using the new workflow command generator
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Convert workflow artifacts to expected format
|
||||
const workflows = workflowArtifacts
|
||||
.filter((artifact) => artifact.type === 'workflow-command')
|
||||
.map((artifact) => ({
|
||||
module: artifact.module,
|
||||
name: path.basename(artifact.relativePath, '.md'),
|
||||
path: artifact.sourcePath,
|
||||
content: artifact.content,
|
||||
}));
|
||||
|
||||
const bmadCommandsDir = path.join(location, 'bmad');
|
||||
const agentsDir = path.join(bmadCommandsDir, 'agents');
|
||||
const tasksDir = path.join(bmadCommandsDir, 'tasks');
|
||||
const toolsDir = path.join(bmadCommandsDir, 'tools');
|
||||
const workflowsDir = path.join(bmadCommandsDir, 'workflows');
|
||||
|
||||
await this.ensureDir(agentsDir);
|
||||
await this.ensureDir(tasksDir);
|
||||
await this.ensureDir(toolsDir);
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Install agent launchers
|
||||
for (const artifact of agentArtifacts) {
|
||||
const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, artifact.content);
|
||||
}
|
||||
|
||||
// Install tasks
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const commandContent = this.createTaskCommand(task, content);
|
||||
|
||||
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
}
|
||||
|
||||
// Install tools
|
||||
for (const tool of tools) {
|
||||
const content = await this.readFile(tool.path);
|
||||
const commandContent = this.createToolCommand(tool, content);
|
||||
|
||||
const targetPath = path.join(toolsDir, `${tool.module}-${tool.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
}
|
||||
|
||||
// Install workflows (already generated commands)
|
||||
for (const workflow of workflows) {
|
||||
// Use the pre-generated workflow command content
|
||||
const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`);
|
||||
await this.writeFile(targetPath, workflow.content);
|
||||
}
|
||||
|
||||
const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length;
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentArtifacts.length} agents installed`));
|
||||
console.log(chalk.dim(` - ${tasks.length} tasks installed`));
|
||||
console.log(chalk.dim(` - ${tools.length} tools installed`));
|
||||
console.log(chalk.dim(` - ${workflows.length} workflows installed`));
|
||||
console.log(chalk.dim(` - Location: ${path.relative(projectDir, location)}`));
|
||||
console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentArtifacts.length,
|
||||
tasks: tasks.length,
|
||||
tools: tools.length,
|
||||
workflows: workflows.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task command content
|
||||
*/
|
||||
createTaskCommand(task, content) {
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
||||
|
||||
return `---
|
||||
description: "Execute the ${taskName} task"
|
||||
---
|
||||
|
||||
# ${taskName} Task
|
||||
|
||||
${content}
|
||||
|
||||
## Module
|
||||
BMAD ${task.module.toUpperCase()} module
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tool command content
|
||||
*/
|
||||
createToolCommand(tool, content) {
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name);
|
||||
|
||||
return `---
|
||||
description: "Use the ${toolName} tool"
|
||||
---
|
||||
|
||||
# ${toolName} Tool
|
||||
|
||||
${content}
|
||||
|
||||
## Module
|
||||
BMAD ${tool.module.toUpperCase()} module
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow command content
|
||||
*/
|
||||
createWorkflowCommand(workflow, content) {
|
||||
const description = workflow.description || `Execute the ${workflow.name} workflow`;
|
||||
|
||||
return `---
|
||||
description: "${description}"
|
||||
---
|
||||
|
||||
# ${workflow.name} Workflow
|
||||
|
||||
${content}
|
||||
|
||||
## Module
|
||||
BMAD ${workflow.module.toUpperCase()} module
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Auggie configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
|
||||
// Only clean up project directory
|
||||
const location = path.join(projectDir, '.augment', 'commands');
|
||||
const bmadDir = path.join(location, 'bmad');
|
||||
|
||||
if (await fs.pathExists(bmadDir)) {
|
||||
await fs.remove(bmadDir);
|
||||
console.log(chalk.dim(` Removed old BMAD commands`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Auggie
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
// Auggie uses .augment/commands directory
|
||||
const location = path.join(projectDir, '.augment', 'commands');
|
||||
const bmadCommandsDir = path.join(location, 'bmad');
|
||||
const agentsDir = path.join(bmadCommandsDir, 'agents');
|
||||
|
||||
// Create .augment/commands/bmad/agents directory if it doesn't exist
|
||||
await fs.ensureDir(agentsDir);
|
||||
|
||||
// Create custom agent launcher
|
||||
const launcherContent = `---
|
||||
description: "Use the ${agentName} custom agent"
|
||||
---
|
||||
|
||||
# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this command to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
## Module
|
||||
BMAD Custom agent
|
||||
`;
|
||||
|
||||
const fileName = `custom-${agentName.toLowerCase()}.md`;
|
||||
const launcherPath = path.join(agentsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'auggie',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AuggieSetup };
|
||||
@@ -1,506 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const {
|
||||
loadModuleInjectionConfig,
|
||||
shouldApplyInjection,
|
||||
filterAgentInstructions,
|
||||
resolveSubagentFiles,
|
||||
} = require('./shared/module-injections');
|
||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||
const { customAgentColonName } = require('./shared/path-utils');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Claude Code IDE setup handler
|
||||
*/
|
||||
class ClaudeCodeSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('claude-code', 'Claude Code', true); // preferred IDE
|
||||
this.configDir = '.claude';
|
||||
this.commandsDir = 'commands';
|
||||
this.agentsDir = 'agents';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for subagent installation location
|
||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||
*/
|
||||
async promptInstallLocation() {
|
||||
return prompts.select({
|
||||
message: 'Where would you like to install Claude Code subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Collect configuration choices before installation
|
||||
// * @param {Object} options - Configuration options
|
||||
// * @returns {Object} Collected configuration
|
||||
// */
|
||||
// async collectConfiguration(options = {}) {
|
||||
// const config = {
|
||||
// subagentChoices: null,
|
||||
// installLocation: null,
|
||||
// };
|
||||
|
||||
// const sourceModulesPath = getSourcePath('modules');
|
||||
// const modules = options.selectedModules || [];
|
||||
|
||||
// for (const moduleName of modules) {
|
||||
// // Check for Claude Code sub-module injection config in SOURCE directory
|
||||
// const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
|
||||
|
||||
// if (await this.exists(injectionConfigPath)) {
|
||||
// const yaml = require('yaml');
|
||||
|
||||
// try {
|
||||
// // Load injection configuration
|
||||
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
||||
// const injectionConfig = yaml.parse(configContent);
|
||||
|
||||
// // Ask about subagents if they exist and we haven't asked yet
|
||||
// if (injectionConfig.subagents && !config.subagentChoices) {
|
||||
// config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||
|
||||
// if (config.subagentChoices.install !== 'none') {
|
||||
// config.installLocation = await this.promptInstallLocation();
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return config;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
const entries = await fs.readdir(commandsDir);
|
||||
let removedCount = 0;
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('bmad')) {
|
||||
await fs.remove(path.join(commandsDir, entry));
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
// Also remove legacy bmad folder if it exists
|
||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadFolder)) {
|
||||
await fs.remove(bmadFolder);
|
||||
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up legacy folder structure (module/type/name.md) if it exists
|
||||
* This can be called after migration to remove old nested directories
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanupLegacyFolders(projectDir) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (!(await fs.pathExists(commandsDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove legacy bmad folder if it exists
|
||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadFolder)) {
|
||||
await fs.remove(bmadFolder);
|
||||
console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Claude Code IDE configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
// Store project directory for use in processContent
|
||||
this.projectDir = projectDir;
|
||||
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .claude/commands directory structure
|
||||
const claudeDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(claudeDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
||||
// Creates: .claude/commands/bmad_bmm_pm.md
|
||||
|
||||
// Generate agent launchers using AgentCommandGenerator
|
||||
// This creates small launcher files that reference the actual agents in _bmad/
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Write agent launcher files using flat underscore naming
|
||||
// Creates files like: bmad_bmm_pm.md
|
||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
||||
|
||||
// Process Claude Code specific injections for installed modules
|
||||
// Use pre-collected configuration if available, or skip if already configured
|
||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
||||
// IDE is already configured from previous installation, skip prompting
|
||||
// Just process with default/existing configuration
|
||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
|
||||
} else if (options.preCollectedConfig) {
|
||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
||||
} else {
|
||||
await this.processModuleInjections(projectDir, bmadDir, options);
|
||||
}
|
||||
|
||||
// Skip CLAUDE.md creation - let user manage their own CLAUDE.md file
|
||||
// await this.createClaudeConfig(projectDir, modules);
|
||||
|
||||
// Generate workflow commands from manifest (if it exists)
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Write workflow-command artifacts using flat underscore naming
|
||||
// Creates files like: bmad_bmm_correct-course.md
|
||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
||||
|
||||
// Generate task and tool commands from manifests (if they exist)
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||
if (workflowCommandCount > 0) {
|
||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
||||
}
|
||||
if (taskToolResult.generated > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
};
|
||||
}
|
||||
|
||||
// Method removed - CLAUDE.md file management left to user
|
||||
|
||||
/**
|
||||
* Read and process file content
|
||||
*/
|
||||
async readAndProcess(filePath, metadata) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return this.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override processContent to keep {project-root} placeholder
|
||||
*/
|
||||
processContent(content, metadata = {}) {
|
||||
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
|
||||
return super.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agents from source modules (not installed location)
|
||||
*/
|
||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
||||
const agents = [];
|
||||
|
||||
// Add core agents
|
||||
const corePath = getModulePath('core');
|
||||
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
||||
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
||||
agents.push(...coreAgents);
|
||||
}
|
||||
|
||||
// Add module agents
|
||||
for (const moduleName of selectedModules) {
|
||||
const modulePath = path.join(sourceDir, moduleName);
|
||||
const agentsPath = path.join(modulePath, 'agents');
|
||||
|
||||
if (await fs.pathExists(agentsPath)) {
|
||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
||||
agents.push(...moduleAgents);
|
||||
}
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process module injections with pre-collected configuration
|
||||
*/
|
||||
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
||||
// Get list of installed modules
|
||||
const modules = options.selectedModules || [];
|
||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
||||
|
||||
// Get the actual source directory (not the installation directory)
|
||||
await this.processModuleInjectionsInternal({
|
||||
projectDir,
|
||||
modules,
|
||||
handler: 'claude-code',
|
||||
subagentChoices,
|
||||
installLocation,
|
||||
interactive: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Claude Code specific injections for installed modules
|
||||
* Looks for injections.yaml in each module's claude-code sub-module
|
||||
*/
|
||||
async processModuleInjections(projectDir, bmadDir, options) {
|
||||
// Get list of installed modules
|
||||
const modules = options.selectedModules || [];
|
||||
let subagentChoices = null;
|
||||
let installLocation = null;
|
||||
|
||||
// Get the actual source directory (not the installation directory)
|
||||
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
||||
projectDir,
|
||||
modules,
|
||||
handler: 'claude-code',
|
||||
subagentChoices,
|
||||
installLocation,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
if (updatedChoices) {
|
||||
subagentChoices = updatedChoices;
|
||||
}
|
||||
if (updatedLocation) {
|
||||
installLocation = updatedLocation;
|
||||
}
|
||||
}
|
||||
|
||||
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
||||
let choices = subagentChoices;
|
||||
let location = installLocation;
|
||||
|
||||
for (const moduleName of modules) {
|
||||
const configData = await loadModuleInjectionConfig(handler, moduleName);
|
||||
|
||||
if (!configData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { config, handlerBaseDir } = configData;
|
||||
|
||||
if (interactive) {
|
||||
console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`));
|
||||
}
|
||||
|
||||
if (interactive && config.subagents && !choices) {
|
||||
// choices = await this.promptSubagentInstallation(config.subagents);
|
||||
// if (choices.install !== 'none') {
|
||||
// location = await this.promptInstallLocation();
|
||||
// }
|
||||
}
|
||||
|
||||
if (config.injections && choices && choices.install !== 'none') {
|
||||
for (const injection of config.injections) {
|
||||
if (shouldApplyInjection(injection, choices)) {
|
||||
await this.injectContent(projectDir, injection, choices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.subagents && choices && choices.install !== 'none') {
|
||||
await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project');
|
||||
}
|
||||
}
|
||||
|
||||
return { subagentChoices: choices, installLocation: location };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for subagent installation preferences
|
||||
*/
|
||||
async promptSubagentInstallation(subagentConfig) {
|
||||
// First ask if they want to install subagents
|
||||
const install = await prompts.select({
|
||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
});
|
||||
|
||||
if (install === 'selective') {
|
||||
// Show list of available subagents with descriptions
|
||||
const subagentInfo = {
|
||||
'market-researcher.md': 'Market research and competitive analysis',
|
||||
'requirements-analyst.md': 'Requirements extraction and validation',
|
||||
'technical-evaluator.md': 'Technology stack evaluation',
|
||||
'epic-optimizer.md': 'Epic and story breakdown optimization',
|
||||
'document-reviewer.md': 'Document quality review',
|
||||
};
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||
options: subagentConfig.files.map((file) => ({
|
||||
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
})),
|
||||
initialValues: subagentConfig.files,
|
||||
});
|
||||
|
||||
return { install: 'selective', selected };
|
||||
}
|
||||
|
||||
return { install };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject content at specified point in file
|
||||
*/
|
||||
async injectContent(projectDir, injection, subagentChoices = null) {
|
||||
const targetPath = path.join(projectDir, injection.file);
|
||||
|
||||
if (await this.exists(targetPath)) {
|
||||
let content = await fs.readFile(targetPath, 'utf8');
|
||||
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
|
||||
|
||||
if (content.includes(marker)) {
|
||||
let injectionContent = injection.content;
|
||||
|
||||
// Filter content if selective subagents chosen
|
||||
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
||||
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
||||
}
|
||||
|
||||
content = content.replace(marker, injectionContent);
|
||||
await fs.writeFile(targetPath, content);
|
||||
console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy selected subagents to appropriate Claude agents directory
|
||||
*/
|
||||
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
||||
const os = require('node:os');
|
||||
|
||||
// Determine target directory based on user choice
|
||||
let targetDir;
|
||||
if (location === 'user') {
|
||||
targetDir = path.join(os.homedir(), '.claude', 'agents');
|
||||
console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`));
|
||||
} else {
|
||||
targetDir = path.join(projectDir, '.claude', 'agents');
|
||||
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
await this.ensureDir(targetDir);
|
||||
|
||||
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
||||
|
||||
let copiedCount = 0;
|
||||
for (const resolved of resolvedFiles) {
|
||||
try {
|
||||
const sourcePath = resolved.absolutePath;
|
||||
|
||||
const subFolder = path.dirname(resolved.relativePath);
|
||||
let targetPath;
|
||||
if (subFolder && subFolder !== '.') {
|
||||
const targetSubDir = path.join(targetDir, subFolder);
|
||||
await this.ensureDir(targetSubDir);
|
||||
targetPath = path.join(targetSubDir, path.basename(resolved.file));
|
||||
} else {
|
||||
targetPath = path.join(targetDir, path.basename(resolved.file));
|
||||
}
|
||||
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`));
|
||||
copiedCount++;
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (copiedCount > 0) {
|
||||
console.log(chalk.dim(` Total subagents installed: ${copiedCount}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Claude Code
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
const launcherContent = `---
|
||||
name: '${agentName}'
|
||||
description: '${agentName} agent'
|
||||
---
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
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>
|
||||
`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
// Written directly to commands dir (no bmad subfolder)
|
||||
const launcherName = customAgentColonName(agentName);
|
||||
const launcherPath = path.join(commandsDir, launcherName);
|
||||
await this.writeFile(launcherPath, launcherContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `/${launcherName.replace('.md', '')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ClaudeCodeSetup };
|
||||
@@ -1,272 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Cline IDE setup handler
|
||||
* Installs BMAD artifacts to .clinerules/workflows with flattened naming
|
||||
*/
|
||||
class ClineSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('cline', 'Cline', false);
|
||||
this.configDir = '.clinerules';
|
||||
this.workflowsDir = 'workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Cline IDE 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 .clinerules/workflows directory
|
||||
const clineDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(clineDir, this.workflowsDir);
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Clear old BMAD files
|
||||
await this.clearOldBmadFiles(workflowsDir);
|
||||
|
||||
// Collect all artifacts
|
||||
const { artifacts, counts } = await this.collectClineArtifacts(projectDir, bmadDir, options);
|
||||
|
||||
// Write flattened files
|
||||
const written = await this.flattenAndWriteArtifacts(artifacts, workflowsDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
console.log(chalk.dim(` - ${counts.tasks} tasks installed`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands installed`));
|
||||
if (counts.workflowLaunchers > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers installed`));
|
||||
}
|
||||
console.log(chalk.dim(` - ${written} files written to ${path.relative(projectDir, workflowsDir)}`));
|
||||
|
||||
// Usage instructions
|
||||
console.log(chalk.yellow('\n ⚠️ How to Use Cline Workflows'));
|
||||
console.log(chalk.cyan(' BMAD workflows are available as slash commands in Cline'));
|
||||
console.log(chalk.dim(' Usage:'));
|
||||
console.log(chalk.dim(' - Type / to see available commands'));
|
||||
console.log(chalk.dim(' - All BMAD items start with "bmad_"'));
|
||||
console.log(chalk.dim(' - Example: /bmad_bmm_pm'));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: counts.agents,
|
||||
tasks: counts.tasks,
|
||||
workflows: counts.workflows,
|
||||
workflowLaunchers: counts.workflowLaunchers,
|
||||
written,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Cline installation by checking for .clinerules/workflows directory
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (!(await fs.pathExists(workflowsDir))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(workflowsDir);
|
||||
return entries.some((entry) => entry.startsWith('bmad'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all artifacts for Cline export
|
||||
*/
|
||||
async collectClineArtifacts(projectDir, bmadDir, options = {}) {
|
||||
const selectedModules = options.selectedModules || [];
|
||||
const artifacts = [];
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
|
||||
// Process agent launchers with project-specific paths
|
||||
for (const agentArtifact of agentArtifacts) {
|
||||
const content = agentArtifact.content;
|
||||
|
||||
artifacts.push({
|
||||
type: 'agent',
|
||||
module: agentArtifact.module,
|
||||
sourcePath: agentArtifact.sourcePath,
|
||||
relativePath: agentArtifact.relativePath,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
// Get tasks
|
||||
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
|
||||
for (const task of tasks) {
|
||||
const content = await this.readAndProcessWithProject(
|
||||
task.path,
|
||||
{
|
||||
module: task.module,
|
||||
name: task.name,
|
||||
},
|
||||
projectDir,
|
||||
);
|
||||
|
||||
artifacts.push({
|
||||
type: 'task',
|
||||
module: task.module,
|
||||
sourcePath: task.path,
|
||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
// Get workflows
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
artifacts.push(...workflowArtifacts);
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
counts: {
|
||||
agents: agentArtifacts.length,
|
||||
tasks: tasks.length,
|
||||
workflows: workflowCounts.commands,
|
||||
workflowLaunchers: workflowCounts.launchers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten file path to bmad_module_type_name.md format
|
||||
* Uses shared toDashPath utility
|
||||
*/
|
||||
flattenFilename(relativePath) {
|
||||
return toDashPath(relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all artifacts with flattened names
|
||||
*/
|
||||
async flattenAndWriteArtifacts(artifacts, destDir) {
|
||||
let written = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const flattenedName = this.flattenFilename(artifact.relativePath);
|
||||
const targetPath = path.join(destDir, flattenedName);
|
||||
await fs.writeFile(targetPath, artifact.content);
|
||||
written++;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old BMAD files from the workflows directory
|
||||
*/
|
||||
async clearOldBmadFiles(destDir) {
|
||||
if (!(await fs.pathExists(destDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(destDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.startsWith('bmad')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = path.join(destDir, entry);
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (stat.isFile()) {
|
||||
await fs.remove(entryPath);
|
||||
} else if (stat.isDirectory()) {
|
||||
await fs.remove(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and process file with project-specific paths
|
||||
*/
|
||||
async readAndProcessWithProject(filePath, metadata, projectDir) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return super.processContent(content, metadata, projectDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Cline configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
await this.clearOldBmadFiles(workflowsDir);
|
||||
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Cline
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const clineDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(clineDir, this.workflowsDir);
|
||||
|
||||
// Create .clinerules/workflows directory if it doesn't exist
|
||||
await fs.ensureDir(workflowsDir);
|
||||
|
||||
// Create custom agent launcher workflow
|
||||
const launcherContent = `name: ${agentName}
|
||||
description: Custom BMAD agent: ${agentName}
|
||||
|
||||
# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this workflow as ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
const fileName = customAgentDashName(agentName);
|
||||
const launcherPath = path.join(workflowsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'cline',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: fileName.replace('.md', ''),
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Ensure directory exists
|
||||
*/
|
||||
async ensureDir(dirPath) {
|
||||
await fs.ensureDir(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ClineSetup };
|
||||
@@ -154,17 +154,25 @@ class CodexSetup extends BaseIdeSetup {
|
||||
|
||||
// Check global location
|
||||
if (await fs.pathExists(globalDir)) {
|
||||
const entries = await fs.readdir(globalDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
try {
|
||||
const entries = await fs.readdir(globalDir);
|
||||
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// Check project-specific location
|
||||
if (await fs.pathExists(projectSpecificDir)) {
|
||||
const entries = await fs.readdir(projectSpecificDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
try {
|
||||
const entries = await fs.readdir(projectSpecificDir);
|
||||
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,19 +261,39 @@ class CodexSetup extends BaseIdeSetup {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(destDir);
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(destDir);
|
||||
} catch (error) {
|
||||
// Directory exists but can't be read - skip cleanup
|
||||
console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entries || !Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip non-strings or undefined entries
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (!entry.startsWith('bmad')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = path.join(destDir, entry);
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (stat.isFile()) {
|
||||
await fs.remove(entryPath);
|
||||
} else if (stat.isDirectory()) {
|
||||
await fs.remove(entryPath);
|
||||
try {
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (stat.isFile()) {
|
||||
await fs.remove(entryPath);
|
||||
} else if (stat.isDirectory()) {
|
||||
await fs.remove(entryPath);
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be processed
|
||||
console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { customAgentColonName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Crush IDE setup handler
|
||||
* Creates commands in .crush/commands/ directory structure using flat colon naming
|
||||
*/
|
||||
class CrushSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('crush', 'Crush');
|
||||
this.configDir = '.crush';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Crush IDE 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}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .crush/commands directory
|
||||
const crushDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(crushDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
||||
// Creates: .crush/commands/bmad_bmm_pm.md
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Write agent launcher files using flat underscore naming
|
||||
// Creates files like: bmad_bmm_pm.md
|
||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
||||
|
||||
// Get ALL workflows using the new workflow command generator
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Write workflow-command artifacts using flat underscore naming
|
||||
// Creates files like: bmad_bmm_correct-course.md
|
||||
const workflowCount = await workflowGenerator.writeColonArtifacts(commandsDir, workflowArtifacts);
|
||||
|
||||
// Generate task and tool commands using flat underscore naming
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agent commands created`));
|
||||
console.log(chalk.dim(` - ${taskToolResult.tasks} task commands created`));
|
||||
console.log(chalk.dim(` - ${taskToolResult.tools} tool commands created`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflow commands created`));
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
console.log(chalk.dim('\n Commands can be accessed via Crush command palette'));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskToolResult.tasks || 0,
|
||||
tools: taskToolResult.tools || 0,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Crush configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
const entries = await fs.readdir(commandsDir);
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('bmad')) {
|
||||
await fs.remove(path.join(commandsDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also remove legacy bmad folder if it exists
|
||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadFolder)) {
|
||||
await fs.remove(bmadFolder);
|
||||
console.log(chalk.dim(`Removed BMAD commands from Crush`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Crush
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Create .crush/commands directory if it doesn't exist
|
||||
await fs.ensureDir(commandsDir);
|
||||
|
||||
// Create custom agent launcher
|
||||
const launcherContent = `# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this command to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
// Written directly to commands dir (no bmad subfolder)
|
||||
const launcherName = customAgentColonName(agentName);
|
||||
const launcherPath = path.join(commandsDir, launcherName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'crush',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: launcherName.replace('.md', ''),
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CrushSetup };
|
||||
@@ -1,160 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { customAgentColonName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Cursor IDE setup handler
|
||||
*/
|
||||
class CursorSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('cursor', 'Cursor', true); // preferred IDE
|
||||
this.configDir = '.cursor';
|
||||
this.rulesDir = 'rules';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
const entries = await fs.readdir(commandsDir);
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('bmad')) {
|
||||
await fs.remove(path.join(commandsDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also remove legacy bmad folder if it exists
|
||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadFolder)) {
|
||||
await fs.remove(bmadFolder);
|
||||
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Cursor IDE 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}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .cursor/commands directory structure
|
||||
const cursorDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(cursorDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
||||
// Creates: .cursor/commands/bmad_bmm_pm.md
|
||||
|
||||
// Generate agent launchers using AgentCommandGenerator
|
||||
// This creates small launcher files that reference the actual agents in _bmad/
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Write agent launcher files using flat underscore naming
|
||||
// Creates files like: bmad_bmm_pm.md
|
||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
||||
|
||||
// Generate workflow commands from manifest (if it exists)
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Write workflow-command artifacts using flat underscore naming
|
||||
// Creates files like: bmad_bmm_correct-course.md
|
||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
||||
|
||||
// Generate task and tool commands from manifests (if they exist)
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||
if (workflowCommandCount > 0) {
|
||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
||||
}
|
||||
if (taskToolResult.generated > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskToolResult.tasks || 0,
|
||||
tools: taskToolResult.tools || 0,
|
||||
workflows: workflowCommandCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Cursor
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
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>
|
||||
`;
|
||||
|
||||
// Cursor uses YAML frontmatter matching Claude Code format
|
||||
const commandContent = `---
|
||||
name: '${agentName}'
|
||||
description: '${agentName} agent'
|
||||
---
|
||||
|
||||
${launcherContent}
|
||||
`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
// Written directly to commands dir (no bmad subfolder)
|
||||
const launcherName = customAgentColonName(agentName);
|
||||
const launcherPath = path.join(commandsDir, launcherName);
|
||||
await this.writeFile(launcherPath, commandContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `/${launcherName.replace('.md', '')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CursorSetup };
|
||||
@@ -1,301 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
|
||||
/**
|
||||
* Gemini CLI setup handler
|
||||
* Creates TOML files in .gemini/commands/ structure
|
||||
*/
|
||||
class GeminiSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('gemini', 'Gemini CLI', false);
|
||||
this.configDir = '.gemini';
|
||||
this.commandsDir = 'commands';
|
||||
this.agentTemplatePath = path.join(__dirname, 'templates', 'gemini-agent-command.toml');
|
||||
this.taskTemplatePath = path.join(__dirname, 'templates', 'gemini-task-command.toml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config values from bmad installation
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @returns {Object} Config values
|
||||
*/
|
||||
async loadConfigValues(bmadDir) {
|
||||
const configValues = {
|
||||
user_name: 'User', // Default fallback
|
||||
};
|
||||
|
||||
// Try to load core config.yaml
|
||||
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
|
||||
if (config.user_name) {
|
||||
configValues.user_name = config.user_name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` Warning: Could not load config values: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
return configValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Gemini CLI 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 .gemini/commands directory (flat structure with bmad- prefix)
|
||||
const geminiDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(geminiDir, this.commandsDir);
|
||||
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Get tasks and workflows (ALL workflows now generate commands)
|
||||
const tasks = await this.getTasks(bmadDir);
|
||||
|
||||
// Get ALL workflows using the new workflow command generator
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Install agents as TOML files with bmad- prefix (flat structure)
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const tomlContent = await this.createAgentLauncherToml(artifact);
|
||||
|
||||
// Flat structure: bmad-agent-{module}-{name}.toml
|
||||
const tomlPath = path.join(commandsDir, `bmad-agent-${artifact.module}-${artifact.name}.toml`);
|
||||
await this.writeFile(tomlPath, tomlContent);
|
||||
agentCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Added agent: /bmad_agents_${artifact.module}_${artifact.name}`));
|
||||
}
|
||||
|
||||
// Install tasks as TOML files with bmad- prefix (flat structure)
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const tomlContent = await this.createTaskToml(task, content);
|
||||
|
||||
// Flat structure: bmad-task-{module}-{name}.toml
|
||||
const tomlPath = path.join(commandsDir, `bmad-task-${task.module}-${task.name}.toml`);
|
||||
await this.writeFile(tomlPath, tomlContent);
|
||||
taskCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Added task: /bmad_tasks_${task.module}_${task.name}`));
|
||||
}
|
||||
|
||||
// Install workflows as TOML files with bmad- prefix (flat structure)
|
||||
let workflowCount = 0;
|
||||
for (const artifact of workflowArtifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
// Create TOML wrapper around workflow command content
|
||||
const tomlContent = await this.createWorkflowToml(artifact);
|
||||
|
||||
// Flat structure: bmad-workflow-{module}-{name}.toml
|
||||
const workflowName = path.basename(artifact.relativePath, '.md');
|
||||
const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`);
|
||||
await this.writeFile(tomlPath, tomlContent);
|
||||
workflowCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Added workflow: /bmad_workflows_${artifact.module}_${workflowName}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents configured`));
|
||||
console.log(chalk.dim(` - ${taskCount} tasks configured`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflows configured`));
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
console.log(chalk.dim(` - Agent activation: /bmad_agents_{agent-name}`));
|
||||
console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`));
|
||||
console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent launcher TOML content from artifact
|
||||
*/
|
||||
async createAgentLauncherToml(artifact) {
|
||||
// Strip frontmatter from launcher content
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
||||
|
||||
// Extract title from launcher frontmatter
|
||||
const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
|
||||
|
||||
// Create TOML wrapper around launcher content (without frontmatter)
|
||||
const description = `BMAD ${artifact.module.toUpperCase()} Agent: ${title}`;
|
||||
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${contentWithoutFrontmatter}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent TOML content using template
|
||||
*/
|
||||
async createAgentToml(agent, content) {
|
||||
// Extract metadata
|
||||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
|
||||
|
||||
// Load template
|
||||
const template = await fs.readFile(this.agentTemplatePath, 'utf8');
|
||||
|
||||
// Replace template variables
|
||||
// Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini
|
||||
const tomlContent = template
|
||||
.replaceAll('{{title}}', title)
|
||||
.replaceAll('{_bmad}', '_bmad')
|
||||
.replaceAll('{_bmad}', this.bmadFolderName)
|
||||
.replaceAll('{{module}}', agent.module)
|
||||
.replaceAll('{{name}}', agent.name);
|
||||
|
||||
return tomlContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task TOML content using template
|
||||
*/
|
||||
async createTaskToml(task, content) {
|
||||
// Extract task name from XML if available
|
||||
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
|
||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
||||
|
||||
// Load template
|
||||
const template = await fs.readFile(this.taskTemplatePath, 'utf8');
|
||||
|
||||
// Replace template variables
|
||||
const tomlContent = template
|
||||
.replaceAll('{{taskName}}', taskName)
|
||||
.replaceAll('{_bmad}', '_bmad')
|
||||
.replaceAll('{_bmad}', this.bmadFolderName)
|
||||
.replaceAll('{{module}}', task.module)
|
||||
.replaceAll('{{filename}}', task.filename);
|
||||
|
||||
return tomlContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow TOML content from artifact
|
||||
*/
|
||||
async createWorkflowToml(artifact) {
|
||||
// Extract description from artifact content
|
||||
const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
||||
const description = descriptionMatch
|
||||
? descriptionMatch[1]
|
||||
: `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`;
|
||||
|
||||
// Strip frontmatter from command content
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
||||
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${contentWithoutFrontmatter}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Gemini configuration - surgically remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
// Remove any bmad* files (cleans up old bmad- and bmad: formats)
|
||||
const files = await fs.readdir(commandsDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.toml')) {
|
||||
await fs.remove(path.join(commandsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Gemini
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const geminiDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(geminiDir, this.commandsDir);
|
||||
|
||||
// Create .gemini/commands directory if it doesn't exist
|
||||
await fs.ensureDir(commandsDir);
|
||||
|
||||
// Create custom agent launcher in TOML format
|
||||
const launcherContent = `description = "Custom BMAD Agent: ${agentName}"
|
||||
prompt = """
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this command to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*
|
||||
"""`;
|
||||
|
||||
const fileName = `bmad-custom-${agentName.toLowerCase()}.toml`;
|
||||
const launcherPath = path.join(commandsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'gemini',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GeminiSetup };
|
||||
@@ -1,383 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* GitHub Copilot setup handler
|
||||
* Creates agents in .github/agents/ and configures VS Code settings
|
||||
*/
|
||||
class GitHubCopilotSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('github-copilot', 'GitHub Copilot', true); // preferred IDE
|
||||
this.configDir = '.github';
|
||||
this.agentsDir = 'agents';
|
||||
this.vscodeDir = '.vscode';
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration choices before installation
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
const config = {};
|
||||
|
||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
||||
|
||||
config.vsCodeConfig = await prompts.select({
|
||||
message: 'How would you like to configure VS Code settings?',
|
||||
choices: [
|
||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||
{ name: 'Configure each setting manually', value: 'manual' },
|
||||
{ name: 'Skip settings configuration', value: 'skip' },
|
||||
],
|
||||
default: 'defaults',
|
||||
});
|
||||
|
||||
if (config.vsCodeConfig === 'manual') {
|
||||
config.manualSettings = await prompts.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'maxRequests',
|
||||
message: 'Maximum requests per session (1-50)?',
|
||||
default: '15',
|
||||
validate: (input) => {
|
||||
const num = parseInt(input, 10);
|
||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
||||
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'runTasks',
|
||||
message: 'Allow running workspace tasks?',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'mcpDiscovery',
|
||||
message: 'Enable MCP server discovery?',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'autoFix',
|
||||
message: 'Enable automatic error fixing?',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'autoApprove',
|
||||
message: 'Auto-approve tools (less secure)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}...`));
|
||||
|
||||
// Configure VS Code settings using pre-collected config if available
|
||||
const config = options.preCollectedConfig || {};
|
||||
await this.configureVsCodeSettings(projectDir, { ...options, ...config });
|
||||
|
||||
// Create .github/agents directory
|
||||
const githubDir = path.join(projectDir, this.configDir);
|
||||
const agentsDir = path.join(githubDir, this.agentsDir);
|
||||
await this.ensureDir(agentsDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Create agent files with bmd- prefix
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const content = artifact.content;
|
||||
const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content);
|
||||
|
||||
// Use bmd- prefix: bmd-custom-{module}-{name}.agent.md
|
||||
const targetPath = path.join(agentsDir, `bmd-custom-${artifact.module}-${artifact.name}.agent.md`);
|
||||
await this.writeFile(targetPath, agentContent);
|
||||
agentCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Created agent: bmd-custom-${artifact.module}-${artifact.name}`));
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents created`));
|
||||
console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`));
|
||||
console.log(chalk.dim(` - VS Code settings configured`));
|
||||
console.log(chalk.dim('\n Agents available in VS Code Chat view'));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
settings: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure VS Code settings for GitHub Copilot
|
||||
*/
|
||||
async configureVsCodeSettings(projectDir, options) {
|
||||
const fs = require('fs-extra');
|
||||
const vscodeDir = path.join(projectDir, this.vscodeDir);
|
||||
const settingsPath = path.join(vscodeDir, 'settings.json');
|
||||
|
||||
await this.ensureDir(vscodeDir);
|
||||
|
||||
// Read existing settings
|
||||
let existingSettings = {};
|
||||
if (await fs.pathExists(settingsPath)) {
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, 'utf8');
|
||||
existingSettings = JSON.parse(content);
|
||||
console.log(chalk.yellow(' Found existing .vscode/settings.json'));
|
||||
} catch {
|
||||
console.warn(chalk.yellow(' Could not parse settings.json, creating new'));
|
||||
}
|
||||
}
|
||||
|
||||
// Use pre-collected configuration or skip if not available
|
||||
let configChoice = options.vsCodeConfig;
|
||||
if (!configChoice) {
|
||||
// If no pre-collected config, skip configuration
|
||||
console.log(chalk.yellow(' ⚠ No configuration collected, skipping VS Code settings'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (configChoice === 'skip') {
|
||||
console.log(chalk.yellow(' ⚠ Skipping VS Code settings'));
|
||||
return;
|
||||
}
|
||||
|
||||
let bmadSettings = {};
|
||||
|
||||
if (configChoice === 'defaults') {
|
||||
bmadSettings = {
|
||||
'chat.agent.enabled': true,
|
||||
'chat.agent.maxRequests': 15,
|
||||
'github.copilot.chat.agent.runTasks': true,
|
||||
'chat.mcp.discovery.enabled': true,
|
||||
'github.copilot.chat.agent.autoFix': true,
|
||||
'chat.tools.autoApprove': false,
|
||||
};
|
||||
console.log(chalk.green(' ✓ Using recommended defaults'));
|
||||
} else {
|
||||
// Manual configuration - use pre-collected settings
|
||||
const manual = options.manualSettings || {};
|
||||
|
||||
const maxRequests = parseInt(manual.maxRequests || '15', 10);
|
||||
bmadSettings = {
|
||||
'chat.agent.enabled': true,
|
||||
'chat.agent.maxRequests': isNaN(maxRequests) ? 15 : maxRequests,
|
||||
'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks,
|
||||
'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery,
|
||||
'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix,
|
||||
'chat.tools.autoApprove': manual.autoApprove || false,
|
||||
};
|
||||
}
|
||||
|
||||
// Merge settings (existing take precedence)
|
||||
const mergedSettings = { ...bmadSettings, ...existingSettings };
|
||||
|
||||
// Write settings
|
||||
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
||||
console.log(chalk.green(' ✓ VS Code settings configured'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent content
|
||||
*/
|
||||
async createAgentContent(agent, content) {
|
||||
// Extract metadata from launcher frontmatter if present
|
||||
const descMatch = content.match(/description:\s*"([^"]+)"/);
|
||||
const title = descMatch ? descMatch[1] : this.formatTitle(agent.name);
|
||||
|
||||
const description = `Activates the ${title} agent persona.`;
|
||||
|
||||
// Strip any existing frontmatter from the content
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
let cleanContent = content;
|
||||
if (frontmatterRegex.test(content)) {
|
||||
cleanContent = content.replace(frontmatterRegex, '').trim();
|
||||
}
|
||||
|
||||
// Available GitHub Copilot tools (November 2025 - Official VS Code Documentation)
|
||||
// Reference: https://code.visualstudio.com/docs/copilot/reference/copilot-vscode-features#_chat-tools
|
||||
const tools = [
|
||||
'changes', // List of source control changes
|
||||
'edit', // Edit files in your workspace including: createFile, createDirectory, editNotebook, newJupyterNotebook and editFiles
|
||||
'fetch', // Fetch content from web page
|
||||
'githubRepo', // Perform code search in GitHub repo
|
||||
'problems', // Add workspace issues from Problems panel
|
||||
'runCommands', // Runs commands in the terminal including: getTerminalOutput, terminalSelection, terminalLastCommand and runInTerminal
|
||||
'runTasks', // Runs tasks and gets their output for your workspace
|
||||
'runTests', // Run unit tests in workspace
|
||||
'search', // Search and read files in your workspace, including:fileSearch, textSearch, listDirectory, readFile, codebase and searchResults
|
||||
'runSubagent', // Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management.
|
||||
'testFailure', // Get unit test failure information
|
||||
'todos', // Tool for managing and tracking todo items for task planning
|
||||
'usages', // Find references and navigate definitions
|
||||
];
|
||||
|
||||
let agentContent = `---
|
||||
description: "${description.replaceAll('"', String.raw`\"`)}"
|
||||
tools: ${JSON.stringify(tools)}
|
||||
---
|
||||
|
||||
# ${title} Agent
|
||||
|
||||
${cleanContent}
|
||||
|
||||
`;
|
||||
|
||||
return agentContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const fs = require('fs-extra');
|
||||
|
||||
// Clean up old chatmodes directory
|
||||
const chatmodesDir = path.join(projectDir, this.configDir, 'chatmodes');
|
||||
if (await fs.pathExists(chatmodesDir)) {
|
||||
const files = await fs.readdir(chatmodesDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.chatmode.md')) {
|
||||
await fs.remove(path.join(chatmodesDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} old BMAD chat modes`));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up new agents directory
|
||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
||||
if (await fs.pathExists(agentsDir)) {
|
||||
const files = await fs.readdir(agentsDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmd-') && file.endsWith('.agent.md')) {
|
||||
await fs.remove(path.join(agentsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for GitHub Copilot
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(agentsDir);
|
||||
|
||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
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>
|
||||
`;
|
||||
|
||||
// GitHub Copilot needs specific tools in frontmatter
|
||||
const copilotTools = [
|
||||
'changes',
|
||||
'codebase',
|
||||
'createDirectory',
|
||||
'createFile',
|
||||
'editFiles',
|
||||
'fetch',
|
||||
'fileSearch',
|
||||
'githubRepo',
|
||||
'listDirectory',
|
||||
'problems',
|
||||
'readFile',
|
||||
'runInTerminal',
|
||||
'runTask',
|
||||
'runTests',
|
||||
'runVscodeCommand',
|
||||
'search',
|
||||
'searchResults',
|
||||
'terminalLastCommand',
|
||||
'terminalSelection',
|
||||
'testFailure',
|
||||
'textSearch',
|
||||
'usages',
|
||||
];
|
||||
|
||||
const agentContent = `---
|
||||
description: "Activates the ${metadata.title || agentName} agent persona."
|
||||
tools: ${JSON.stringify(copilotTools)}
|
||||
---
|
||||
|
||||
# ${metadata.title || agentName} Agent
|
||||
|
||||
${launcherContent}
|
||||
`;
|
||||
|
||||
const agentFilePath = path.join(agentsDir, `bmd-custom-${agentName}.agent.md`);
|
||||
await this.writeFile(agentFilePath, agentContent);
|
||||
|
||||
return {
|
||||
path: agentFilePath,
|
||||
command: `bmd-custom-${agentName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GitHubCopilotSetup };
|
||||
@@ -1,191 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
|
||||
/**
|
||||
* iFlow CLI setup handler
|
||||
* Creates commands in .iflow/commands/ directory structure
|
||||
*/
|
||||
class IFlowSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('iflow', 'iFlow CLI');
|
||||
this.configDir = '.iflow';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup iFlow CLI 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 .iflow/commands/bmad directory structure
|
||||
const iflowDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
|
||||
const agentsDir = path.join(commandsDir, 'agents');
|
||||
const tasksDir = path.join(commandsDir, 'tasks');
|
||||
const workflowsDir = path.join(commandsDir, 'workflows');
|
||||
|
||||
await this.ensureDir(agentsDir);
|
||||
await this.ensureDir(tasksDir);
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Setup agents as commands
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const commandContent = await this.createAgentCommand(artifact);
|
||||
|
||||
const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Get tasks and workflows (ALL workflows now generate commands)
|
||||
const tasks = await this.getTasks(bmadDir);
|
||||
|
||||
// Get ALL workflows using the new workflow command generator
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
// Setup tasks as commands
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const commandContent = this.createTaskCommand(task, content);
|
||||
|
||||
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
// Setup workflows as commands (already generated)
|
||||
let workflowCount = 0;
|
||||
for (const artifact of workflowArtifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`);
|
||||
await this.writeFile(targetPath, artifact.content);
|
||||
workflowCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agent commands created`));
|
||||
console.log(chalk.dim(` - ${taskCount} task commands created`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflow commands created`));
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent command content
|
||||
*/
|
||||
async createAgentCommand(artifact) {
|
||||
// The launcher content is already complete - just return it as-is
|
||||
return artifact.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task command content
|
||||
*/
|
||||
createTaskCommand(task, content) {
|
||||
// Extract task name
|
||||
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
|
||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
||||
|
||||
let commandContent = `# /task-${task.name} Command
|
||||
|
||||
When this command is used, execute the following task:
|
||||
|
||||
## ${taskName} Task
|
||||
|
||||
${content}
|
||||
|
||||
## Usage
|
||||
|
||||
This command executes the ${taskName} task from the BMAD ${task.module.toUpperCase()} module.
|
||||
|
||||
## Module
|
||||
|
||||
Part of the BMAD ${task.module.toUpperCase()} module.
|
||||
`;
|
||||
|
||||
return commandContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup iFlow configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
|
||||
|
||||
if (await fs.pathExists(bmadCommandsDir)) {
|
||||
await fs.remove(bmadCommandsDir);
|
||||
console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for iFlow
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const iflowDir = path.join(projectDir, this.configDir);
|
||||
const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
|
||||
|
||||
// Create .iflow/commands/bmad directory if it doesn't exist
|
||||
await fs.ensureDir(bmadCommandsDir);
|
||||
|
||||
// Create custom agent launcher
|
||||
const launcherContent = `# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this command to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
const fileName = `custom-${agentName.toLowerCase()}.md`;
|
||||
const launcherPath = path.join(bmadCommandsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'iflow',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { IFlowSetup };
|
||||
@@ -5,11 +5,15 @@ const chalk = require('chalk');
|
||||
/**
|
||||
* IDE Manager - handles IDE-specific setup
|
||||
* Dynamically discovers and loads IDE handlers
|
||||
*
|
||||
* Loading strategy:
|
||||
* 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
|
||||
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||
*/
|
||||
class IdeManager {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
this.loadHandlers();
|
||||
this._initialized = false;
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
}
|
||||
|
||||
@@ -28,53 +32,76 @@ class IdeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load all IDE handlers from directory
|
||||
* Ensure handlers are loaded (lazy loading)
|
||||
*/
|
||||
loadHandlers() {
|
||||
async ensureInitialized() {
|
||||
if (!this._initialized) {
|
||||
await this.loadHandlers();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load all IDE handlers
|
||||
* 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
|
||||
* 2. Load config-driven handlers from platform-codes.yaml
|
||||
*/
|
||||
async loadHandlers() {
|
||||
// Load custom installer files
|
||||
this.loadCustomInstallerFiles();
|
||||
|
||||
// Load config-driven handlers from platform-codes.yaml
|
||||
await this.loadConfigDrivenHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom installer files (unique installation logic)
|
||||
* These files have special installation patterns that don't fit the config-driven model
|
||||
*/
|
||||
loadCustomInstallerFiles() {
|
||||
const ideDir = __dirname;
|
||||
const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
|
||||
|
||||
try {
|
||||
// Get all JS files in the IDE directory
|
||||
const files = fs.readdirSync(ideDir).filter((file) => {
|
||||
// Skip base class, manager, utility files (starting with _), and helper modules
|
||||
return (
|
||||
file.endsWith('.js') &&
|
||||
!file.startsWith('_') &&
|
||||
file !== 'manager.js' &&
|
||||
file !== 'workflow-command-generator.js' &&
|
||||
file !== 'task-tool-command-generator.js'
|
||||
);
|
||||
});
|
||||
for (const file of customFiles) {
|
||||
const filePath = path.join(ideDir, file);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
// Sort alphabetically for consistent ordering
|
||||
files.sort();
|
||||
try {
|
||||
const HandlerModule = require(filePath);
|
||||
const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
|
||||
|
||||
for (const file of files) {
|
||||
const moduleName = path.basename(file, '.js');
|
||||
|
||||
try {
|
||||
const modulePath = path.join(ideDir, file);
|
||||
const HandlerModule = require(modulePath);
|
||||
|
||||
// Get the first exported class (handles various export styles)
|
||||
const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]];
|
||||
|
||||
if (HandlerClass) {
|
||||
const instance = new HandlerClass();
|
||||
// Use the name property from the instance (set in constructor)
|
||||
// Only add if the instance has a valid name
|
||||
if (instance.name && typeof instance.name === 'string') {
|
||||
this.handlers.set(instance.name, instance);
|
||||
} else {
|
||||
console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`));
|
||||
}
|
||||
if (HandlerClass) {
|
||||
const instance = new HandlerClass();
|
||||
if (instance.name && typeof instance.name === 'string') {
|
||||
this.handlers.set(instance.name, instance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Failed to load IDE handlers:'), error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config-driven handlers from platform-codes.yaml
|
||||
* This creates ConfigDrivenIdeSetup instances for platforms with installer config
|
||||
*/
|
||||
async loadConfigDrivenHandlers() {
|
||||
const { loadPlatformCodes } = require('./platform-codes');
|
||||
const platformConfig = await loadPlatformCodes();
|
||||
|
||||
const { ConfigDrivenIdeSetup } = require('./_config-driven');
|
||||
|
||||
for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) {
|
||||
// Skip if already loaded by custom installer
|
||||
if (this.handlers.has(platformCode)) continue;
|
||||
|
||||
// Skip if no installer config (platform may not need installation)
|
||||
if (!platformInfo.installer) continue;
|
||||
|
||||
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
|
||||
handler.setBmadFolderName(this.bmadFolderName);
|
||||
this.handlers.set(platformCode, handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('node:os');
|
||||
const chalk = require('chalk');
|
||||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
|
||||
/**
|
||||
* OpenCode IDE setup handler
|
||||
*/
|
||||
class OpenCodeSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('opencode', 'OpenCode', true); // Mark as preferred/recommended
|
||||
this.configDir = '.opencode';
|
||||
this.commandsDir = 'command';
|
||||
this.agentsDir = 'agent';
|
||||
}
|
||||
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
const baseDir = path.join(projectDir, this.configDir);
|
||||
const commandsBaseDir = path.join(baseDir, this.commandsDir);
|
||||
const agentsBaseDir = path.join(baseDir, this.agentsDir);
|
||||
|
||||
await this.ensureDir(commandsBaseDir);
|
||||
await this.ensureDir(agentsBaseDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Install primary agents with flat naming: bmad-agent-{module}-{name}.md
|
||||
// OpenCode agents go in the agent folder (not command folder)
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const agentContent = artifact.content;
|
||||
// Flat structure in agent folder: bmad-agent-{module}-{name}.md
|
||||
const targetPath = path.join(agentsBaseDir, `bmad-agent-${artifact.module}-${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, agentContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Install workflow commands with flat naming: bmad-{module}-{workflow-name}
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
let workflowCommandCount = 0;
|
||||
for (const artifact of workflowArtifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
const commandContent = artifact.content;
|
||||
// Flat structure: bmad-{module}-{workflow-name}.md
|
||||
// artifact.relativePath is like: bmm/workflows/plan-project.md
|
||||
const workflowName = path.basename(artifact.relativePath, '.md');
|
||||
const targetPath = path.join(commandsBaseDir, `bmad-${artifact.module}-${workflowName}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
workflowCommandCount++;
|
||||
}
|
||||
// Skip workflow launcher READMEs as they're not needed in flat structure
|
||||
}
|
||||
|
||||
// Install task and tool commands with flat naming
|
||||
const { tasks, tools } = await this.generateFlatTaskToolCommands(bmadDir, commandsBaseDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/`));
|
||||
if (workflowCommandCount > 0) {
|
||||
console.log(chalk.dim(` - ${workflowCommandCount} workflows installed to .opencode/command/`));
|
||||
}
|
||||
if (tasks + tools > 0) {
|
||||
console.log(chalk.dim(` - ${tasks + tools} tasks/tools installed to .opencode/command/ (${tasks} tasks, ${tools} tools)`));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
workflows: workflowCommandCount,
|
||||
workflowCounts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate flat task and tool commands for OpenCode
|
||||
* OpenCode doesn't support nested command directories
|
||||
*/
|
||||
async generateFlatTaskToolCommands(bmadDir, commandsBaseDir) {
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const tasks = await taskToolGen.loadTaskManifest(bmadDir);
|
||||
const tools = await taskToolGen.loadToolManifest(bmadDir);
|
||||
|
||||
// Filter to only standalone items
|
||||
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||
|
||||
// Generate command files for tasks with flat naming: bmad-task-{module}-{name}.md
|
||||
for (const task of standaloneTasks) {
|
||||
const commandContent = taskToolGen.generateCommandContent(task, 'task');
|
||||
const targetPath = path.join(commandsBaseDir, `bmad-task-${task.module}-${task.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
}
|
||||
|
||||
// Generate command files for tools with flat naming: bmad-tool-{module}-{name}.md
|
||||
for (const tool of standaloneTools) {
|
||||
const commandContent = taskToolGen.generateCommandContent(tool, 'tool');
|
||||
const targetPath = path.join(commandsBaseDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: standaloneTasks.length,
|
||||
tools: standaloneTools.length,
|
||||
};
|
||||
}
|
||||
|
||||
async readAndProcess(filePath, metadata) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return this.processContent(content, metadata);
|
||||
}
|
||||
|
||||
async createAgentContent(content, metadata) {
|
||||
const { frontmatter = {}, body } = this.parseFrontmatter(content);
|
||||
|
||||
frontmatter.description =
|
||||
frontmatter.description && String(frontmatter.description).trim().length > 0
|
||||
? frontmatter.description
|
||||
: `BMAD ${metadata.module} agent: ${metadata.name}`;
|
||||
|
||||
// OpenCode agents use: 'primary' mode for main agents
|
||||
frontmatter.mode = 'primary';
|
||||
|
||||
const frontmatterString = this.stringifyFrontmatter(frontmatter);
|
||||
|
||||
// Get the activation header from central template
|
||||
const activationHeader = await this.getAgentCommandHeader();
|
||||
|
||||
return `${frontmatterString}\n\n${activationHeader}\n\n${body}`;
|
||||
}
|
||||
|
||||
parseFrontmatter(content) {
|
||||
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
||||
if (!match) {
|
||||
return { data: {}, body: content };
|
||||
}
|
||||
|
||||
const body = content.slice(match[0].length);
|
||||
|
||||
let frontmatter = {};
|
||||
try {
|
||||
frontmatter = yaml.parse(match[1]) || {};
|
||||
} catch {
|
||||
frontmatter = {};
|
||||
}
|
||||
|
||||
return { frontmatter, body };
|
||||
}
|
||||
|
||||
stringifyFrontmatter(frontmatter) {
|
||||
const yamlText = yaml
|
||||
.dump(frontmatter, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
sortKeys: false,
|
||||
})
|
||||
.trimEnd();
|
||||
|
||||
return `---\n${yamlText}\n---`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup OpenCode configuration - surgically remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
let removed = 0;
|
||||
|
||||
// Clean up agent folder
|
||||
if (await fs.pathExists(agentsDir)) {
|
||||
const files = await fs.readdir(agentsDir);
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(agentsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up command folder
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
const files = await fs.readdir(commandsDir);
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(commandsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for OpenCode
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(agentsDir);
|
||||
|
||||
const launcherContent = `---
|
||||
name: '${agentName}'
|
||||
description: '${metadata.title || agentName} agent'
|
||||
mode: 'primary'
|
||||
---
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
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>
|
||||
`;
|
||||
|
||||
// OpenCode uses flat naming: bmad-agent-custom-{name}.md
|
||||
const launcherPath = path.join(agentsDir, `bmad-agent-custom-${agentName}.md`);
|
||||
await this.writeFile(launcherPath, launcherContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `bmad-agent-custom-${agentName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { OpenCodeSetup };
|
||||
100
tools/cli/installers/lib/ide/platform-codes.js
Normal file
100
tools/cli/installers/lib/ide/platform-codes.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
|
||||
const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml');
|
||||
|
||||
let _cachedPlatformCodes = null;
|
||||
|
||||
/**
|
||||
* Load the platform codes configuration from YAML
|
||||
* @returns {Object} Platform codes configuration
|
||||
*/
|
||||
async function loadPlatformCodes() {
|
||||
if (_cachedPlatformCodes) {
|
||||
return _cachedPlatformCodes;
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(PLATFORM_CODES_PATH))) {
|
||||
throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`);
|
||||
}
|
||||
|
||||
const content = await fs.readFile(PLATFORM_CODES_PATH, 'utf8');
|
||||
_cachedPlatformCodes = yaml.parse(content);
|
||||
return _cachedPlatformCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform information by code
|
||||
* @param {string} platformCode - Platform code (e.g., 'claude-code', 'cursor')
|
||||
* @returns {Object|null} Platform info or null if not found
|
||||
*/
|
||||
function getPlatformInfo(platformCode) {
|
||||
if (!_cachedPlatformCodes) {
|
||||
throw new Error('Platform codes not loaded. Call loadPlatformCodes() first.');
|
||||
}
|
||||
|
||||
return _cachedPlatformCodes.platforms[platformCode] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all preferred platforms
|
||||
* @returns {Promise<Array>} Array of preferred platform codes
|
||||
*/
|
||||
async function getPreferredPlatforms() {
|
||||
const config = await loadPlatformCodes();
|
||||
return Object.entries(config.platforms)
|
||||
.filter(([_, info]) => info.preferred)
|
||||
.map(([code, _]) => code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all platform codes by category
|
||||
* @param {string} category - Category to filter by (ide, cli, tool, etc.)
|
||||
* @returns {Promise<Array>} Array of platform codes in the category
|
||||
*/
|
||||
async function getPlatformsByCategory(category) {
|
||||
const config = await loadPlatformCodes();
|
||||
return Object.entries(config.platforms)
|
||||
.filter(([_, info]) => info.category === category)
|
||||
.map(([code, _]) => code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all platforms with installer config
|
||||
* @returns {Promise<Array>} Array of platform codes that have installer config
|
||||
*/
|
||||
async function getConfigDrivenPlatforms() {
|
||||
const config = await loadPlatformCodes();
|
||||
return Object.entries(config.platforms)
|
||||
.filter(([_, info]) => info.installer)
|
||||
.map(([code, _]) => code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platforms that use custom installers (no installer config)
|
||||
* @returns {Promise<Array>} Array of platform codes with custom installers
|
||||
*/
|
||||
async function getCustomInstallerPlatforms() {
|
||||
const config = await loadPlatformCodes();
|
||||
return Object.entries(config.platforms)
|
||||
.filter(([_, info]) => !info.installer)
|
||||
.map(([code, _]) => code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached platform codes (useful for testing)
|
||||
*/
|
||||
function clearCache() {
|
||||
_cachedPlatformCodes = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadPlatformCodes,
|
||||
getPlatformInfo,
|
||||
getPreferredPlatforms,
|
||||
getPlatformsByCategory,
|
||||
getConfigDrivenPlatforms,
|
||||
getCustomInstallerPlatforms,
|
||||
clearCache,
|
||||
};
|
||||
241
tools/cli/installers/lib/ide/platform-codes.yaml
Normal file
241
tools/cli/installers/lib/ide/platform-codes.yaml
Normal file
@@ -0,0 +1,241 @@
|
||||
# BMAD Platform Codes Configuration
|
||||
# Central configuration for all platform/IDE codes used in the BMAD system
|
||||
#
|
||||
# This file defines:
|
||||
# 1. Platform metadata (name, preferred status, category, description)
|
||||
# 2. Installer configuration (target directories, templates, artifact types)
|
||||
#
|
||||
# Format:
|
||||
# code: Platform identifier used internally
|
||||
# name: Display name shown to users
|
||||
# preferred: Whether this platform is shown as a recommended option on install
|
||||
# category: Type of platform (ide, cli, tool, service)
|
||||
# description: Brief description of the platform
|
||||
# installer: Installation configuration (optional - omit for custom installers)
|
||||
|
||||
platforms:
|
||||
# ============================================================================
|
||||
# CLI Tools
|
||||
# ============================================================================
|
||||
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
category: cli
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
installer:
|
||||
target_dir: .claude/commands
|
||||
template_type: default
|
||||
|
||||
auggie:
|
||||
name: "Auggie"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "AI development tool"
|
||||
installer:
|
||||
target_dir: .augment/commands
|
||||
template_type: default
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Google's CLI for Gemini"
|
||||
installer:
|
||||
target_dir: .gemini/commands
|
||||
template_type: gemini
|
||||
|
||||
# ============================================================================
|
||||
# IDEs
|
||||
# ============================================================================
|
||||
|
||||
cursor:
|
||||
name: "Cursor"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-first code editor"
|
||||
installer:
|
||||
target_dir: .cursor/commands
|
||||
template_type: default
|
||||
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
installer:
|
||||
target_dir: .windsurf/workflows
|
||||
template_type: windsurf
|
||||
|
||||
cline:
|
||||
name: "Cline"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding assistant"
|
||||
installer:
|
||||
target_dir: .clinerules/workflows
|
||||
template_type: windsurf
|
||||
|
||||
roo:
|
||||
name: "Roo Cline"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Enhanced Cline fork"
|
||||
installer:
|
||||
target_dir: .roo/commands
|
||||
template_type: default
|
||||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "GitHub's AI pair programmer"
|
||||
installer:
|
||||
targets:
|
||||
- target_dir: .github/agents
|
||||
template_type: copilot_agents
|
||||
artifact_types: [agents]
|
||||
- target_dir: .vscode
|
||||
template_type: vscode_settings
|
||||
artifact_types: []
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "OpenCode terminal coding assistant"
|
||||
installer:
|
||||
target_dir: .opencode/command
|
||||
template_type: opencode
|
||||
|
||||
crush:
|
||||
name: "Crush"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI development assistant"
|
||||
installer:
|
||||
target_dir: .crush/commands
|
||||
template_type: default
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI workflow automation"
|
||||
installer:
|
||||
target_dir: .iflow/commands
|
||||
template_type: default
|
||||
|
||||
qwen:
|
||||
name: "QwenCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Qwen AI coding assistant"
|
||||
installer:
|
||||
target_dir: .qwen/commands
|
||||
template_type: default
|
||||
|
||||
rovo-dev:
|
||||
name: "Rovo Dev"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Atlassian's Rovo development environment"
|
||||
installer:
|
||||
target_dir: .rovodev/workflows
|
||||
template_type: rovodev
|
||||
|
||||
trae:
|
||||
name: "Trae"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding tool"
|
||||
installer:
|
||||
target_dir: .trae/rules
|
||||
template_type: trae
|
||||
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Google's AI development environment"
|
||||
installer:
|
||||
target_dir: .agent/workflows
|
||||
template_type: antigravity
|
||||
# Note: Antigravity uses .agent/workflows/ directory (not .antigravity/)
|
||||
|
||||
# ============================================================================
|
||||
# Custom Installers (no installer config - use custom file)
|
||||
# These have unique installation logic that doesn't fit the config-driven model
|
||||
# ============================================================================
|
||||
|
||||
codex:
|
||||
name: "Codex"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "OpenAI Codex integration"
|
||||
# No installer config - uses custom codex.js
|
||||
|
||||
kilo:
|
||||
name: "KiloCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding platform"
|
||||
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
|
||||
|
||||
# ============================================================================
|
||||
# Installer Config Schema
|
||||
# ============================================================================
|
||||
#
|
||||
# installer:
|
||||
# target_dir: string # Directory where artifacts are installed
|
||||
# template_type: string # Default template type to use
|
||||
# header_template: string (optional) # Override for header/frontmatter template
|
||||
# body_template: string (optional) # Override for body/content template
|
||||
# targets: array (optional) # For multi-target installations
|
||||
# - target_dir: string
|
||||
# template_type: string
|
||||
# artifact_types: [agents, workflows, tasks, tools]
|
||||
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
||||
|
||||
# ============================================================================
|
||||
# Platform Categories
|
||||
# ============================================================================
|
||||
|
||||
categories:
|
||||
ide:
|
||||
name: "Integrated Development Environment"
|
||||
description: "Full-featured code editors with AI assistance"
|
||||
|
||||
cli:
|
||||
name: "Command Line Interface"
|
||||
description: "Terminal-based tools"
|
||||
|
||||
tool:
|
||||
name: "Development Tool"
|
||||
description: "Standalone development utilities"
|
||||
|
||||
service:
|
||||
name: "Cloud Service"
|
||||
description: "Cloud-based development platforms"
|
||||
|
||||
extension:
|
||||
name: "Editor Extension"
|
||||
description: "Plugins for existing editors"
|
||||
|
||||
# ============================================================================
|
||||
# Naming Conventions and Rules
|
||||
# ============================================================================
|
||||
|
||||
conventions:
|
||||
code_format: "lowercase-kebab-case"
|
||||
name_format: "Title Case"
|
||||
max_code_length: 20
|
||||
allowed_characters: "a-z0-9-"
|
||||
@@ -1,372 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
|
||||
/**
|
||||
* Qwen Code setup handler
|
||||
* Creates TOML command files in .qwen/commands/BMad/
|
||||
*/
|
||||
class QwenSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('qwen', 'Qwen Code');
|
||||
this.configDir = '.qwen';
|
||||
this.commandsDir = 'commands';
|
||||
this.bmadDir = 'bmad';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Qwen Code 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 .qwen/commands/BMad directory structure
|
||||
const qwenDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(qwenDir, this.commandsDir);
|
||||
const bmadCommandsDir = path.join(commandsDir, this.bmadDir);
|
||||
|
||||
await this.ensureDir(bmadCommandsDir);
|
||||
|
||||
// Update existing settings.json if present
|
||||
await this.updateSettings(qwenDir);
|
||||
|
||||
// Clean up old configuration if exists
|
||||
await this.cleanupOldConfig(qwenDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Get tasks, tools, and workflows (standalone only for tools/workflows)
|
||||
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
|
||||
const tools = await this.getTools(bmadDir, true);
|
||||
const workflows = await this.getWorkflows(bmadDir, true);
|
||||
|
||||
// Create directories for each module (including standalone)
|
||||
const modules = new Set();
|
||||
for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
||||
|
||||
for (const module of modules) {
|
||||
await this.ensureDir(path.join(bmadCommandsDir, module));
|
||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'agents'));
|
||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks'));
|
||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'tools'));
|
||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows'));
|
||||
}
|
||||
|
||||
// Create TOML files for each agent launcher
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
// Convert markdown launcher content to TOML format
|
||||
const tomlContent = this.processAgentLauncherContent(artifact.content, {
|
||||
module: artifact.module,
|
||||
name: artifact.name,
|
||||
});
|
||||
|
||||
const targetPath = path.join(bmadCommandsDir, artifact.module, 'agents', `${artifact.name}.toml`);
|
||||
|
||||
await this.writeFile(targetPath, tomlContent);
|
||||
|
||||
agentCount++;
|
||||
console.log(chalk.green(` ✓ Added agent: /bmad_${artifact.module}_agents_${artifact.name}`));
|
||||
}
|
||||
|
||||
// Create TOML files for each task
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readAndProcess(task.path, {
|
||||
module: task.module,
|
||||
name: task.name,
|
||||
});
|
||||
|
||||
const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`);
|
||||
|
||||
await this.writeFile(targetPath, content);
|
||||
|
||||
taskCount++;
|
||||
console.log(chalk.green(` ✓ Added task: /bmad_${task.module}_tasks_${task.name}`));
|
||||
}
|
||||
|
||||
// Create TOML files for each tool
|
||||
let toolCount = 0;
|
||||
for (const tool of tools) {
|
||||
const content = await this.readAndProcess(tool.path, {
|
||||
module: tool.module,
|
||||
name: tool.name,
|
||||
});
|
||||
|
||||
const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`);
|
||||
|
||||
await this.writeFile(targetPath, content);
|
||||
|
||||
toolCount++;
|
||||
console.log(chalk.green(` ✓ Added tool: /bmad_${tool.module}_tools_${tool.name}`));
|
||||
}
|
||||
|
||||
// Create TOML files for each workflow
|
||||
let workflowCount = 0;
|
||||
for (const workflow of workflows) {
|
||||
const content = await this.readAndProcess(workflow.path, {
|
||||
module: workflow.module,
|
||||
name: workflow.name,
|
||||
});
|
||||
|
||||
const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`);
|
||||
|
||||
await this.writeFile(targetPath, content);
|
||||
|
||||
workflowCount++;
|
||||
console.log(chalk.green(` ✓ Added workflow: /bmad_${workflow.module}_workflows_${workflow.name}`));
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents configured`));
|
||||
console.log(chalk.dim(` - ${taskCount} tasks configured`));
|
||||
console.log(chalk.dim(` - ${toolCount} tools configured`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflows configured`));
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
tools: toolCount,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings.json to remove old agent references
|
||||
*/
|
||||
async updateSettings(qwenDir) {
|
||||
const fs = require('fs-extra');
|
||||
const settingsPath = path.join(qwenDir, 'settings.json');
|
||||
|
||||
if (await fs.pathExists(settingsPath)) {
|
||||
try {
|
||||
const settingsContent = await fs.readFile(settingsPath, 'utf8');
|
||||
const settings = JSON.parse(settingsContent);
|
||||
let updated = false;
|
||||
|
||||
// Remove agent file references from contextFileName
|
||||
if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
|
||||
const originalLength = settings.contextFileName.length;
|
||||
settings.contextFileName = settings.contextFileName.filter(
|
||||
(fileName) => !fileName.startsWith('agents/') && !fileName.startsWith('bmad-method/'),
|
||||
);
|
||||
|
||||
if (settings.contextFileName.length !== originalLength) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
||||
console.log(chalk.green(' ✓ Updated .qwen/settings.json'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(' ⚠ Could not update settings.json:'), error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old configuration directories
|
||||
*/
|
||||
async cleanupOldConfig(qwenDir) {
|
||||
const fs = require('fs-extra');
|
||||
const agentsDir = path.join(qwenDir, 'agents');
|
||||
const bmadMethodDir = path.join(qwenDir, 'bmad-method');
|
||||
const bmadDir = path.join(qwenDir, 'bmadDir');
|
||||
|
||||
if (await fs.pathExists(agentsDir)) {
|
||||
await fs.remove(agentsDir);
|
||||
console.log(chalk.green(' ✓ Removed old agents directory'));
|
||||
}
|
||||
|
||||
if (await fs.pathExists(bmadMethodDir)) {
|
||||
await fs.remove(bmadMethodDir);
|
||||
console.log(chalk.green(' ✓ Removed old bmad-method directory'));
|
||||
}
|
||||
|
||||
if (await fs.pathExists(bmadDir)) {
|
||||
await fs.remove(bmadDir);
|
||||
console.log(chalk.green(' ✓ Removed old BMad directory'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and process file content
|
||||
*/
|
||||
async readAndProcess(filePath, metadata) {
|
||||
const fs = require('fs-extra');
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
return this.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process agent launcher content and convert to TOML format
|
||||
* @param {string} launcherContent - Launcher markdown content
|
||||
* @param {Object} metadata - File metadata
|
||||
* @returns {string} TOML formatted content
|
||||
*/
|
||||
processAgentLauncherContent(launcherContent, metadata = {}) {
|
||||
// Strip frontmatter from launcher content
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '');
|
||||
|
||||
// Extract title for TOML description
|
||||
const titleMatch = launcherContent.match(/description:\s*"([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : metadata.name;
|
||||
|
||||
// Create TOML with launcher content (without frontmatter)
|
||||
return `description = "BMAD ${metadata.module.toUpperCase()} Agent: ${title}"
|
||||
prompt = """
|
||||
${contentWithoutFrontmatter.trim()}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override processContent to add TOML metadata header for Qwen
|
||||
* @param {string} content - File content
|
||||
* @param {Object} metadata - File metadata
|
||||
* @returns {string} Processed content with Qwen template
|
||||
*/
|
||||
processContent(content, metadata = {}) {
|
||||
// First apply base processing (includes activation injection for agents)
|
||||
let prompt = super.processContent(content, metadata);
|
||||
|
||||
// Determine the type and description based on content
|
||||
const isAgent = content.includes('<agent');
|
||||
const isTask = content.includes('<task');
|
||||
const isTool = content.includes('<tool');
|
||||
const isWorkflow = content.includes('workflow:') || content.includes('name:');
|
||||
|
||||
let description = '';
|
||||
|
||||
if (isAgent) {
|
||||
// Extract agent title if available
|
||||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : metadata.name;
|
||||
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
|
||||
} else if (isTask) {
|
||||
// Extract task name if available
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const taskName = nameMatch ? nameMatch[1] : metadata.name;
|
||||
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`;
|
||||
} else if (isTool) {
|
||||
// Extract tool name if available
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const toolName = nameMatch ? nameMatch[1] : metadata.name;
|
||||
description = `BMAD ${metadata.module.toUpperCase()} Tool: ${toolName}`;
|
||||
} else if (isWorkflow) {
|
||||
// Workflow
|
||||
description = `BMAD ${metadata.module.toUpperCase()} Workflow: ${metadata.name}`;
|
||||
} else {
|
||||
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;
|
||||
}
|
||||
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${prompt}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format name as title
|
||||
*/
|
||||
formatTitle(name) {
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Qwen configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, this.bmadDir);
|
||||
const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method');
|
||||
const oldBMadDir = path.join(projectDir, this.configDir, 'BMad');
|
||||
|
||||
if (await fs.pathExists(bmadCommandsDir)) {
|
||||
await fs.remove(bmadCommandsDir);
|
||||
console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`));
|
||||
}
|
||||
|
||||
if (await fs.pathExists(oldBmadMethodDir)) {
|
||||
await fs.remove(oldBmadMethodDir);
|
||||
console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`));
|
||||
}
|
||||
|
||||
if (await fs.pathExists(oldBMadDir)) {
|
||||
await fs.remove(oldBMadDir);
|
||||
console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Qwen
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const qwenDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(qwenDir, this.commandsDir);
|
||||
const bmadCommandsDir = path.join(commandsDir, this.bmadDir);
|
||||
|
||||
// Create .qwen/commands/BMad directory if it doesn't exist
|
||||
await fs.ensureDir(bmadCommandsDir);
|
||||
|
||||
// Create custom agent launcher in TOML format (same pattern as regular agents)
|
||||
const launcherContent = `# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this command to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
// Use Qwen's TOML conversion method
|
||||
const tomlContent = this.processAgentLauncherContent(launcherContent, {
|
||||
name: agentName,
|
||||
module: 'custom',
|
||||
});
|
||||
|
||||
const fileName = `custom-${agentName.toLowerCase()}.toml`;
|
||||
const launcherPath = path.join(bmadCommandsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, tomlContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'qwen',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { QwenSetup };
|
||||
@@ -1,273 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Roo IDE setup handler
|
||||
* Creates custom commands in .roo/commands directory
|
||||
*/
|
||||
class RooSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('roo', 'Roo Code');
|
||||
this.configDir = '.roo';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Roo IDE 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 .roo/commands directory
|
||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
await this.ensureDir(rooCommandsDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
let addedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const artifact of agentArtifacts) {
|
||||
// Use shared toDashPath to get consistent naming: bmad_bmm_name.md
|
||||
const commandName = toDashPath(artifact.relativePath).replace('.md', '');
|
||||
const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
|
||||
|
||||
// Skip if already exists
|
||||
if (await this.pathExists(commandPath)) {
|
||||
console.log(chalk.dim(` Skipping ${commandName} - already exists`));
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// artifact.sourcePath contains the full path to the agent file
|
||||
if (!artifact.sourcePath) {
|
||||
console.error(`Error: Missing sourcePath for artifact ${artifact.name} from module ${artifact.module}`);
|
||||
console.error(`Artifact object:`, artifact);
|
||||
throw new Error(`Missing sourcePath for agent: ${artifact.name}`);
|
||||
}
|
||||
|
||||
const content = await this.readFile(artifact.sourcePath);
|
||||
|
||||
// Create command file that references the actual _bmad agent
|
||||
await this.createCommandFile(
|
||||
{ module: artifact.module, name: artifact.name, path: artifact.sourcePath },
|
||||
content,
|
||||
commandPath,
|
||||
projectDir,
|
||||
);
|
||||
|
||||
addedCount++;
|
||||
console.log(chalk.green(` ✓ Added command: ${commandName}`));
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${addedCount} commands added`));
|
||||
if (skippedCount > 0) {
|
||||
console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`));
|
||||
}
|
||||
console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/`));
|
||||
console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commands: addedCount,
|
||||
skipped: skippedCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unified command file for agents
|
||||
* @param {string} commandPath - Path where to write the command file
|
||||
* @param {Object} options - Command options
|
||||
* @param {string} options.name - Display name for the command
|
||||
* @param {string} options.description - Description for the command
|
||||
* @param {string} options.agentPath - Path to the agent file (relative to project root)
|
||||
* @param {string} [options.icon] - Icon emoji (defaults to 🤖)
|
||||
* @param {string} [options.extraContent] - Additional content to include before activation
|
||||
*/
|
||||
async createAgentCommandFile(commandPath, options) {
|
||||
const { name, description, agentPath, icon = '🤖', extraContent = '' } = options;
|
||||
|
||||
// Build command content with YAML frontmatter
|
||||
let commandContent = `---\n`;
|
||||
commandContent += `name: '${icon} ${name}'\n`;
|
||||
commandContent += `description: '${description}'\n`;
|
||||
commandContent += `---\n\n`;
|
||||
|
||||
commandContent += `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n\n`;
|
||||
|
||||
// Add any extra content (e.g., warnings for custom agents)
|
||||
if (extraContent) {
|
||||
commandContent += `${extraContent}\n\n`;
|
||||
}
|
||||
|
||||
commandContent += `<agent-activation CRITICAL="TRUE">\n`;
|
||||
commandContent += `1. LOAD the FULL agent file from @${agentPath}\n`;
|
||||
commandContent += `2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n`;
|
||||
commandContent += `3. Execute ALL activation steps exactly as written in the agent file\n`;
|
||||
commandContent += `4. Follow the agent's persona and menu system precisely\n`;
|
||||
commandContent += `5. Stay in character throughout the session\n`;
|
||||
commandContent += `</agent-activation>\n`;
|
||||
|
||||
// Write command file
|
||||
await this.writeFile(commandPath, commandContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a command file for an agent
|
||||
*/
|
||||
async createCommandFile(agent, content, commandPath, projectDir) {
|
||||
// Extract metadata from agent content
|
||||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
|
||||
|
||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
||||
const icon = iconMatch ? iconMatch[1] : '🤖';
|
||||
|
||||
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
|
||||
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
|
||||
|
||||
// Get relative path
|
||||
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
|
||||
|
||||
// Use unified method
|
||||
await this.createAgentCommandFile(commandPath, {
|
||||
name: title,
|
||||
description: whenToUse,
|
||||
agentPath: relativePath,
|
||||
icon: icon,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format name as title
|
||||
*/
|
||||
formatTitle(name) {
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Roo configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (await fs.pathExists(rooCommandsDir)) {
|
||||
const files = await fs.readdir(rooCommandsDir);
|
||||
let removedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(rooCommandsDir, file));
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(chalk.dim(`Removed ${removedCount} BMAD commands from .roo/commands/`));
|
||||
}
|
||||
}
|
||||
|
||||
// Also clean up old .roomodes file if it exists
|
||||
const roomodesPath = path.join(projectDir, '.roomodes');
|
||||
if (await fs.pathExists(roomodesPath)) {
|
||||
const content = await fs.readFile(roomodesPath, 'utf8');
|
||||
|
||||
// Remove BMAD modes only
|
||||
const lines = content.split('\n');
|
||||
const filteredLines = [];
|
||||
let skipMode = false;
|
||||
let removedCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^\s*- slug: bmad/.test(line)) {
|
||||
skipMode = true;
|
||||
removedCount++;
|
||||
} else if (skipMode && /^\s*- slug: /.test(line)) {
|
||||
skipMode = false;
|
||||
}
|
||||
|
||||
if (!skipMode) {
|
||||
filteredLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Write back filtered content
|
||||
await fs.writeFile(roomodesPath, filteredLines.join('\n'));
|
||||
if (removedCount > 0) {
|
||||
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Roo
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata (unused, kept for compatibility)
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
await this.ensureDir(rooCommandsDir);
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
const commandName = customAgentDashName(agentName).replace('.md', '');
|
||||
const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
|
||||
|
||||
// Check if command already exists
|
||||
if (await this.pathExists(commandPath)) {
|
||||
return {
|
||||
ide: 'roo',
|
||||
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
|
||||
command: commandName,
|
||||
type: 'custom-agent-launcher',
|
||||
alreadyExists: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Read the custom agent file to extract metadata (same as regular agents)
|
||||
const fullAgentPath = path.join(projectDir, agentPath);
|
||||
const content = await this.readFile(fullAgentPath);
|
||||
|
||||
// Extract metadata from agent content
|
||||
const titleMatch = content.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName);
|
||||
|
||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
||||
const icon = iconMatch ? iconMatch[1] : '🤖';
|
||||
|
||||
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
|
||||
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
|
||||
|
||||
// Use unified method without extra content (clean)
|
||||
await this.createAgentCommandFile(commandPath, {
|
||||
name: title,
|
||||
description: whenToUse,
|
||||
agentPath: agentPath,
|
||||
icon: icon,
|
||||
});
|
||||
|
||||
return {
|
||||
ide: 'roo',
|
||||
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
|
||||
command: commandName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RooSetup };
|
||||
@@ -1,290 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
|
||||
/**
|
||||
* Rovo Dev IDE setup handler
|
||||
*
|
||||
* Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/
|
||||
* Installs workflows and tasks/tools as reference guides in .rovodev/
|
||||
* Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs
|
||||
*/
|
||||
class RovoDevSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('rovo-dev', 'Atlassian Rovo Dev', false);
|
||||
this.configDir = '.rovodev';
|
||||
this.subagentsDir = 'subagents';
|
||||
this.workflowsDir = 'workflows';
|
||||
this.referencesDir = 'references';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
||||
|
||||
if (!(await fs.pathExists(rovoDevDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean BMAD agents from subagents directory
|
||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
||||
if (await fs.pathExists(subagentsDir)) {
|
||||
const entries = await fs.readdir(subagentsDir);
|
||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
||||
|
||||
for (const file of bmadFiles) {
|
||||
await fs.remove(path.join(subagentsDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean BMAD workflows from workflows directory
|
||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
const entries = await fs.readdir(workflowsDir);
|
||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
||||
|
||||
for (const file of bmadFiles) {
|
||||
await fs.remove(path.join(workflowsDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean BMAD tasks/tools from references directory
|
||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
||||
if (await fs.pathExists(referencesDir)) {
|
||||
const entries = await fs.readdir(referencesDir);
|
||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
||||
|
||||
for (const file of bmadFiles) {
|
||||
await fs.remove(path.join(referencesDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Rovo Dev 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}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .rovodev directory structure
|
||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
||||
|
||||
await this.ensureDir(subagentsDir);
|
||||
await this.ensureDir(workflowsDir);
|
||||
await this.ensureDir(referencesDir);
|
||||
|
||||
// Generate and install agents
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`;
|
||||
const targetPath = path.join(subagentsDir, subagentFilename);
|
||||
const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module);
|
||||
await this.writeFile(targetPath, subagentContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Generate and install workflows
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
|
||||
let workflowCount = 0;
|
||||
for (const artifact of workflowArtifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
const workflowFilename = path.basename(artifact.relativePath);
|
||||
const targetPath = path.join(workflowsDir, workflowFilename);
|
||||
await this.writeFile(targetPath, artifact.content);
|
||||
workflowCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and install tasks and tools
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen);
|
||||
|
||||
// Summary output
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`));
|
||||
if (workflowCount > 0) {
|
||||
console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`));
|
||||
}
|
||||
if (taskCount + toolCount > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`),
|
||||
);
|
||||
}
|
||||
console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`));
|
||||
console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`));
|
||||
console.log(chalk.dim(` - Workflows and references are available in .rovodev/ directory`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
workflows: workflowCount,
|
||||
tasks: taskCount,
|
||||
tools: toolCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task and tool reference guides
|
||||
* @param {string} bmadDir - BMAD directory
|
||||
* @param {string} referencesDir - References directory
|
||||
* @param {TaskToolCommandGenerator} taskToolGen - Generator instance
|
||||
*/
|
||||
async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) {
|
||||
const tasks = await taskToolGen.loadTaskManifest(bmadDir);
|
||||
const tools = await taskToolGen.loadToolManifest(bmadDir);
|
||||
|
||||
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
||||
|
||||
let taskCount = 0;
|
||||
for (const task of standaloneTasks) {
|
||||
const commandContent = taskToolGen.generateCommandContent(task, 'task');
|
||||
const targetPath = path.join(referencesDir, `bmad-task-${task.module}-${task.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
let toolCount = 0;
|
||||
for (const tool of standaloneTools) {
|
||||
const commandContent = taskToolGen.generateCommandContent(tool, 'tool');
|
||||
const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
||||
await this.writeFile(targetPath, commandContent);
|
||||
toolCount++;
|
||||
}
|
||||
|
||||
return { tasks: taskCount, tools: toolCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BMAD agent launcher to Rovo Dev subagent format
|
||||
*
|
||||
* Rovo Dev subagents use Markdown files with YAML frontmatter containing:
|
||||
* - name: Unique identifier for the subagent
|
||||
* - description: One-line description of the subagent's purpose
|
||||
* - tools: Array of tools the subagent can use (optional)
|
||||
* - model: Specific model for this subagent (optional)
|
||||
* - load_memory: Whether to load memory files (optional, defaults to true)
|
||||
*
|
||||
* @param {string} launcherContent - Original agent launcher content
|
||||
* @param {string} agentName - Name of the agent
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @returns {string} Rovo Dev subagent-formatted content
|
||||
*/
|
||||
convertToRovoDevSubagent(launcherContent, agentName, moduleName) {
|
||||
// Extract metadata from the launcher XML
|
||||
const titleMatch = launcherContent.match(/title="([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName);
|
||||
|
||||
const descriptionMatch = launcherContent.match(/description="([^"]+)"/);
|
||||
const description = descriptionMatch ? descriptionMatch[1] : `BMAD agent: ${title}`;
|
||||
|
||||
const roleDefinitionMatch = launcherContent.match(/roleDefinition="([^"]+)"/);
|
||||
const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1] : `You are a specialized agent for ${title.toLowerCase()} tasks.`;
|
||||
|
||||
// Extract the main system prompt from the launcher (content after closing tags)
|
||||
let systemPrompt = roleDefinition;
|
||||
|
||||
// Try to extract additional instructions from the launcher content
|
||||
const instructionsMatch = launcherContent.match(/<instructions>([\s\S]*?)<\/instructions>/);
|
||||
if (instructionsMatch) {
|
||||
systemPrompt += '\n\n' + instructionsMatch[1].trim();
|
||||
}
|
||||
|
||||
// Build YAML frontmatter for Rovo Dev subagent
|
||||
const frontmatter = {
|
||||
name: `bmad-${moduleName}-${agentName}`,
|
||||
description: description,
|
||||
// Note: tools and model can be added by users in their .rovodev/subagents/*.md files
|
||||
// We don't enforce specific tools since BMAD agents are flexible
|
||||
};
|
||||
|
||||
// Create YAML frontmatter string with proper quoting for special characters
|
||||
let yamlContent = '---\n';
|
||||
yamlContent += `name: ${frontmatter.name}\n`;
|
||||
// Quote description to handle colons and other special characters in YAML
|
||||
yamlContent += `description: "${frontmatter.description.replaceAll('"', String.raw`\"`)}"\n`;
|
||||
yamlContent += '---\n';
|
||||
|
||||
// Combine frontmatter with system prompt
|
||||
const subagentContent = yamlContent + systemPrompt;
|
||||
|
||||
return subagentContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether Rovo Dev is already configured in the project
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
||||
|
||||
if (!(await fs.pathExists(rovoDevDir))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for BMAD agents in subagents directory
|
||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
||||
if (await fs.pathExists(subagentsDir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(subagentsDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Continue checking other directories
|
||||
}
|
||||
}
|
||||
|
||||
// Check for BMAD workflows in workflows directory
|
||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(workflowsDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Continue checking other directories
|
||||
}
|
||||
}
|
||||
|
||||
// Check for BMAD tasks/tools in references directory
|
||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
||||
if (await fs.pathExists(referencesDir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(referencesDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RovoDevSetup };
|
||||
@@ -31,11 +31,23 @@ class AgentCommandGenerator {
|
||||
const launcherContent = await this.generateLauncherContent(agent);
|
||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
||||
// Calculate the relative agent path (e.g., bmm/agents/pm.md)
|
||||
let agentRelPath = agent.path;
|
||||
// Remove _bmad/ prefix if present to get relative path from project root
|
||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||
if (agentRelPath.includes('_bmad/')) {
|
||||
const parts = agentRelPath.split(/_bmad\//);
|
||||
if (parts.length > 1) {
|
||||
agentRelPath = parts.slice(1).join('/');
|
||||
}
|
||||
}
|
||||
artifacts.push({
|
||||
type: 'agent-launcher',
|
||||
module: agent.module,
|
||||
name: agent.name,
|
||||
relativePath: path.join(agent.module, 'agents', agentPathInModule),
|
||||
description: agent.description || `${agent.name} agent`,
|
||||
module: agent.module,
|
||||
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
||||
agentPath: agentRelPath, // Relative path to actual agent file
|
||||
content: launcherContent,
|
||||
sourcePath: agent.path,
|
||||
});
|
||||
@@ -119,8 +131,10 @@ class AgentCommandGenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write agent launcher artifacts using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_pm.md
|
||||
* Write agent launcher artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-pm.agent.md
|
||||
*
|
||||
* The .agent.md suffix distinguishes agents from workflows/tasks/tools.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Agent launcher artifacts
|
||||
@@ -131,7 +145,7 @@ class AgentCommandGenerator {
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'agent-launcher') {
|
||||
// Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md
|
||||
// Convert relativePath to dash format: bmm/agents/pm.md → bmad-bmm-pm.agent.md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(launcherPath));
|
||||
|
||||
@@ -86,6 +86,11 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip if entry.name is undefined or not a string
|
||||
if (!entry.name || typeof entry.name !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
* Path transformation utilities for IDE installer standardization
|
||||
*
|
||||
* Provides utilities to convert hierarchical paths to flat naming conventions.
|
||||
* - Underscore format (bmad_module_name.md) - Windows-compatible universal format
|
||||
*
|
||||
* DASH-BASED NAMING (new standard):
|
||||
* - Agents: bmad-module-name.agent.md (with .agent.md suffix)
|
||||
* - Workflows/Tasks/Tools: bmad-module-name.md
|
||||
*
|
||||
* Example outputs:
|
||||
* - cis/agents/storymaster.md → bmad-cis-storymaster.agent.md
|
||||
* - bmm/workflows/plan-project.md → bmad-bmm-plan-project.md
|
||||
* - bmm/tasks/create-story.md → bmad-bmm-create-story.md
|
||||
* - core/agents/brainstorming.md → bmad-brainstorming.agent.md
|
||||
*/
|
||||
|
||||
// Type segments - agents are included in naming, others are filtered out
|
||||
@@ -10,111 +19,120 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
||||
const AGENT_SEGMENT = 'agents';
|
||||
|
||||
/**
|
||||
* Convert hierarchical path to flat underscore-separated name
|
||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad_bmm_agent_pm.md'
|
||||
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad_bmm_correct-course.md'
|
||||
* Converts: 'core', 'agents', 'brainstorming' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
|
||||
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
|
||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad-bmm-pm.agent.md'
|
||||
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad-bmm-correct-course.md'
|
||||
* Converts: 'core', 'agents', 'brainstorming' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
*
|
||||
* @param {string} module - Module name (e.g., 'bmm', 'core')
|
||||
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
|
||||
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
|
||||
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md'
|
||||
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-bmm-correct-course.md'
|
||||
*/
|
||||
function toUnderscoreName(module, type, name) {
|
||||
function toDashName(module, type, name) {
|
||||
const isAgent = type === AGENT_SEGMENT;
|
||||
// For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md'
|
||||
|
||||
// For core module, skip the module prefix: use 'bmad-name.md' instead of 'bmad-core-name.md'
|
||||
if (module === 'core') {
|
||||
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
||||
return isAgent ? `bmad-${name}.agent.md` : `bmad-${name}.md`;
|
||||
}
|
||||
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
||||
|
||||
// Module artifacts: bmad-module-name.md or bmad-module-name.agent.md
|
||||
// eslint-disable-next-line unicorn/prefer-string-replace-all -- regex replace is intentional here
|
||||
const dashName = name.replace(/\//g, '-'); // Flatten nested paths
|
||||
return isAgent ? `bmad-${module}-${dashName}.agent.md` : `bmad-${module}-${dashName}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to flat underscore-separated name
|
||||
* Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md'
|
||||
* Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.md'
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
|
||||
* Convert relative path to flat dash-separated name
|
||||
* Converts: 'bmm/agents/pm.md' → 'bmad-bmm-pm.agent.md'
|
||||
* Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md'
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
*
|
||||
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
||||
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md'
|
||||
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-brainstorming.md'
|
||||
*/
|
||||
function toUnderscorePath(relativePath) {
|
||||
function toDashPath(relativePath) {
|
||||
if (!relativePath || typeof relativePath !== 'string') {
|
||||
// Return a safe default for invalid input
|
||||
return 'bmad-unknown.md';
|
||||
}
|
||||
|
||||
const withoutExt = relativePath.replace('.md', '');
|
||||
const parts = withoutExt.split(/[/\\]/);
|
||||
|
||||
const module = parts[0];
|
||||
const type = parts[1];
|
||||
const name = parts.slice(2).join('_');
|
||||
const name = parts.slice(2).join('-');
|
||||
|
||||
// Use toUnderscoreName for consistency
|
||||
return toUnderscoreName(module, type, name);
|
||||
return toDashName(module, type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom agent underscore name
|
||||
* Creates: 'bmad_custom_fred-commit-poet.md'
|
||||
* Create custom agent dash name
|
||||
* Creates: 'bmad-custom-fred-commit-poet.agent.md'
|
||||
*
|
||||
* @param {string} agentName - Custom agent name
|
||||
* @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md'
|
||||
* @returns {string} Flat filename like 'bmad-custom-fred-commit-poet.agent.md'
|
||||
*/
|
||||
function customAgentUnderscoreName(agentName) {
|
||||
return `bmad_custom_${agentName}.md`;
|
||||
function customAgentDashName(agentName) {
|
||||
return `bmad-custom-${agentName}.agent.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a filename uses underscore format
|
||||
* Check if a filename uses dash format
|
||||
* @param {string} filename - Filename to check
|
||||
* @returns {boolean} True if filename uses underscore format
|
||||
* @returns {boolean} True if filename uses dash format
|
||||
*/
|
||||
function isUnderscoreFormat(filename) {
|
||||
return filename.startsWith('bmad_') && filename.includes('_');
|
||||
function isDashFormat(filename) {
|
||||
return filename.startsWith('bmad-') && filename.includes('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parts from an underscore-formatted filename
|
||||
* Parses: 'bmad_bmm_agent_pm.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
||||
* Parses: 'bmad_bmm_correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
|
||||
* Parses: 'bmad_agent_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
||||
* Parses: 'bmad_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
|
||||
* Extract parts from a dash-formatted filename
|
||||
* Parses: 'bmad-bmm-pm.agent.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
||||
* Parses: 'bmad-bmm-correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
|
||||
* Parses: 'bmad-brainstorming.agent.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
||||
* Parses: 'bmad-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
|
||||
*
|
||||
* @param {string} filename - Underscore-formatted filename
|
||||
* @param {string} filename - Dash-formatted filename
|
||||
* @returns {Object|null} Parsed parts or null if invalid format
|
||||
*/
|
||||
function parseUnderscoreName(filename) {
|
||||
function parseDashName(filename) {
|
||||
const withoutExt = filename.replace('.md', '');
|
||||
const parts = withoutExt.split('_');
|
||||
const parts = withoutExt.split('-');
|
||||
|
||||
if (parts.length < 2 || parts[0] !== 'bmad') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is an agent file (has 'agent' as one of the parts)
|
||||
const agentIndex = parts.indexOf('agent');
|
||||
// Check if this is an agent file (has .agent suffix)
|
||||
const isAgent = withoutExt.endsWith('.agent');
|
||||
|
||||
if (agentIndex !== -1) {
|
||||
if (isAgent) {
|
||||
// This is an agent file
|
||||
// Format: bmad_agent_name (core) or bmad_module_agent_name
|
||||
if (agentIndex === 1) {
|
||||
// Core agent: bmad_agent_name
|
||||
// Format: bmad-name.agent (core) or bmad-module-name.agent
|
||||
if (parts.length === 3) {
|
||||
// Core agent: bmad-name.agent
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: 'core',
|
||||
type: 'agents',
|
||||
name: parts.slice(agentIndex + 1).join('_'),
|
||||
name: parts[1],
|
||||
};
|
||||
} else {
|
||||
// Module agent: bmad_module_agent_name
|
||||
// Module agent: bmad-module-name.agent
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: parts[1],
|
||||
type: 'agents',
|
||||
name: parts.slice(agentIndex + 1).join('_'),
|
||||
name: parts.slice(2).join('-'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Not an agent file - must be a workflow/tool/task
|
||||
// If only 2 parts (bmad_name), it's a core workflow/tool/task
|
||||
// If only 2 parts (bmad-name), it's a core workflow/tool/task
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
prefix: parts[0],
|
||||
@@ -124,42 +142,140 @@ function parseUnderscoreName(filename) {
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, it's a module workflow/tool/task (bmad_module_name)
|
||||
// Otherwise, it's a module workflow/tool/task (bmad-module-name)
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: parts[1],
|
||||
type: 'workflows', // Default to workflows for non-agent module items
|
||||
name: parts.slice(2).join('-'),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert hierarchical path to flat underscore-separated name (LEGACY)
|
||||
* @deprecated Use toDashName instead
|
||||
*/
|
||||
function toUnderscoreName(module, type, name) {
|
||||
const isAgent = type === AGENT_SEGMENT;
|
||||
if (module === 'core') {
|
||||
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
||||
}
|
||||
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to flat underscore-separated name (LEGACY)
|
||||
* @deprecated Use toDashPath instead
|
||||
*/
|
||||
function toUnderscorePath(relativePath) {
|
||||
const withoutExt = relativePath.replace('.md', '');
|
||||
const parts = withoutExt.split(/[/\\]/);
|
||||
|
||||
const module = parts[0];
|
||||
const type = parts[1];
|
||||
const name = parts.slice(2).join('_');
|
||||
|
||||
return toUnderscoreName(module, type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom agent underscore name (LEGACY)
|
||||
* @deprecated Use customAgentDashName instead
|
||||
*/
|
||||
function customAgentUnderscoreName(agentName) {
|
||||
return `bmad_custom_${agentName}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a filename uses underscore format (LEGACY)
|
||||
* @deprecated Use isDashFormat instead
|
||||
*/
|
||||
function isUnderscoreFormat(filename) {
|
||||
return filename.startsWith('bmad_') && filename.includes('_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parts from an underscore-formatted filename (LEGACY)
|
||||
* @deprecated Use parseDashName instead
|
||||
*/
|
||||
function parseUnderscoreName(filename) {
|
||||
const withoutExt = filename.replace('.md', '');
|
||||
const parts = withoutExt.split('_');
|
||||
|
||||
if (parts.length < 2 || parts[0] !== 'bmad') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const agentIndex = parts.indexOf('agent');
|
||||
|
||||
if (agentIndex !== -1) {
|
||||
if (agentIndex === 1) {
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: 'core',
|
||||
type: 'agents',
|
||||
name: parts.slice(agentIndex + 1).join('_'),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: parts[1],
|
||||
type: 'agents',
|
||||
name: parts.slice(agentIndex + 1).join('_'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: 'core',
|
||||
type: 'workflows',
|
||||
name: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: parts[1],
|
||||
type: 'workflows',
|
||||
name: parts.slice(2).join('_'),
|
||||
};
|
||||
}
|
||||
|
||||
// Backward compatibility aliases (deprecated)
|
||||
// Backward compatibility aliases (colon format was same as underscore)
|
||||
const toColonName = toUnderscoreName;
|
||||
const toColonPath = toUnderscorePath;
|
||||
const toDashPath = toUnderscorePath;
|
||||
const customAgentColonName = customAgentUnderscoreName;
|
||||
const customAgentDashName = customAgentUnderscoreName;
|
||||
const isColonFormat = isUnderscoreFormat;
|
||||
const isDashFormat = isUnderscoreFormat;
|
||||
const parseColonName = parseUnderscoreName;
|
||||
const parseDashName = parseUnderscoreName;
|
||||
|
||||
module.exports = {
|
||||
// New standard (dash-based)
|
||||
toDashName,
|
||||
toDashPath,
|
||||
customAgentDashName,
|
||||
isDashFormat,
|
||||
parseDashName,
|
||||
|
||||
// Legacy (underscore-based) - kept for backward compatibility
|
||||
toUnderscoreName,
|
||||
toUnderscorePath,
|
||||
customAgentUnderscoreName,
|
||||
isUnderscoreFormat,
|
||||
parseUnderscoreName,
|
||||
|
||||
// Backward compatibility aliases
|
||||
toColonName,
|
||||
toColonPath,
|
||||
toDashPath,
|
||||
customAgentColonName,
|
||||
customAgentDashName,
|
||||
isColonFormat,
|
||||
isDashFormat,
|
||||
parseColonName,
|
||||
parseDashName,
|
||||
|
||||
TYPE_SEGMENTS,
|
||||
AGENT_SEGMENT,
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ class TaskToolCommandGenerator {
|
||||
|
||||
// Convert path to use {project-root} placeholder
|
||||
let itemPath = item.path;
|
||||
if (itemPath.startsWith('bmad/')) {
|
||||
if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) {
|
||||
itemPath = `{project-root}/${itemPath}`;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ description: '${description.replaceAll("'", "''")}'
|
||||
|
||||
# ${item.displayName || item.name}
|
||||
|
||||
LOAD and execute the ${type} at: ${itemPath}
|
||||
Read the entire ${type} file at: ${itemPath}
|
||||
|
||||
Follow all instructions in the ${type} file exactly as written.
|
||||
`;
|
||||
@@ -239,8 +239,10 @@ Follow all instructions in the ${type} file exactly as written.
|
||||
}
|
||||
|
||||
/**
|
||||
* Write task/tool artifacts using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_bmad-help.md
|
||||
* Write task/tool artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-bmad-help.md
|
||||
*
|
||||
* Note: Tasks/tools do NOT have .agent.md suffix - only agents do.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
||||
@@ -252,7 +254,7 @@ Follow all instructions in the ${type} file exactly as written.
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
||||
// Use underscore format: bmad_module_name.md
|
||||
// Use dash format: bmad-module-name.md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
|
||||
@@ -67,10 +67,26 @@ class WorkflowCommandGenerator {
|
||||
|
||||
for (const workflow of allWorkflows) {
|
||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml)
|
||||
let workflowRelPath = workflow.path;
|
||||
// Remove _bmad/ prefix if present to get relative path from project root
|
||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||
if (workflowRelPath.includes('_bmad/')) {
|
||||
const parts = workflowRelPath.split(/_bmad\//);
|
||||
if (parts.length > 1) {
|
||||
workflowRelPath = parts.slice(1).join('/');
|
||||
}
|
||||
}
|
||||
// Determine if this is a YAML workflow
|
||||
const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.endsWith('.yml');
|
||||
artifacts.push({
|
||||
type: 'workflow-command',
|
||||
isYamlWorkflow: isYamlWorkflow, // For template selection
|
||||
name: workflow.name,
|
||||
description: workflow.description || `${workflow.name} workflow`,
|
||||
module: workflow.module,
|
||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
||||
content: commandContent,
|
||||
sourcePath: workflow.path,
|
||||
});
|
||||
@@ -265,8 +281,10 @@ When running any workflow:
|
||||
}
|
||||
|
||||
/**
|
||||
* Write workflow command artifacts using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_correct-course.md
|
||||
* Write workflow command artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-correct-course.md
|
||||
*
|
||||
* Note: Workflows do NOT have .agent.md suffix - only agents do.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Workflow artifacts
|
||||
@@ -277,7 +295,7 @@ When running any workflow:
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
// Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md
|
||||
// Convert relativePath to dash format: bmm/workflows/correct-course.md → bmad-bmm-correct-course.md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
||||
|
||||
Follow all instructions in the workflow file exactly as written.
|
||||
1
tools/cli/installers/lib/ide/templates/combined/claude-agent.md
Symbolic link
1
tools/cli/installers/lib/ide/templates/combined/claude-agent.md
Symbolic link
@@ -0,0 +1 @@
|
||||
default-agent.md
|
||||
@@ -0,0 +1 @@
|
||||
default-workflow-yaml.md
|
||||
@@ -0,0 +1 @@
|
||||
default-workflow.md
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||
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>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
|
||||
|
||||
<steps CRITICAL="TRUE">
|
||||
1. Always LOAD the FULL @{project-root}/{{bmadFolderName}}/core/tasks/workflow.xml
|
||||
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config @{project-root}/{{bmadFolderName}}/{{path}}
|
||||
3. Pass the yaml path @{project-root}/{{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions
|
||||
4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions
|
||||
5. Save outputs after EACH section when generating any documents from templates
|
||||
</steps>
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
|
||||
@@ -0,0 +1,9 @@
|
||||
# {{name}}
|
||||
|
||||
{{description}}
|
||||
|
||||
---
|
||||
|
||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
||||
|
||||
Follow all instructions in the workflow file exactly as written.
|
||||
9
tools/cli/installers/lib/ide/templates/combined/trae.md
Normal file
9
tools/cli/installers/lib/ide/templates/combined/trae.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# {{name}}
|
||||
|
||||
{{description}}
|
||||
|
||||
## Instructions
|
||||
|
||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
||||
|
||||
Follow all instructions in the workflow file exactly as written.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: '{{description}}'
|
||||
auto_execution_mode: "iterate"
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
Read the entire workflow file at {project-root}/_bmad/{{workflow_path}}
|
||||
|
||||
Follow all instructions in the workflow file exactly as written.
|
||||
10
tools/cli/installers/lib/ide/templates/split/gemini/body.md
Normal file
10
tools/cli/installers/lib/ide/templates/split/gemini/body.md
Normal file
@@ -0,0 +1,10 @@
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||
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>
|
||||
@@ -0,0 +1,2 @@
|
||||
name = "{{name}}"
|
||||
description = "{{description}}"
|
||||
@@ -0,0 +1,10 @@
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||
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>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
@@ -1,313 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
|
||||
/**
|
||||
* Trae IDE setup handler
|
||||
*/
|
||||
class TraeSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('trae', 'Trae');
|
||||
this.configDir = '.trae';
|
||||
this.rulesDir = 'rules';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Trae IDE 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 .trae/rules directory
|
||||
const traeDir = path.join(projectDir, this.configDir);
|
||||
const rulesDir = path.join(traeDir, this.rulesDir);
|
||||
|
||||
await this.ensureDir(rulesDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Get tasks, tools, and workflows (standalone only)
|
||||
const tasks = await this.getTasks(bmadDir, true);
|
||||
const tools = await this.getTools(bmadDir, true);
|
||||
const workflows = await this.getWorkflows(bmadDir, true);
|
||||
|
||||
// Process agents as rules with bmad- prefix
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const processedContent = await this.createAgentRule(artifact, bmadDir, projectDir);
|
||||
|
||||
// Use bmad- prefix: bmad-agent-{module}-{name}.md
|
||||
const targetPath = path.join(rulesDir, `bmad-agent-${artifact.module}-${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Process tasks as rules with bmad- prefix
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const processedContent = this.createTaskRule(task, content);
|
||||
|
||||
// Use bmad- prefix: bmad-task-{module}-{name}.md
|
||||
const targetPath = path.join(rulesDir, `bmad-task-${task.module}-${task.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
// Process tools as rules with bmad- prefix
|
||||
let toolCount = 0;
|
||||
for (const tool of tools) {
|
||||
const content = await this.readFile(tool.path);
|
||||
const processedContent = this.createToolRule(tool, content);
|
||||
|
||||
// Use bmad- prefix: bmad-tool-{module}-{name}.md
|
||||
const targetPath = path.join(rulesDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
toolCount++;
|
||||
}
|
||||
|
||||
// Process workflows as rules with bmad- prefix
|
||||
let workflowCount = 0;
|
||||
for (const workflow of workflows) {
|
||||
const content = await this.readFile(workflow.path);
|
||||
const processedContent = this.createWorkflowRule(workflow, content);
|
||||
|
||||
// Use bmad- prefix: bmad-workflow-{module}-{name}.md
|
||||
const targetPath = path.join(rulesDir, `bmad-workflow-${workflow.module}-${workflow.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
workflowCount++;
|
||||
}
|
||||
|
||||
const totalRules = agentCount + taskCount + toolCount + workflowCount;
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agent rules created`));
|
||||
console.log(chalk.dim(` - ${taskCount} task rules created`));
|
||||
console.log(chalk.dim(` - ${toolCount} tool rules created`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflow rules created`));
|
||||
console.log(chalk.dim(` - Total: ${totalRules} rules`));
|
||||
console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, rulesDir)}`));
|
||||
console.log(chalk.dim(` - Agents can be activated with @{agent-name}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rules: totalRules,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
tools: toolCount,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rule content for an agent
|
||||
*/
|
||||
async createAgentRule(artifact, bmadDir, projectDir) {
|
||||
// Strip frontmatter from launcher
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
||||
|
||||
// Extract metadata from launcher content
|
||||
const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
|
||||
|
||||
// Calculate relative path for reference
|
||||
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
|
||||
|
||||
let ruleContent = `# ${title} Agent Rule
|
||||
|
||||
This rule is triggered when the user types \`@${artifact.name}\` and activates the ${title} agent persona.
|
||||
|
||||
## Agent Activation
|
||||
|
||||
${contentWithoutFrontmatter}
|
||||
|
||||
## File Reference
|
||||
|
||||
The full agent definition is located at: \`${relativePath}\`
|
||||
`;
|
||||
|
||||
return ruleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rule content for a task
|
||||
*/
|
||||
createTaskRule(task, content) {
|
||||
// Extract task name from content
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
||||
|
||||
let ruleContent = `# ${taskName} Task Rule
|
||||
|
||||
This rule defines the ${taskName} task workflow.
|
||||
|
||||
## Task Definition
|
||||
|
||||
When this task is triggered, execute the following workflow:
|
||||
|
||||
${content}
|
||||
|
||||
## Usage
|
||||
|
||||
Reference this task with \`@task-${task.name}\` to execute the defined workflow.
|
||||
|
||||
## Module
|
||||
|
||||
Part of the BMAD ${task.module.toUpperCase()} module.
|
||||
`;
|
||||
|
||||
return ruleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rule content for a tool
|
||||
*/
|
||||
createToolRule(tool, content) {
|
||||
// Extract tool name from content
|
||||
const nameMatch = content.match(/name="([^"]+)"/);
|
||||
const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name);
|
||||
|
||||
let ruleContent = `# ${toolName} Tool Rule
|
||||
|
||||
This rule defines the ${toolName} tool.
|
||||
|
||||
## Tool Definition
|
||||
|
||||
When this tool is triggered, execute the following:
|
||||
|
||||
${content}
|
||||
|
||||
## Usage
|
||||
|
||||
Reference this tool with \`@tool-${tool.name}\` to execute it.
|
||||
|
||||
## Module
|
||||
|
||||
Part of the BMAD ${tool.module.toUpperCase()} module.
|
||||
`;
|
||||
|
||||
return ruleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rule content for a workflow
|
||||
*/
|
||||
createWorkflowRule(workflow, content) {
|
||||
let ruleContent = `# ${workflow.name} Workflow Rule
|
||||
|
||||
This rule defines the ${workflow.name} workflow.
|
||||
|
||||
## Workflow Description
|
||||
|
||||
${workflow.description || 'No description provided'}
|
||||
|
||||
## Workflow Definition
|
||||
|
||||
${content}
|
||||
|
||||
## Usage
|
||||
|
||||
Reference this workflow with \`@workflow-${workflow.name}\` to execute the guided workflow.
|
||||
|
||||
## Module
|
||||
|
||||
Part of the BMAD ${workflow.module.toUpperCase()} module.
|
||||
`;
|
||||
|
||||
return ruleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format agent/task name as title
|
||||
*/
|
||||
formatTitle(name) {
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Trae configuration - surgically remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const rulesPath = path.join(projectDir, this.configDir, this.rulesDir);
|
||||
|
||||
if (await fs.pathExists(rulesPath)) {
|
||||
// Remove any bmad* files (cleans up old bmad- and bmad: formats)
|
||||
const files = await fs.readdir(rulesPath);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(rulesPath, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD rules`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Trae
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const traeDir = path.join(projectDir, this.configDir);
|
||||
const rulesDir = path.join(traeDir, this.rulesDir);
|
||||
|
||||
// Create .trae/rules directory if it doesn't exist
|
||||
await fs.ensureDir(rulesDir);
|
||||
|
||||
// Create custom agent launcher
|
||||
const launcherContent = `# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this rule to activate ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
const fileName = `bmad-agent-custom-${agentName.toLowerCase()}.md`;
|
||||
const launcherPath = path.join(rulesDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'trae',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TraeSetup };
|
||||
@@ -1,258 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
|
||||
/**
|
||||
* Windsurf IDE setup handler
|
||||
*/
|
||||
class WindsurfSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('windsurf', 'Windsurf', true); // preferred IDE
|
||||
this.configDir = '.windsurf';
|
||||
this.workflowsDir = 'workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Windsurf IDE 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 .windsurf/workflows/bmad directory structure
|
||||
const windsurfDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
|
||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
||||
|
||||
await this.ensureDir(bmadWorkflowsDir);
|
||||
|
||||
// Clean up any existing BMAD workflows before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Convert artifacts to agent format for module organization
|
||||
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
|
||||
|
||||
// Get tasks, tools, and workflows (standalone only)
|
||||
const tasks = await this.getTasks(bmadDir, true);
|
||||
const tools = await this.getTools(bmadDir, true);
|
||||
const workflows = await this.getWorkflows(bmadDir, true);
|
||||
|
||||
// Create directories for each module under bmad/
|
||||
const modules = new Set();
|
||||
for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
||||
|
||||
for (const module of modules) {
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'agents'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tasks'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tools'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'workflows'));
|
||||
}
|
||||
|
||||
// Process agent launchers as workflows with organized structure
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const processedContent = this.createWorkflowContent({ module: artifact.module, name: artifact.name }, artifact.content);
|
||||
|
||||
// Organized path: bmad/module/agents/agent-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, artifact.module, 'agents', `${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Process tasks as workflows with organized structure
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const processedContent = this.createTaskWorkflowContent(task, content);
|
||||
|
||||
// Organized path: bmad/module/tasks/task-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, task.module, 'tasks', `${task.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
// Process tools as workflows with organized structure
|
||||
let toolCount = 0;
|
||||
for (const tool of tools) {
|
||||
const content = await this.readFile(tool.path);
|
||||
const processedContent = this.createToolWorkflowContent(tool, content);
|
||||
|
||||
// Organized path: bmad/module/tools/tool-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, tool.module, 'tools', `${tool.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
toolCount++;
|
||||
}
|
||||
|
||||
// Process workflows with organized structure
|
||||
let workflowCount = 0;
|
||||
for (const workflow of workflows) {
|
||||
const content = await this.readFile(workflow.path);
|
||||
const processedContent = this.createWorkflowWorkflowContent(workflow, content);
|
||||
|
||||
// Organized path: bmad/module/workflows/workflow-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, workflow.module, 'workflows', `${workflow.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
workflowCount++;
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||
console.log(chalk.dim(` - ${taskCount} tasks installed`));
|
||||
console.log(chalk.dim(` - ${toolCount} tools installed`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflows installed`));
|
||||
console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`));
|
||||
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
|
||||
|
||||
// Provide additional configuration hints
|
||||
if (options.showHints !== false) {
|
||||
console.log(chalk.dim('\n Windsurf workflow settings:'));
|
||||
console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)'));
|
||||
console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks/tools)'));
|
||||
console.log(chalk.dim(' - auto_execution_mode: 1 (recommended for workflows)'));
|
||||
console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu'));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
tools: toolCount,
|
||||
workflows: workflowCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for an agent
|
||||
*/
|
||||
createWorkflowContent(agent, content) {
|
||||
// Strip existing frontmatter from launcher
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: ${agent.name}
|
||||
auto_execution_mode: 3
|
||||
---
|
||||
|
||||
${contentWithoutFrontmatter}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a task
|
||||
*/
|
||||
createTaskWorkflowContent(task, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: task-${task.name}
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a tool
|
||||
*/
|
||||
createToolWorkflowContent(tool, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: tool-${tool.name}
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a workflow
|
||||
*/
|
||||
createWorkflowWorkflowContent(workflow, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: ${workflow.name}
|
||||
auto_execution_mode: 1
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Windsurf configuration - surgically remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const bmadPath = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad');
|
||||
|
||||
if (await fs.pathExists(bmadPath)) {
|
||||
// Remove the entire bmad folder - this is our territory
|
||||
await fs.remove(bmadPath);
|
||||
console.log(chalk.dim(` Cleaned up existing BMAD workflows`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Windsurf
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const fs = require('fs-extra');
|
||||
const customAgentsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad', 'custom', 'agents');
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(customAgentsDir);
|
||||
|
||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from @${agentPath}
|
||||
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>
|
||||
`;
|
||||
|
||||
// Windsurf uses workflow format with frontmatter
|
||||
const workflowContent = `---
|
||||
description: ${metadata.title || agentName}
|
||||
auto_execution_mode: 3
|
||||
---
|
||||
|
||||
${launcherContent}`;
|
||||
|
||||
const launcherPath = path.join(customAgentsDir, `${agentName}.md`);
|
||||
await fs.writeFile(launcherPath, workflowContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `bmad/custom/agents/${agentName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { WindsurfSetup };
|
||||
@@ -81,7 +81,7 @@ class UI {
|
||||
hasLegacyCfg = bmadResult.hasLegacyCfg;
|
||||
}
|
||||
|
||||
// Handle legacy .bmad or _cfg folder - these are very old (more than 2 versions behind)
|
||||
// Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha)
|
||||
// Show version warning instead of offering conversion
|
||||
if (hasLegacyBmadFolder || hasLegacyCfg) {
|
||||
console.log('');
|
||||
@@ -92,9 +92,8 @@ class UI {
|
||||
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.',
|
||||
),
|
||||
);
|
||||
console.log(chalk.yellow('This version is more than 2 alpha versions behind current.'));
|
||||
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
|
||||
console.log('');
|
||||
console.log(chalk.dim('For stability, we only support updates from the previous 2 alpha versions.'));
|
||||
console.log(chalk.dim('Legacy installations may have compatibility issues.'));
|
||||
console.log('');
|
||||
console.log(chalk.dim('For the best experience, we strongly recommend:'));
|
||||
@@ -188,8 +187,8 @@ class UI {
|
||||
const currentVersion = require(packageJsonPath).version;
|
||||
const installedVersion = existingInstall.version || 'unknown';
|
||||
|
||||
// Check if version is too old and warn user
|
||||
const shouldProceed = await this.showOldAlphaVersionWarning(installedVersion, currentVersion, path.basename(bmadDir));
|
||||
// Check if version is pre beta
|
||||
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir));
|
||||
|
||||
// If user chose to cancel, exit the installer
|
||||
if (!shouldProceed) {
|
||||
@@ -362,6 +361,7 @@ class UI {
|
||||
// Get IDE manager to fetch available IDEs dynamically
|
||||
const { IdeManager } = require('../installers/lib/ide/manager');
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs
|
||||
|
||||
const preferredIdes = ideManager.getPreferredIdes();
|
||||
const otherIdes = ideManager.getOtherIdes();
|
||||
@@ -1456,96 +1456,40 @@ class UI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse alpha version string (e.g., "6.0.0-Alpha.20")
|
||||
* @param {string} version - Version string
|
||||
* @returns {Object|null} Object with alphaNumber and fullVersion, or null if invalid
|
||||
*/
|
||||
parseAlphaVersion(version) {
|
||||
if (!version || version === 'unknown') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove 'v' prefix if present
|
||||
const cleanVersion = version.toString().replace(/^v/i, '');
|
||||
|
||||
// Match alpha version pattern: X.Y.Z-Alpha.N (case-insensitive)
|
||||
const match = cleanVersion.match(/[\d.]+-Alpha\.(\d+)/i);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
alphaNumber: parseInt(match[1], 10),
|
||||
fullVersion: cleanVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if installed version is more than 2 alpha versions behind current
|
||||
* Check if installed version is a legacy version that needs fresh install
|
||||
* @param {string} installedVersion - The installed version
|
||||
* @param {string} currentVersion - The current version
|
||||
* @returns {Object} Object with { isOldVersion, versionDiff, shouldWarn, installed, current }
|
||||
* @returns {boolean} True if legacy (v4 or any alpha)
|
||||
*/
|
||||
checkAlphaVersionAge(installedVersion, currentVersion) {
|
||||
const installed = this.parseAlphaVersion(installedVersion);
|
||||
const current = this.parseAlphaVersion(currentVersion);
|
||||
|
||||
// If we can't parse either version, don't warn
|
||||
if (!installed || !current) {
|
||||
return { isOldVersion: false, versionDiff: 0, shouldWarn: false };
|
||||
isLegacyVersion(installedVersion) {
|
||||
if (!installedVersion || installedVersion === 'unknown') {
|
||||
return true; // Treat unknown as legacy for safety
|
||||
}
|
||||
|
||||
// Calculate alpha version difference
|
||||
const versionDiff = current.alphaNumber - installed.alphaNumber;
|
||||
|
||||
// Consider it old if more than 2 versions behind
|
||||
const isOldVersion = versionDiff > 2;
|
||||
|
||||
return {
|
||||
isOldVersion,
|
||||
versionDiff,
|
||||
shouldWarn: isOldVersion,
|
||||
installed: installed.fullVersion,
|
||||
current: current.fullVersion,
|
||||
installedAlpha: installed.alphaNumber,
|
||||
currentAlpha: current.alphaNumber,
|
||||
};
|
||||
// Check if version string contains -alpha or -Alpha (any v6 alpha)
|
||||
return /-alpha\./i.test(installedVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show warning for old alpha version and ask if user wants to proceed
|
||||
* Show warning for legacy version (v4 or alpha) and ask if user wants to proceed
|
||||
* @param {string} installedVersion - The installed version
|
||||
* @param {string} currentVersion - The current version
|
||||
* @param {string} bmadFolderName - Name of the BMAD folder
|
||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||
*/
|
||||
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
||||
|
||||
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
||||
const isUnknownVersion = installedVersion === 'unknown' || !versionInfo.installed;
|
||||
|
||||
if (!versionInfo.shouldWarn && !isUnknownVersion) {
|
||||
return true; // Not old, proceed
|
||||
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||
if (!this.isLegacyVersion(installedVersion)) {
|
||||
return true; // Not legacy, proceed
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.yellow.bold('⚠️ VERSION WARNING'));
|
||||
console.log(chalk.yellow('─'.repeat(80)));
|
||||
|
||||
if (isUnknownVersion) {
|
||||
if (installedVersion === 'unknown') {
|
||||
console.log(chalk.yellow('Unable to detect your installed BMAD version.'));
|
||||
console.log(chalk.yellow('This appears to be a legacy or unsupported installation.'));
|
||||
console.log('');
|
||||
console.log(chalk.dim('For stability, we only support updates from the previous 2 alpha versions.'));
|
||||
console.log(chalk.dim('Legacy installations may have compatibility issues.'));
|
||||
} else {
|
||||
console.log(chalk.yellow(`You are updating from ${versionInfo.installed} to ${versionInfo.current}.`));
|
||||
console.log(chalk.yellow(`This is ${versionInfo.versionDiff} alpha versions behind.`));
|
||||
console.log('');
|
||||
console.log(chalk.dim(`For stability, we only support updates from the previous 2 alpha versions`));
|
||||
console.log(chalk.dim(`(Alpha.${versionInfo.currentAlpha - 2} through Alpha.${versionInfo.currentAlpha - 1}).`));
|
||||
console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`));
|
||||
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
Reference in New Issue
Block a user