diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 616e6e430..6651143d6 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -18,7 +18,7 @@ Use `npx bmad-method install` to set up BMad in your project. One command handle - **Node.js** 20+ (the installer requires it) - **Git** (for cloning external modules) -- **An AI tool** such as Claude Code or Cursor — or install without one using `--tools none` +- **An AI tool** such as Claude Code or Cursor (run `npx bmad-method install --list-tools` to see all supported tools) ::: @@ -122,7 +122,8 @@ Under `--yes`, patch and minor upgrades apply automatically. Majors stay frozen | `--yes`, `-y` | Skip all prompts; accept flag values + defaults | | `--directory ` | Install into this directory (default: current working dir) | | `--modules ` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. | -| `--tools ` or `--tools none` | IDE/tool selection. `none` skips tool config entirely. | +| `--tools ` | IDE/tool selection. Required for fresh `--yes` installs. Run `--list-tools` for valid IDs. | +| `--list-tools` | Print all supported tool/IDE IDs (with target directories) and exit. | | `--action ` | `install`, `update`, or `quick-update`. Defaults based on existing install state. | | `--custom-source ` | Install custom modules from Git URLs or local paths | | `--channel ` | Apply to all externals (aliased as `--all-stable` / `--all-next`) | @@ -165,17 +166,17 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code ```bash npx bmad-method install --yes --action update \ - --modules bmm,bmb,gds \ - --tools none + --modules bmm,bmb,gds ``` +`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install. + **Mix channels — bmb on next, gds on stable:** ```bash npx bmad-method install --yes --action update \ --modules bmm,bmb,cis,gds \ - --next=bmb \ - --tools none + --next=bmb ``` :::caution[Rate limit on shared IPs] @@ -204,7 +205,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules` ```bash npx bmad-method install --yes --modules bmb,cis \ - --pin bmb=v1.7.0 --pin cis=v0.4.2 --tools none + --pin bmb=v1.7.0 --pin cis=v0.4.2 --tools claude-code ``` ## Troubleshooting diff --git a/docs/how-to/install-custom-modules.md b/docs/how-to/install-custom-modules.md index 288415afa..c4a38d41d 100644 --- a/docs/how-to/install-custom-modules.md +++ b/docs/how-to/install-custom-modules.md @@ -68,6 +68,7 @@ Select **Yes**, then provide a source: | Input Type | Example | | --------------------- | ------------------------------------------------- | | HTTPS URL (any host) | `https://github.com/org/repo` | +| HTTP URL (any host) | `http://host/org/repo` | | HTTPS URL with subdir | `https://github.com/org/repo/tree/main/my-module` | | SSH URL | `git@github.com:org/repo.git` | | Local path | `/Users/me/projects/my-module` | diff --git a/docs/vi-vn/how-to/install-custom-modules.md b/docs/vi-vn/how-to/install-custom-modules.md index 59ca36560..0b4064f1c 100644 --- a/docs/vi-vn/how-to/install-custom-modules.md +++ b/docs/vi-vn/how-to/install-custom-modules.md @@ -68,6 +68,7 @@ Chọn **Yes**, rồi nhập nguồn: | Loại đầu vào | Ví dụ | | --------------------- | ------------------------------------------------- | | HTTPS URL trên bất kỳ host nào | `https://github.com/org/repo` | +| HTTP URL trên bất kỳ host nào | `http://host/org/repo` | | HTTPS URL trỏ vào một thư mục con | `https://github.com/org/repo/tree/main/my-module` | | SSH URL | `git@github.com:org/repo.git` | | Đường dẫn cục bộ | `/Users/me/projects/my-module` | diff --git a/docs/zh-cn/how-to/install-custom-modules.md b/docs/zh-cn/how-to/install-custom-modules.md index 6b35c5df0..00193a3ed 100644 --- a/docs/zh-cn/how-to/install-custom-modules.md +++ b/docs/zh-cn/how-to/install-custom-modules.md @@ -68,6 +68,7 @@ Would you like to install from a custom source (Git URL or local path)? | 输入类型 | 示例 | | -------- | ---- | | HTTPS URL(任意主机) | `https://github.com/org/repo` | +| HTTP URL(任意主机) | `http://host/org/repo` | | 带子目录的 HTTPS URL | `https://github.com/org/repo/tree/main/my-module` | | SSH URL | `git@github.com:org/repo.git` | | 本地路径 | `/Users/me/projects/my-module` | diff --git a/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md b/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md index 3275c5db2..246071a6a 100644 --- a/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +++ b/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md @@ -227,37 +227,39 @@ Prepare the content to append to the document: ### Architecture Completeness Checklist -**✅ Requirements Analysis** +Mark each item `[x]` only if validation confirms it; leave `[ ]` if it is missing, partial, or unverified. Any unchecked item must be reflected in the Gap Analysis above and in the Overall Status below. -- [x] Project context thoroughly analyzed -- [x] Scale and complexity assessed -- [x] Technical constraints identified -- [x] Cross-cutting concerns mapped +**Requirements Analysis** -**✅ Architectural Decisions** +- [ ] Project context thoroughly analyzed +- [ ] Scale and complexity assessed +- [ ] Technical constraints identified +- [ ] Cross-cutting concerns mapped -- [x] Critical decisions documented with versions -- [x] Technology stack fully specified -- [x] Integration patterns defined -- [x] Performance considerations addressed +**Architectural Decisions** -**✅ Implementation Patterns** +- [ ] Critical decisions documented with versions +- [ ] Technology stack fully specified +- [ ] Integration patterns defined +- [ ] Performance considerations addressed -- [x] Naming conventions established -- [x] Structure patterns defined -- [x] Communication patterns specified -- [x] Process patterns documented +**Implementation Patterns** -**✅ Project Structure** +- [ ] Naming conventions established +- [ ] Structure patterns defined +- [ ] Communication patterns specified +- [ ] Process patterns documented -- [x] Complete directory structure defined -- [x] Component boundaries established -- [x] Integration points mapped -- [x] Requirements to structure mapping complete +**Project Structure** + +- [ ] Complete directory structure defined +- [ ] Component boundaries established +- [ ] Integration points mapped +- [ ] Requirements to structure mapping complete ### Architecture Readiness Assessment -**Overall Status:** READY FOR IMPLEMENTATION +**Overall Status:** {{READY FOR IMPLEMENTATION | READY WITH MINOR GAPS | NOT READY}} (choose READY FOR IMPLEMENTATION only when all 16 checklist items are `[x]` and no Critical Gaps remain; choose NOT READY when any Critical Gap is open or any Requirements Analysis or Architectural Decisions item is unchecked; otherwise READY WITH MINOR GAPS) **Confidence Level:** {{high/medium/low}} based on validation results diff --git a/src/bmm-skills/module-help.csv b/src/bmm-skills/module-help.csv index 8b824795f..78326a02e 100644 --- a/src/bmm-skills/module-help.csv +++ b/src/bmm-skills/module-help.csv @@ -1,33 +1,33 @@ module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt, -BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,anytime,,,false,project-knowledge,* -BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,anytime,,,false,output_folder,project context -BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,anytime,,,false,implementation_artifacts,spec and project implementation -BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,anytime,,,false,planning_artifacts,change proposal +BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,* +BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context +BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation +BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation -BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,1-analysis,,,false,planning_artifacts,brainstorming session -BMad Method,bmad-market-research,Market Research,MR,"Market analysis competitive landscape customer needs and trends.",,1-analysis,,,false,"planning_artifacts|project-knowledge",research documents -BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents -BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents +BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session +BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents +BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents +BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document -BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,2-planning,,,true,planning_artifacts,prd +BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,,2-planning,,,true,planning_artifacts,prd BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd -BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,2-planning,bmad-create-prd,,false,planning_artifacts,ux design -BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,3-solutioning,,,true,planning_artifacts,architecture -BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories -BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report -BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,4-implementation,,,true,implementation_artifacts,sprint status -BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,4-implementation,bmad-sprint-planning,,false,, -BMad Method,bmad-create-story,Create Story,CS,"Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.",create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story +BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-create-prd,,false,planning_artifacts,ux design +BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture +BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories +BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report +BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status +BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,, +BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report -BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,4-implementation,bmad-create-story:validate,,true,, -BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,4-implementation,bmad-dev-story,,false,, -BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,4-implementation,,,false,, -BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite -BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective +BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,, +BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,, +BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,, +BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite +BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective diff --git a/src/bmm-skills/module.yaml b/src/bmm-skills/module.yaml index cf3232614..490de183c 100644 --- a/src/bmm-skills/module.yaml +++ b/src/bmm-skills/module.yaml @@ -5,15 +5,11 @@ default_selected: true # This module will be selected by default for new install # Variables from Core Config inserted: ## user_name +## project_name ## communication_language ## document_output_language ## output_folder -project_name: - prompt: "What is your project called?" - default: "{directory_name}" - result: "{value}" - user_skill_level: prompt: - "What is your development experience level?" diff --git a/src/core-skills/module-help.csv b/src/core-skills/module-help.csv index f3521c743..fec435f18 100644 --- a/src/core-skills/module-help.csv +++ b/src/core-skills/module-help.csv @@ -1,13 +1,13 @@ module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt, -Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,anytime,,,false,{output_folder}/brainstorming,brainstorming session -Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,anytime,,,false,, -Core,bmad-help,BMad Help,BH,,,anytime,,,false,, -Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,anytime,,,false,, -Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,[path],anytime,,,false,, -Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes -Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,[path],anytime,,,false,report located with target document, -Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",[path],anytime,,,false,, -Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,[path],anytime,,,false,, -Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s) -Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,anytime,,,false,{project-root}/_bmad/custom,TOML override files +Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session +Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,, +Core,bmad-help,BMad Help,BH,,,,anytime,,,false,, +Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,, +Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,, +Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes +Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document, +Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,, +Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,, +Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s) +Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files diff --git a/src/core-skills/module.yaml b/src/core-skills/module.yaml index 0ccc68a78..b2b2650fb 100644 --- a/src/core-skills/module.yaml +++ b/src/core-skills/module.yaml @@ -11,6 +11,11 @@ user_name: default: "BMad" result: "{value}" +project_name: + prompt: "What is your project called?" + default: "{directory_name}" + result: "{value}" + communication_language: prompt: "What language should agents use when chatting with you?" scope: user diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 8da88958c..e92f7a757 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1933,12 +1933,12 @@ async function runTests() { const moduleConfigs = { core: { user_name: 'TestUser', + project_name: 'demo-project', communication_language: 'Spanish', document_output_language: 'English', output_folder: '_bmad-output', }, bmm: { - project_name: 'demo-project', user_skill_level: 'expert', planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', @@ -1946,7 +1946,10 @@ async function runTests() { // Spread-from-core pollution: legacy per-module config.yaml merges // core values into every module; writeCentralConfig must strip these // from [modules.bmm] so core values only live in [core]. + // project_name is now a core key (#2279), so it joins user_name etc. + // as a spread-from-core key that must be stripped. user_name: 'TestUser', + project_name: 'stale-bmm-copy', communication_language: 'Spanish', document_output_language: 'English', output_folder: '_bmad-output', @@ -1994,6 +1997,7 @@ async function runTests() { assert(teamContent.includes('[core]'), 'config.toml has [core] section'); assert(teamContent.includes('document_output_language = "English"'), 'Team-scope core key lands in config.toml'); assert(teamContent.includes('output_folder = "_bmad-output"'), 'Team-scope output_folder lands in config.toml'); + assert(teamContent.includes('project_name = "demo-project"'), 'project_name lands in [core] (core key as of #2279)'); assert(!teamContent.includes('user_name'), 'user_name (scope: user) is absent from config.toml'); assert(!teamContent.includes('communication_language'), 'communication_language (scope: user) is absent from config.toml'); @@ -2008,7 +2012,9 @@ async function runTests() { assert(bmmTeamMatch !== null, 'config.toml has [modules.bmm] section'); if (bmmTeamMatch) { const bmmTeamBlock = bmmTeamMatch[0]; - assert(bmmTeamBlock.includes('project_name = "demo-project"'), 'bmm team-scope key lands under [modules.bmm]'); + assert(bmmTeamBlock.includes('planning_artifacts'), 'bmm-owned team-scope key (planning_artifacts) lands under [modules.bmm]'); + assert(!bmmTeamBlock.includes('project_name'), 'project_name stripped from [modules.bmm] (now a core key, #2279)'); + assert(!bmmTeamBlock.includes('stale-bmm-copy'), 'stale bmm-copy of project_name not leaked into config.toml'); assert(!bmmTeamBlock.includes('user_name'), 'user_name stripped from [modules.bmm] (core-key pollution)'); assert(!bmmTeamBlock.includes('communication_language'), 'communication_language stripped from [modules.bmm]'); assert(!bmmTeamBlock.includes('user_skill_level'), 'user_skill_level (scope: user) absent from [modules.bmm] in config.toml'); @@ -3000,6 +3006,210 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 42: --tools flag parsing & validation (#2326) + // ============================================================ + console.log(`${colors.yellow}Test Suite 42: --tools flag parsing & validation${colors.reset}\n`); + try { + const { UI } = require('../tools/installer/ui'); + const ui = new UI(); + const known = new Set(['claude-code', 'cursor', 'windsurf']); + + assert( + JSON.stringify(ui._parseToolsFlag('claude-code', known)) === JSON.stringify(['claude-code']), + 'parseToolsFlag returns single ID', + ); + + assert( + JSON.stringify(ui._parseToolsFlag('claude-code,cursor', known)) === JSON.stringify(['claude-code', 'cursor']), + 'parseToolsFlag returns multiple IDs', + ); + + assert( + JSON.stringify(ui._parseToolsFlag(' claude-code , cursor ', known)) === JSON.stringify(['claude-code', 'cursor']), + 'parseToolsFlag trims whitespace', + ); + + let emptyErr; + try { + ui._parseToolsFlag('', known); + } catch (error) { + emptyErr = error; + } + assert( + emptyErr && emptyErr.expected === true && /empty/i.test(emptyErr.message), + 'parseToolsFlag rejects empty string with expected=true', + ); + + let commasOnlyErr; + try { + ui._parseToolsFlag(' , , ', known); + } catch (error) { + commasOnlyErr = error; + } + assert(commasOnlyErr && commasOnlyErr.expected === true, 'parseToolsFlag rejects whitespace/comma-only input'); + + let noneErr; + try { + ui._parseToolsFlag('none', known); + } catch (error) { + noneErr = error; + } + assert(noneErr && noneErr.expected === true && /Unknown tool ID/.test(noneErr.message), 'parseToolsFlag rejects "none" as unknown ID'); + + let typoErr; + try { + ui._parseToolsFlag('claude-code,claude-cdoe', known); + } catch (error) { + typoErr = error; + } + const typoHeader = typoErr ? typoErr.message.split('\n')[0] : ''; + assert( + typoErr && typoErr.expected === true && /claude-cdoe/.test(typoHeader) && !/claude-code/.test(typoHeader), + 'parseToolsFlag reports only the unknown ID in error header (valid ones not listed as unknown)', + ); + + // --list-tools and --tools validation must agree on what counts as a valid ID. + const { formatPlatformList } = require('../tools/installer/ide/platform-codes'); + const { IdeManager } = require('../tools/installer/ide/manager'); + const ideManager42 = new IdeManager(); + await ideManager42.ensureInitialized(); + const validIds = new Set(ideManager42.getAvailableIdes().map((i) => i.value)); + const listed = await formatPlatformList(); + // Each entry line starts with ' *' (preferred) or ' ' (other), followed by the ID, then padding. + const entryLines = listed.split('\n').filter((l) => /^( \*| {2})[a-z]/.test(l)); + const listedIds = entryLines.map((l) => l.trim().replace(/^\*/, '').split(/\s+/)[0]); + const missingFromList = [...validIds].filter((id) => !listedIds.includes(id)); + const extraInList = listedIds.filter((id) => !validIds.has(id)); + assert( + missingFromList.length === 0 && extraInList.length === 0, + '--list-tools output matches the IDs that --tools accepts', + `Missing from list: ${missingFromList.join(',') || '(none)'}; Extra in list: ${extraInList.join(',') || '(none)'}`, + ); + } catch (error) { + console.log(`${colors.red}Test Suite 42 setup failed: ${error.message}${colors.reset}`); + console.log(error.stack); + failed++; + } + + console.log(''); + + // ============================================================ + // Test Suite 43: project_name promoted to core + hoist migration (#2279) + // ============================================================ + console.log(`${colors.yellow}Test Suite 43: project_name in core + hoist migration${colors.reset}\n`); + try { + const yamlLib = require('yaml'); + const coreSchemaPath = path.join(__dirname, '..', 'src', 'core-skills', 'module.yaml'); + const bmmSchemaPath = path.join(__dirname, '..', 'src', 'bmm-skills', 'module.yaml'); + const coreSchema = yamlLib.parse(await fs.readFile(coreSchemaPath, 'utf8')); + const bmmSchema = yamlLib.parse(await fs.readFile(bmmSchemaPath, 'utf8')); + + assert( + coreSchema.project_name && coreSchema.project_name.prompt && coreSchema.project_name.default === '{directory_name}', + 'core/module.yaml declares project_name with {directory_name} default', + ); + + assert(coreSchema.project_name.scope === undefined, 'project_name has no user scope (project-scoped, not user-scoped)'); + + assert(bmmSchema.project_name === undefined, 'bmm/module.yaml no longer declares project_name (now inherited from core)'); + + // Set up a mock existing install: bmm directory has project_name (legacy), + // core has user_name but not project_name. After hoist, project_name should + // move to core, leaving bmm with only its own keys. + const fixtureRoot43 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43-')); + const bmadDir43 = path.join(fixtureRoot43, '_bmad'); + await fs.ensureDir(path.join(bmadDir43, '_config')); + await fs.writeFile(path.join(bmadDir43, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8'); + await fs.ensureDir(path.join(bmadDir43, 'core')); + await fs.ensureDir(path.join(bmadDir43, 'bmm')); + await fs.writeFile(path.join(bmadDir43, 'core', 'config.yaml'), 'user_name: alice\n', 'utf8'); + await fs.writeFile( + path.join(bmadDir43, 'bmm', 'config.yaml'), + 'project_name: legacy-from-bmm\nuser_skill_level: intermediate\n', + 'utf8', + ); + + const officialModules43 = new OfficialModules(); + await officialModules43.loadExistingConfig(fixtureRoot43); + + assert( + officialModules43.existingConfig.core?.project_name === 'legacy-from-bmm', + 'loadExistingConfig hoists bmm.project_name to core on existing-install upgrade', + ); + + assert( + !('project_name' in (officialModules43.existingConfig.bmm || {})), + 'loadExistingConfig removes project_name from bmm after hoisting', + ); + + assert( + officialModules43.existingConfig.bmm?.user_skill_level === 'intermediate', + 'loadExistingConfig leaves non-core bmm keys (user_skill_level) untouched', + ); + + assert(officialModules43.existingConfig.core?.user_name === 'alice', 'loadExistingConfig preserves pre-existing core values'); + + // Precedence: if core already has the key, hoist must NOT overwrite it. + const fixtureRoot43b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43b-')); + const bmadDir43b = path.join(fixtureRoot43b, '_bmad'); + await fs.ensureDir(path.join(bmadDir43b, '_config')); + await fs.writeFile(path.join(bmadDir43b, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8'); + await fs.ensureDir(path.join(bmadDir43b, 'core')); + await fs.ensureDir(path.join(bmadDir43b, 'bmm')); + await fs.writeFile(path.join(bmadDir43b, 'core', 'config.yaml'), 'project_name: from-core\n', 'utf8'); + await fs.writeFile(path.join(bmadDir43b, 'bmm', 'config.yaml'), 'project_name: stale-from-bmm\n', 'utf8'); + + const officialModules43b = new OfficialModules(); + await officialModules43b.loadExistingConfig(fixtureRoot43b); + + assert(officialModules43b.existingConfig.core?.project_name === 'from-core', 'hoist does not overwrite an existing core value'); + + assert( + !('project_name' in (officialModules43b.existingConfig.bmm || {})), + 'hoist still strips the duplicate from bmm so writeCentralConfig partition stays clean', + ); + + // Malformed config.yaml (parses to a scalar) must not crash loadExistingConfig + // or the hoist pass — they should treat it as "no config for that module" + // and continue. Regression for augment review on PR #2348. + const fixtureRoot43c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43c-')); + const bmadDir43c = path.join(fixtureRoot43c, '_bmad'); + await fs.ensureDir(path.join(bmadDir43c, '_config')); + await fs.writeFile(path.join(bmadDir43c, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8'); + await fs.ensureDir(path.join(bmadDir43c, 'core')); + await fs.ensureDir(path.join(bmadDir43c, 'bmm')); + // Scalar YAML — yaml.parse returns the literal 42 (truthy non-object). + // Pre-fix this crashed _hoistCoreKeysFromLegacyModuleConfigs with + // "Cannot use 'in' operator to search for 'project_name' in 42". + await fs.writeFile(path.join(bmadDir43c, 'core', 'config.yaml'), '42\n', 'utf8'); + await fs.writeFile(path.join(bmadDir43c, 'bmm', 'config.yaml'), 'project_name: rescued\n', 'utf8'); + + const officialModules43c = new OfficialModules(); + let crashErr; + try { + await officialModules43c.loadExistingConfig(fixtureRoot43c); + } catch (error) { + crashErr = error; + } + assert(!crashErr, 'loadExistingConfig does not crash on a scalar core/config.yaml', crashErr?.stack); + + assert( + officialModules43c.existingConfig.core?.project_name === 'rescued', + 'scalar core gets replaced with {} and bmm.project_name still hoists in', + ); + + await fs.remove(fixtureRoot43).catch(() => {}); + await fs.remove(fixtureRoot43b).catch(() => {}); + await fs.remove(fixtureRoot43c).catch(() => {}); + } catch (error) { + console.log(`${colors.red}Test Suite 43 setup failed: ${error.message}${colors.reset}`); + console.log(error.stack); + failed++; + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/commands/install.js b/tools/installer/commands/install.js index e10a0c96a..55adcfb9c 100644 --- a/tools/installer/commands/install.js +++ b/tools/installer/commands/install.js @@ -15,8 +15,9 @@ module.exports = { ['--modules ', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'], [ '--tools ', - 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.', + 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Required for fresh non-interactive (--yes) installs. Run with --list-tools to see all valid IDs.', ], + ['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'], ['--action ', 'Action type for existing installations: install, update, or quick-update'], ['--user-name ', 'Name for agents to use (default: system username)'], ['--communication-language ', 'Language for agent communication (default: English)'], @@ -40,6 +41,12 @@ module.exports = { ], action: async (options) => { try { + if (options.listTools) { + const { formatPlatformList } = require('../ide/platform-codes'); + process.stdout.write((await formatPlatformList()) + '\n'); + process.exit(0); + } + // Set debug flag as environment variable for all components if (options.debug) { process.env.BMAD_DEBUG_MANIFEST = 'true'; @@ -81,7 +88,7 @@ module.exports = { } else { await prompts.log.error(`Installation failed: ${error.message}`); } - if (error.stack) { + if (error.stack && !error.expected) { await prompts.log.message(error.stack); } } catch { diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index a68193bc6..b91ba6bb7 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -923,29 +923,15 @@ class Installer { /** * Merge all module-help.csv files into a single bmad-help.csv. * Scans all installed modules for module-help.csv and merges them. - * Enriches agent info from the in-memory agent list produced by ManifestGenerator. - * Output is written to _bmad/_config/bmad-help.csv. + * Output preserves the source schema verbatim — see schema below. * @param {string} bmadDir - BMAD installation directory - * @param {Array} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...) + * @param {Array} _agentEntries - Unused; retained for call-site compatibility */ - async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) { + async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) { const allRows = []; - const headerRow = - 'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs'; - - // Build agent lookup from the in-memory list (agent code → command + display fields). - const agentInfo = new Map(); - for (const agent of agentEntries) { - if (!agent || !agent.code) continue; - const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`; - const displayName = agent.name || agent.code; - const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code; - agentInfo.set(agent.code, { - command: agentCommand, - displayName, - title: titleCombined, - }); - } + const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs'; + const COLUMN_COUNT = 13; + const PHASE_INDEX = 7; // Get all installed module directories const entries = await fs.readdir(bmadDir, { withFileTypes: true }); @@ -984,64 +970,19 @@ class Installer { // Parse the line - handle quoted fields with commas const columns = this.parseCSVLine(line); - if (columns.length >= 12) { - // Map old schema to new schema - // Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs - // New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs + if (columns.length < COLUMN_COUNT - 1) continue; - const [ - module, - phase, - name, - code, - sequence, - workflowFile, - command, - required, - agentName, - options, - description, - outputLocation, - outputs, - ] = columns; + // Pad short rows; truncate over-long rows + const padded = columns.slice(0, COLUMN_COUNT); + while (padded.length < COLUMN_COUNT) padded.push(''); - // Pass through _meta rows as-is (module metadata, not a skill) - if (phase === '_meta') { - const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || ''; - const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', '']; - allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(',')); - continue; - } - - // If module column is empty, set it to this module's name (except for core which stays empty for universal tools) - const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || ''; - - // Lookup agent info - const cleanAgentName = agentName ? agentName.trim() : ''; - const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' }; - - // Build new row with agent info - const newRow = [ - finalModule, - phase || '', - name || '', - code || '', - sequence || '', - workflowFile || '', - command || '', - required || 'false', - cleanAgentName, - agentData.command, - agentData.displayName, - agentData.title, - options || '', - description || '', - outputLocation || '', - outputs || '', - ]; - - allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(',')); + // If module column is empty, fill with this module's name + // (core stays empty so its rows render as universal tools) + if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') { + padded[0] = moduleName; } + + allRows.push(padded.map((c) => this.escapeCSVField(c)).join(',')); } if (process.env.BMAD_VERBOSE_INSTALL === 'true') { @@ -1053,44 +994,34 @@ class Installer { } } - // Sort by module, then phase, then sequence - allRows.sort((a, b) => { - const colsA = this.parseCSVLine(a); - const colsB = this.parseCSVLine(b); + // Sort by module, then phase. Stable sort preserves authored order within a phase. + const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) })); + decorated.sort((a, b) => { + const moduleA = (a.cols[0] || '').toLowerCase(); + const moduleB = (b.cols[0] || '').toLowerCase(); + if (moduleA !== moduleB) return moduleA.localeCompare(moduleB); - // Module comparison (empty module/universal tools come first) - const moduleA = (colsA[0] || '').toLowerCase(); - const moduleB = (colsB[0] || '').toLowerCase(); - if (moduleA !== moduleB) { - return moduleA.localeCompare(moduleB); - } + const phaseA = a.cols[PHASE_INDEX] || ''; + const phaseB = b.cols[PHASE_INDEX] || ''; + if (phaseA !== phaseB) return phaseA.localeCompare(phaseB); - // Phase comparison - const phaseA = colsA[1] || ''; - const phaseB = colsB[1] || ''; - if (phaseA !== phaseB) { - return phaseA.localeCompare(phaseB); - } - - // Sequence comparison - const seqA = parseInt(colsA[4] || '0', 10); - const seqB = parseInt(colsB[4] || '0', 10); - return seqA - seqB; + return a.index - b.index; }); + const sortedRows = decorated.map((d) => d.row); // Write merged catalog const outputDir = path.join(bmadDir, '_config'); await fs.ensureDir(outputDir); const outputPath = path.join(outputDir, 'bmad-help.csv'); - const mergedContent = [headerRow, ...allRows].join('\n'); + const mergedContent = [headerRow, ...sortedRows].join('\n'); await fs.writeFile(outputPath, mergedContent, 'utf8'); // Track the installed file this.installedFiles.add(outputPath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`); + await prompts.log.message(` Generated bmad-help.csv: ${sortedRows.length} workflows`); } } diff --git a/tools/installer/ide/platform-codes.js b/tools/installer/ide/platform-codes.js index f29be8fcb..6d1aa9180 100644 --- a/tools/installer/ide/platform-codes.js +++ b/tools/installer/ide/platform-codes.js @@ -31,7 +31,50 @@ function clearCache() { _cachedPlatformCodes = null; } +/** + * Format the installable platform list for human-readable output (used by --list-tools). + * Sourced from IdeManager so this view matches what --tools accepts at install time + * (suspended platforms excluded). + * @returns {Promise} Formatted multi-line string with id, name, target_dir, preferred flag. + */ +async function formatPlatformList() { + const { IdeManager } = require('./manager'); + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + + const entries = ideManager.getAvailableIdes().map((ide) => { + const handler = ideManager.handlers.get(ide.value); + return { + id: ide.value, + name: ide.name, + targetDir: handler?.installerConfig?.target_dir || '', + preferred: ide.preferred, + }; + }); + + const idWidth = Math.max(...entries.map((e) => e.id.length), 'ID'.length); + const nameWidth = Math.max(...entries.map((e) => e.name.length), 'Name'.length); + + const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length)); + const lines = [ + `Supported tool IDs (pass via --tools [,...]):`, + '', + ` ${pad('ID', idWidth)} ${pad('Name', nameWidth)} Target dir`, + ` ${pad('-'.repeat(idWidth), idWidth)} ${pad('-'.repeat(nameWidth), nameWidth)} ${'-'.repeat(10)}`, + ]; + + for (const e of entries) { + const star = e.preferred ? ' *' : ' '; + lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`); + } + + lines.push('', '* = recommended / preferred', '', 'Example: bmad-method install --modules bmm --tools claude-code'); + + return lines.join('\n'); +} + module.exports = { loadPlatformCodes, clearCache, + formatPlatformList, }; diff --git a/tools/installer/modules/custom-module-manager.js b/tools/installer/modules/custom-module-manager.js index f6a26ba37..ca3e52325 100644 --- a/tools/installer/modules/custom-module-manager.js +++ b/tools/installer/modules/custom-module-manager.js @@ -24,8 +24,9 @@ class CustomModuleManager { /** * Parse a user-provided source input into a structured descriptor. - * Accepts local file paths, HTTPS Git URLs, and SSH Git URLs. - * For HTTPS URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir. + * Accepts local file paths, HTTPS Git URLs, HTTP Git URLs, and SSH Git URLs. + * For HTTPS/HTTP URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir. + * The original protocol (http or https) is preserved in the returned cloneUrl. * * @param {string} input - URL or local file path * @returns {Object} Parsed source descriptor: @@ -127,11 +128,11 @@ class CustomModuleManager { }; } - // HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git] - const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/); + // HTTPS/HTTP URL: https://host/owner/repo[/tree/branch/subdir][.git] + const httpsMatch = trimmed.match(/^(https?):\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/); if (httpsMatch) { - const [, host, owner, repo, remainder] = httpsMatch; - const cloneUrl = `https://${host}/${owner}/${repo}`; + const [, protocol, host, owner, repo, remainder] = httpsMatch; + const cloneUrl = `${protocol}://${host}/${owner}/${repo}`; let subdir = null; let urlRef = null; // branch/tag extracted from /tree//subdir @@ -311,7 +312,7 @@ class CustomModuleManager { /** * Clone a custom module repository to cache. * Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted, etc.). - * @param {string} sourceInput - Git URL (HTTPS or SSH) + * @param {string} sourceInput - Git URL (HTTPS, HTTP, or SSH) * @param {Object} [options] - Clone options * @param {boolean} [options.silent] - Suppress spinner output * @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms) diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 4bd1e56b3..615daba86 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -903,7 +903,10 @@ class OfficialModules { try { const content = await fs.readFile(moduleConfigPath, 'utf8'); const moduleConfig = yaml.parse(content); - if (moduleConfig) { + // Only keep plain object parses. A corrupt config.yaml that parses + // to a scalar or array would crash later code that does `key in cfg` + // / `Object.keys(cfg)`; treat it the same as a parse error. + if (moduleConfig && typeof moduleConfig === 'object' && !Array.isArray(moduleConfig)) { this._existingConfig[entry.name] = moduleConfig; foundAny = true; } @@ -914,9 +917,58 @@ class OfficialModules { } } + if (foundAny) { + await this._hoistCoreKeysFromLegacyModuleConfigs(); + } + return foundAny; } + /** + * Migrate prior answers when a key has moved from a non-core module to core + * (e.g. project_name moving from bmm to core in #2279). Without this, the + * partition logic in writeCentralConfig drops the value from the bmm bucket + * (because it's now a core key) without re-homing it under [core], so the + * user's prior answer silently disappears on the next install/quick-update. + */ + async _hoistCoreKeysFromLegacyModuleConfigs() { + const coreSchemaPath = path.join(getSourcePath(), 'core-skills', 'module.yaml'); + if (!(await fs.pathExists(coreSchemaPath))) return; + + let coreSchema; + try { + coreSchema = yaml.parse(await fs.readFile(coreSchemaPath, 'utf8')); + } catch { + return; + } + if (!coreSchema || typeof coreSchema !== 'object') return; + + const coreKeys = new Set( + Object.entries(coreSchema) + .filter(([, v]) => v && typeof v === 'object' && 'prompt' in v) + .map(([k]) => k), + ); + if (coreKeys.size === 0) return; + + // Belt-and-suspenders: loadExistingConfig already filters non-object parses, + // but anyone calling _hoistCoreKeysFromLegacyModuleConfigs in isolation (or + // future code paths populating _existingConfig directly) shouldn't be able + // to crash this with a scalar / array. + const existingCore = this._existingConfig.core; + this._existingConfig.core = existingCore && typeof existingCore === 'object' && !Array.isArray(existingCore) ? existingCore : {}; + + for (const [moduleName, cfg] of Object.entries(this._existingConfig)) { + if (moduleName === 'core' || !cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue; + for (const key of Object.keys(cfg)) { + if (!coreKeys.has(key)) continue; + if (!(key in this._existingConfig.core)) { + this._existingConfig.core[key] = cfg[key]; + } + delete cfg[key]; + } + } + } + /** * Pre-scan module schemas to gather metadata for the configuration gateway prompt. * Returns info about which modules have configurable options. diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 4ec0ef118..12501b3f2 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -200,12 +200,15 @@ class UI { actionType = options.action; await prompts.log.info(`Using action from command-line: ${actionType}`); } else if (options.yes) { - // Default to quick-update if available, otherwise first available choice + // Default to quick-update if available, unless flags that require the + // full update path are present (e.g. --custom-source which re-clones + // modules at a new version — quick-update skips that entirely). if (choices.length === 0) { throw new Error('No valid actions available for this installation'); } const hasQuickUpdate = choices.some((c) => c.value === 'quick-update'); - actionType = hasQuickUpdate ? 'quick-update' : choices[0].value; + const needsFullUpdate = !!options.customSource; + actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value; await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`); } else { actionType = await prompts.select({ @@ -241,8 +244,11 @@ class UI { .map((m) => m.trim()) .filter(Boolean); await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); - } else if (options.customSource) { - // Custom source without --modules: start with empty list (core added below) + } else if (options.customSource && !options.yes) { + // Custom source without --modules or --yes: start with empty list + // (only custom source modules + core will be installed). + // When --yes is also set, fall through to the --yes branch so all + // installed modules are included alongside the custom source modules. selectedModules = []; } else if (options.yes) { selectedModules = await this.getDefaultModules(installedModuleIds); @@ -398,6 +404,37 @@ class UI { * @param {Object} options - Command-line options * @returns {Object} Tool configuration */ + _parseToolsFlag(toolsArg, allKnownValues) { + const selectedIdes = toolsArg + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + + if (selectedIdes.length === 0) { + const err = new Error( + '--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.', + ); + err.expected = true; + throw err; + } + + const unknown = selectedIdes.filter((id) => !allKnownValues.has(id)); + if (unknown.length > 0) { + const err = new Error( + [ + `Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`, + '', + 'Run with --list-tools to see all valid IDs.', + 'Common: claude-code, cursor, copilot, windsurf, cline', + ].join('\n'), + ); + err.expected = true; + throw err; + } + + return selectedIdes; + } + async promptToolSelection(projectDir, options = {}) { const { ExistingInstall } = require('./core/existing-install'); const { Installer } = require('./core/installer'); @@ -432,15 +469,10 @@ class UI { const allTools = [...preferredIdes, ...otherIdes]; // Non-interactive: handle --tools and --yes flags before interactive prompt - if (options.tools) { - if (options.tools.toLowerCase() === 'none') { - await prompts.log.info('Skipping tool configuration (--tools none)'); - return { ides: [], skipIde: true }; - } - const selectedIdes = options.tools - .split(',') - .map((t) => t.trim()) - .filter(Boolean); + // Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and + // gets a specific "passed empty" error instead of being silently ignored. + if (options.tools !== undefined) { + const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues); await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); return { ides: selectedIdes, skipIde: false }; @@ -516,21 +548,13 @@ class UI { let selectedIdes = []; - // Check if tools are provided via command-line - if (options.tools) { - // Check for explicit "none" value to skip tool installation - if (options.tools.toLowerCase() === 'none') { - await prompts.log.info('Skipping tool configuration (--tools none)'); - return { ides: [], skipIde: true }; - } else { - selectedIdes = options.tools - .split(',') - .map((t) => t.trim()) - .filter(Boolean); - await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); - await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); - return { ides: selectedIdes, skipIde: false }; - } + // Check if tools are provided via command-line. + // Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error. + if (options.tools !== undefined) { + selectedIdes = this._parseToolsFlag(options.tools, allKnownValues); + await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + return { ides: selectedIdes, skipIde: false }; } else if (options.yes) { // If --yes flag is set, skip tool prompt and use previously configured tools or empty if (configuredIdes.length > 0) { @@ -538,8 +562,18 @@ class UI { await this.displaySelectedTools(configuredIdes, preferredIdes, allTools); return { ides: configuredIdes, skipIde: false }; } else { - await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)'); - return { ides: [], skipIde: true }; + const err = new Error( + [ + '--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.', + '', + 'Common: claude-code, cursor, copilot, windsurf, cline', + 'See all supported tools: bmad-method install --list-tools', + '', + 'Example: bmad-method install --modules bmm --tools claude-code -y', + ].join('\n'), + ); + err.expected = true; + throw err; } } @@ -724,6 +758,9 @@ class UI { const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); configCollector.collectedConfig.core = { user_name: defaultUsername, + // {directory_name} default per src/core-skills/module.yaml — matches what the + // interactive flow resolves via buildQuestion()'s {directory_name} placeholder. + project_name: path.basename(directory), communication_language: 'English', document_output_language: 'English', output_folder: '_bmad-output',