bmad-method 6.5.1-next.9 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@
13
13
  "name": "bmad-pro-skills",
14
14
  "source": "./",
15
15
  "description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
16
- "version": "6.3.0",
16
+ "version": "6.6.0",
17
17
  "author": {
18
18
  "name": "Brian (BMad) Madison"
19
19
  },
@@ -35,7 +35,7 @@
35
35
  "name": "bmad-method-lifecycle",
36
36
  "source": "./",
37
37
  "description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
38
- "version": "6.3.0",
38
+ "version": "6.6.0",
39
39
  "author": {
40
40
  "name": "Brian (BMad) Madison"
41
41
  },
package/README.md CHANGED
@@ -52,6 +52,15 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, etc.)
52
52
  npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
53
53
  ```
54
54
 
55
+ Override any module config option with `--set <module>.<key>=<value>` (repeatable). Run `--list-options [module]` to see locally-known official keys (built-in modules plus any external officials cached on this machine):
56
+
57
+ ```bash
58
+ npx bmad-method install --yes \
59
+ --modules bmm --tools claude-code \
60
+ --set bmm.project_knowledge=research \
61
+ --set bmm.user_skill_level=expert
62
+ ```
63
+
55
64
  [See all installation options](https://docs.bmad-method.org/how-to/non-interactive-installation/)
56
65
 
57
66
  > **Not sure what to do?** Ask `bmad-help` — it tells you exactly what's next and what's optional. You can also ask questions like `bmad-help I just finished the architecture, what do I do next?`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.5.1-next.9",
4
+ "version": "6.6.0",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -39,12 +39,13 @@
39
39
  "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
40
40
  "lint:md": "markdownlint-cli2 \"**/*.md\"",
41
41
  "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
42
- "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
42
+ "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run validate:refs && npm run validate:skills",
43
43
  "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
44
- "test": "npm run test:refs && npm run test:install && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
44
+ "test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
45
45
  "test:channels": "node test/test-installer-channels.js",
46
46
  "test:install": "node test/test-installation-components.js",
47
47
  "test:refs": "node test/test-file-refs-csv.js",
48
+ "test:urls": "node test/test-parse-source-urls.js",
48
49
  "validate:refs": "node tools/validate-file-refs.js --strict",
49
50
  "validate:skills": "node tools/validate-skills.js --strict"
50
51
  },
@@ -1,33 +1,33 @@
1
1
  module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
2
2
  BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
3
- BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,anytime,,,false,project-knowledge,*
4
- 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
5
- 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
6
- 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
3
+ BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
4
+ 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
5
+ 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
6
+ 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
7
7
  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
8
8
  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
9
9
  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
10
10
  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
11
11
  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
12
- BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,1-analysis,,,false,planning_artifacts,brainstorming session
13
- 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
14
- 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
15
- BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
12
+ BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
13
+ 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
14
+ 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
15
+ BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
16
16
  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
17
17
  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
18
- BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,2-planning,,,true,planning_artifacts,prd
18
+ BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,,2-planning,,,true,planning_artifacts,prd
19
19
  BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report
20
20
  BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd
21
- 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
22
- BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,3-solutioning,,,true,planning_artifacts,architecture
23
- BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
24
- 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
25
- 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
26
- BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,4-implementation,bmad-sprint-planning,,false,,
27
- 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
21
+ 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
22
+ BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
23
+ BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
24
+ 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
25
+ 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
26
+ BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
27
+ 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
28
28
  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
29
- 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,,
30
- 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,,
31
- 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,,
32
- 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
33
- 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
29
+ 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,,
30
+ 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,,
31
+ 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,,
32
+ 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
33
+ 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
@@ -1,13 +1,13 @@
1
1
  module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
2
2
  Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
3
- Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
4
- Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,anytime,,,false,,
5
- Core,bmad-help,BMad Help,BH,,,anytime,,,false,,
6
- Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,anytime,,,false,,
7
- Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,[path],anytime,,,false,,
8
- 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
9
- 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,
10
- 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,,
11
- 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,,
12
- 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)
13
- 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
3
+ Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
4
+ Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
5
+ Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
6
+ Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
7
+ Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
8
+ 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
9
+ 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,
10
+ 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,,
11
+ 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,,
12
+ 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)
13
+ 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
@@ -18,6 +18,16 @@ module.exports = {
18
18
  '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.',
19
19
  ],
20
20
  ['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
21
+ [
22
+ '--set <spec>',
23
+ 'Set a module config option non-interactively. Spec format: <module>.<key>=<value> (e.g. bmm.project_knowledge=research). Repeatable. Run --list-options to see available keys.',
24
+ (value, prev) => [...(prev || []), value],
25
+ [],
26
+ ],
27
+ [
28
+ '--list-options [module]',
29
+ 'List available --set keys for all locally-known official modules, or for a single module by code, then exit.',
30
+ ],
21
31
  ['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
22
32
  ['--user-name <name>', 'Name for agents to use (default: system username)'],
23
33
  ['--communication-language <lang>', 'Language for agent communication (default: English)'],
@@ -47,12 +57,43 @@ module.exports = {
47
57
  process.exit(0);
48
58
  }
49
59
 
60
+ if (options.listOptions !== undefined) {
61
+ const { formatOptionsList } = require('../list-options');
62
+ const moduleArg = options.listOptions === true ? null : options.listOptions;
63
+ const { text, ok } = await formatOptionsList(moduleArg);
64
+ const stream = ok ? process.stdout : process.stderr;
65
+ // process.exit() forces immediate termination and can truncate the
66
+ // buffered write when stdout/stderr is piped or captured by CI. Wait
67
+ // for the write to flush, then set process.exitCode and return so the
68
+ // event loop drains naturally. Non-zero exit when a single-module
69
+ // lookup misses so a CI typo like `--list-options bmn` doesn't look
70
+ // successful in scripts.
71
+ await new Promise((resolve, reject) => {
72
+ stream.write(text + '\n', (error) => (error ? reject(error) : resolve()));
73
+ });
74
+ process.exitCode = ok ? 0 : 1;
75
+ return;
76
+ }
77
+
50
78
  // Set debug flag as environment variable for all components
51
79
  if (options.debug) {
52
80
  process.env.BMAD_DEBUG_MANIFEST = 'true';
53
81
  await prompts.log.info('Debug mode enabled');
54
82
  }
55
83
 
84
+ // Validate --set syntax up-front so malformed entries fail fast,
85
+ // before we touch the network or filesystem. Parsed entries are
86
+ // re-derived inside ui.js where overrides are seeded.
87
+ if (options.set && options.set.length > 0) {
88
+ const { parseSetEntries } = require('../set-overrides');
89
+ try {
90
+ parseSetEntries(options.set);
91
+ } catch (error) {
92
+ await prompts.log.error(error.message);
93
+ process.exit(1);
94
+ }
95
+ }
96
+
56
97
  const config = await ui.promptInstall(options);
57
98
 
58
99
  // Handle cancel
@@ -61,8 +102,13 @@ module.exports = {
61
102
  process.exit(0);
62
103
  }
63
104
 
64
- // Handle quick update separately
105
+ // Handle quick update separately. --set is a post-install TOML patch so
106
+ // it works the same way for quick-update as for a regular install — the
107
+ // installer runs, then `applySetOverrides` patches the central config
108
+ // files. Pass the parsed overrides through.
65
109
  if (config.actionType === 'quick-update') {
110
+ const { parseSetEntries } = require('../set-overrides');
111
+ config.setOverrides = parseSetEntries(options.set || []);
66
112
  const result = await installer.quickUpdate(config);
67
113
  await prompts.log.success('Quick update complete!');
68
114
  await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
@@ -3,7 +3,19 @@
3
3
  * User input comes from either UI answers or headless CLI flags.
4
4
  */
5
5
  class Config {
6
- constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate, channelOptions }) {
6
+ constructor({
7
+ directory,
8
+ modules,
9
+ ides,
10
+ skipPrompts,
11
+ verbose,
12
+ actionType,
13
+ coreConfig,
14
+ moduleConfigs,
15
+ quickUpdate,
16
+ channelOptions,
17
+ setOverrides,
18
+ }) {
7
19
  this.directory = directory;
8
20
  this.modules = Object.freeze([...modules]);
9
21
  this.ides = Object.freeze([...ides]);
@@ -15,6 +27,11 @@ class Config {
15
27
  this._quickUpdate = quickUpdate;
16
28
  // channelOptions carry a Map + Set; don't deep-freeze.
17
29
  this.channelOptions = channelOptions || null;
30
+ // Parsed `--set <module>.<key>=<value>` overrides, applied as a TOML
31
+ // patch AFTER the install finishes. Shape: { moduleCode: { key: value } }.
32
+ // Intentionally NOT integrated with the prompt/template/schema flow; see
33
+ // `tools/installer/set-overrides.js` for the rationale and tradeoffs.
34
+ this.setOverrides = setOverrides || {};
18
35
  Object.freeze(this);
19
36
  }
20
37
 
@@ -40,6 +57,7 @@ class Config {
40
57
  moduleConfigs: userInput.moduleConfigs || null,
41
58
  quickUpdate: userInput._quickUpdate || false,
42
59
  channelOptions: userInput.channelOptions || null,
60
+ setOverrides: userInput.setOverrides || {},
43
61
  });
44
62
  }
45
63
 
@@ -310,6 +310,19 @@ class Installer {
310
310
  moduleConfigs,
311
311
  });
312
312
 
313
+ // Apply post-install --set TOML patches. Runs after writeCentralConfig
314
+ // (inside generateManifests above) so the patch operates on the
315
+ // freshly written `_bmad/config.toml` / `_bmad/config.user.toml`.
316
+ // See `tools/installer/set-overrides.js` for routing rules.
317
+ if (config.setOverrides && Object.keys(config.setOverrides).length > 0) {
318
+ const { applySetOverrides } = require('../set-overrides');
319
+ const applied = await applySetOverrides(config.setOverrides, paths.bmadDir);
320
+ if (applied.length > 0) {
321
+ const summary = applied.map((a) => `${a.module}.${a.key} → ${a.file}`).join(', ');
322
+ await prompts.log.info(`Applied --set overrides: ${summary}`);
323
+ }
324
+ }
325
+
313
326
  message('Generating help catalog...');
314
327
  await this.mergeModuleHelpCatalogs(paths.bmadDir, manifestGen.agents);
315
328
  addResult('Help catalog', 'ok');
@@ -923,29 +936,15 @@ class Installer {
923
936
  /**
924
937
  * Merge all module-help.csv files into a single bmad-help.csv.
925
938
  * Scans all installed modules for module-help.csv and merges them.
926
- * Enriches agent info from the in-memory agent list produced by ManifestGenerator.
927
- * Output is written to _bmad/_config/bmad-help.csv.
939
+ * Output preserves the source schema verbatim see schema below.
928
940
  * @param {string} bmadDir - BMAD installation directory
929
- * @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...)
941
+ * @param {Array<Object>} _agentEntries - Unused; retained for call-site compatibility
930
942
  */
931
- async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) {
943
+ async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
932
944
  const allRows = [];
933
- const headerRow =
934
- 'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
935
-
936
- // Build agent lookup from the in-memory list (agent code → command + display fields).
937
- const agentInfo = new Map();
938
- for (const agent of agentEntries) {
939
- if (!agent || !agent.code) continue;
940
- const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
941
- const displayName = agent.name || agent.code;
942
- const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
943
- agentInfo.set(agent.code, {
944
- command: agentCommand,
945
- displayName,
946
- title: titleCombined,
947
- });
948
- }
945
+ const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
946
+ const COLUMN_COUNT = 13;
947
+ const PHASE_INDEX = 7;
949
948
 
950
949
  // Get all installed module directories
951
950
  const entries = await fs.readdir(bmadDir, { withFileTypes: true });
@@ -984,64 +983,19 @@ class Installer {
984
983
 
985
984
  // Parse the line - handle quoted fields with commas
986
985
  const columns = this.parseCSVLine(line);
987
- if (columns.length >= 12) {
988
- // Map old schema to new schema
989
- // Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
990
- // New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
991
-
992
- const [
993
- module,
994
- phase,
995
- name,
996
- code,
997
- sequence,
998
- workflowFile,
999
- command,
1000
- required,
1001
- agentName,
1002
- options,
1003
- description,
1004
- outputLocation,
1005
- outputs,
1006
- ] = columns;
1007
-
1008
- // Pass through _meta rows as-is (module metadata, not a skill)
1009
- if (phase === '_meta') {
1010
- const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
1011
- const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', ''];
1012
- allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(','));
1013
- continue;
1014
- }
986
+ if (columns.length < COLUMN_COUNT - 1) continue;
1015
987
 
1016
- // If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
1017
- const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
1018
-
1019
- // Lookup agent info
1020
- const cleanAgentName = agentName ? agentName.trim() : '';
1021
- const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
1022
-
1023
- // Build new row with agent info
1024
- const newRow = [
1025
- finalModule,
1026
- phase || '',
1027
- name || '',
1028
- code || '',
1029
- sequence || '',
1030
- workflowFile || '',
1031
- command || '',
1032
- required || 'false',
1033
- cleanAgentName,
1034
- agentData.command,
1035
- agentData.displayName,
1036
- agentData.title,
1037
- options || '',
1038
- description || '',
1039
- outputLocation || '',
1040
- outputs || '',
1041
- ];
1042
-
1043
- allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
988
+ // Pad short rows; truncate over-long rows
989
+ const padded = columns.slice(0, COLUMN_COUNT);
990
+ while (padded.length < COLUMN_COUNT) padded.push('');
991
+
992
+ // If module column is empty, fill with this module's name
993
+ // (core stays empty so its rows render as universal tools)
994
+ if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') {
995
+ padded[0] = moduleName;
1044
996
  }
997
+
998
+ allRows.push(padded.map((c) => this.escapeCSVField(c)).join(','));
1045
999
  }
1046
1000
 
1047
1001
  if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
@@ -1053,44 +1007,34 @@ class Installer {
1053
1007
  }
1054
1008
  }
1055
1009
 
1056
- // Sort by module, then phase, then sequence
1057
- allRows.sort((a, b) => {
1058
- const colsA = this.parseCSVLine(a);
1059
- const colsB = this.parseCSVLine(b);
1010
+ // Sort by module, then phase. Stable sort preserves authored order within a phase.
1011
+ const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) }));
1012
+ decorated.sort((a, b) => {
1013
+ const moduleA = (a.cols[0] || '').toLowerCase();
1014
+ const moduleB = (b.cols[0] || '').toLowerCase();
1015
+ if (moduleA !== moduleB) return moduleA.localeCompare(moduleB);
1060
1016
 
1061
- // Module comparison (empty module/universal tools come first)
1062
- const moduleA = (colsA[0] || '').toLowerCase();
1063
- const moduleB = (colsB[0] || '').toLowerCase();
1064
- if (moduleA !== moduleB) {
1065
- return moduleA.localeCompare(moduleB);
1066
- }
1067
-
1068
- // Phase comparison
1069
- const phaseA = colsA[1] || '';
1070
- const phaseB = colsB[1] || '';
1071
- if (phaseA !== phaseB) {
1072
- return phaseA.localeCompare(phaseB);
1073
- }
1017
+ const phaseA = a.cols[PHASE_INDEX] || '';
1018
+ const phaseB = b.cols[PHASE_INDEX] || '';
1019
+ if (phaseA !== phaseB) return phaseA.localeCompare(phaseB);
1074
1020
 
1075
- // Sequence comparison
1076
- const seqA = parseInt(colsA[4] || '0', 10);
1077
- const seqB = parseInt(colsB[4] || '0', 10);
1078
- return seqA - seqB;
1021
+ return a.index - b.index;
1079
1022
  });
1023
+ const sortedRows = decorated.map((d) => d.row);
1080
1024
 
1081
1025
  // Write merged catalog
1082
1026
  const outputDir = path.join(bmadDir, '_config');
1083
1027
  await fs.ensureDir(outputDir);
1084
1028
  const outputPath = path.join(outputDir, 'bmad-help.csv');
1085
1029
 
1086
- const mergedContent = [headerRow, ...allRows].join('\n');
1030
+ const mergedContent = [headerRow, ...sortedRows].join('\n');
1087
1031
  await fs.writeFile(outputPath, mergedContent, 'utf8');
1088
1032
 
1089
1033
  // Track the installed file
1090
1034
  this.installedFiles.add(outputPath);
1091
1035
 
1092
1036
  if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
1093
- await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`);
1037
+ await prompts.log.message(` Generated bmad-help.csv: ${sortedRows.length} workflows`);
1094
1038
  }
1095
1039
  }
1096
1040
 
@@ -1352,6 +1296,10 @@ class Installer {
1352
1296
  ides: configuredIdes,
1353
1297
  coreConfig: quickModules.collectedConfig.core,
1354
1298
  moduleConfigs: quickModules.collectedConfig,
1299
+ // Forward `--set` overrides so the post-install patch step
1300
+ // (`applySetOverrides`) runs at the end of quick-update too. The
1301
+ // installer.install path applies them after writeCentralConfig.
1302
+ setOverrides: config.setOverrides || {},
1355
1303
  actionType: 'install',
1356
1304
  _quickUpdate: true,
1357
1305
  _preserveModules: skippedModules,
@@ -0,0 +1,210 @@
1
+ const path = require('node:path');
2
+ const fs = require('./fs-native');
3
+ const yaml = require('yaml');
4
+ const { getProjectRoot, getModulePath, getExternalModuleCachePath } = require('./project-root');
5
+
6
+ /**
7
+ * Read a module.yaml and return its declared `code:` field, or null if missing/unparseable.
8
+ */
9
+ async function readModuleCode(yamlPath) {
10
+ try {
11
+ const parsed = yaml.parse(await fs.readFile(yamlPath, 'utf8'));
12
+ if (parsed && typeof parsed === 'object' && typeof parsed.code === 'string') {
13
+ return parsed.code;
14
+ }
15
+ } catch {
16
+ // fall through
17
+ }
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Discover module.yaml files for officials we can read locally:
23
+ * - core, bmm: bundled in src/ (always present)
24
+ * - external officials: only if previously cloned to ~/.bmad/cache/external-modules/
25
+ *
26
+ * Each result's `code` is the `code:` field from the module.yaml when present;
27
+ * that's the value `--set <module>.<key>=<value>` matches against.
28
+ *
29
+ * Community/custom modules are not enumerated; users reference their own
30
+ * module.yaml directly per the design (see issue #1663).
31
+ *
32
+ * @returns {Promise<Array<{code: string, yamlPath: string, source: string}>>}
33
+ */
34
+ async function discoverOfficialModuleYamls() {
35
+ const found = [];
36
+ // Dedupe is case-insensitive because module caches occasionally retain a
37
+ // legacy UPPERCASE-named directory alongside the canonical lowercase one
38
+ // (same module, different cache key from an older schema). We pick whichever
39
+ // entry we see first and skip the alternate-case duplicate. NOTE: `--set`
40
+ // matching itself is case-sensitive (it keys on `moduleName` from the install
41
+ // flow's selected list, which is always lowercase short codes), so the
42
+ // surfaced `code` here is what users should type. Don't change to
43
+ // case-sensitive dedupe without revisiting that contract.
44
+ const seenCodes = new Set();
45
+
46
+ const addFound = async (yamlPath, source, fallbackCode) => {
47
+ const declaredCode = await readModuleCode(yamlPath);
48
+ const code = declaredCode || fallbackCode;
49
+ if (!code) return;
50
+ const lower = code.toLowerCase();
51
+ if (seenCodes.has(lower)) return;
52
+ seenCodes.add(lower);
53
+ found.push({ code, yamlPath, source });
54
+ };
55
+
56
+ // Built-ins.
57
+ for (const code of ['core', 'bmm']) {
58
+ const yamlPath = path.join(getModulePath(code), 'module.yaml');
59
+ if (await fs.pathExists(yamlPath)) {
60
+ // Built-ins use their well-known short codes regardless of what the
61
+ // module.yaml `code:` says, since the install flow keys on these.
62
+ seenCodes.add(code.toLowerCase());
63
+ found.push({ code, yamlPath, source: 'built-in' });
64
+ }
65
+ }
66
+
67
+ // Bundled in src/modules/<code>/module.yaml (rare, but supported by getModulePath).
68
+ const srcModulesDir = path.join(getProjectRoot(), 'src', 'modules');
69
+ if (await fs.pathExists(srcModulesDir)) {
70
+ const entries = await fs.readdir(srcModulesDir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (!entry.isDirectory()) continue;
73
+ const yamlPath = path.join(srcModulesDir, entry.name, 'module.yaml');
74
+ if (await fs.pathExists(yamlPath)) {
75
+ await addFound(yamlPath, 'bundled', entry.name);
76
+ }
77
+ }
78
+ }
79
+
80
+ // External cache (~/.bmad/cache/external-modules/<code>/...).
81
+ const cacheRoot = getExternalModuleCachePath('').replace(/\/$/, '');
82
+ if (await fs.pathExists(cacheRoot)) {
83
+ const rawEntries = await fs.readdir(cacheRoot, { withFileTypes: true });
84
+ for (const entry of rawEntries) {
85
+ if (!entry.isDirectory()) continue;
86
+ const candidates = [
87
+ path.join(cacheRoot, entry.name, 'module.yaml'),
88
+ path.join(cacheRoot, entry.name, 'src', 'module.yaml'),
89
+ path.join(cacheRoot, entry.name, 'skills', 'module.yaml'),
90
+ ];
91
+ for (const candidate of candidates) {
92
+ if (await fs.pathExists(candidate)) {
93
+ await addFound(candidate, 'cached', entry.name);
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return found;
101
+ }
102
+
103
+ function formatPromptText(item) {
104
+ if (Array.isArray(item.prompt)) return item.prompt.join(' ');
105
+ return String(item.prompt || '').trim();
106
+ }
107
+
108
+ function inferType(item) {
109
+ if (item['single-select']) return 'single-select';
110
+ if (item['multi-select']) return 'multi-select';
111
+ if (typeof item.default === 'boolean') return 'boolean';
112
+ if (typeof item.default === 'number') return 'number';
113
+ return 'string';
114
+ }
115
+
116
+ function formatModuleOptions(code, parsed, source) {
117
+ const lines = [];
118
+ const header = source === 'built-in' ? code : `${code} (${source})`;
119
+ lines.push(header + ':');
120
+
121
+ let count = 0;
122
+ for (const [key, item] of Object.entries(parsed)) {
123
+ if (!item || typeof item !== 'object' || !('prompt' in item)) continue;
124
+ count++;
125
+ const type = inferType(item);
126
+ const scope = item.scope === 'user' ? ' [user-scope]' : '';
127
+ const defaultStr = item.default === undefined || item.default === null ? '(none)' : String(item.default);
128
+ lines.push(` ${code}.${key} (${type}${scope}) default: ${defaultStr}`);
129
+ const promptText = formatPromptText(item);
130
+ if (promptText) lines.push(` ${promptText}`);
131
+ if (Array.isArray(item['single-select'])) {
132
+ const values = item['single-select'].map((v) => (typeof v === 'object' ? v.value : v)).filter((v) => v !== undefined);
133
+ if (values.length > 0) lines.push(` values: ${values.join(' | ')}`);
134
+ }
135
+ lines.push('');
136
+ }
137
+
138
+ if (count === 0) {
139
+ lines.push(' (no configurable options)', '');
140
+ }
141
+ return lines.join('\n');
142
+ }
143
+
144
+ /**
145
+ * Render `--list-options` output.
146
+ *
147
+ * Returns `{ text, ok }` so callers can surface a non-zero exit code on
148
+ * a typo'd module-code lookup. Discovery dedupes case-insensitively, so
149
+ * the lookup is also case-insensitive — typing `--list-options BMM` and
150
+ * `--list-options bmm` both find the bmm built-in.
151
+ *
152
+ * @param {string|null} moduleCode - if non-null, restrict to this module
153
+ * @returns {Promise<{text: string, ok: boolean}>}
154
+ */
155
+ async function formatOptionsList(moduleCode) {
156
+ const discovered = await discoverOfficialModuleYamls();
157
+ const needle = moduleCode ? moduleCode.toLowerCase() : null;
158
+ const filtered = needle ? discovered.filter((d) => d.code.toLowerCase() === needle) : discovered;
159
+
160
+ if (filtered.length === 0) {
161
+ if (moduleCode) {
162
+ const text = [
163
+ `No locally-known module.yaml for '${moduleCode}'.`,
164
+ '',
165
+ 'Built-in modules (core, bmm) are always available. External officials',
166
+ 'appear here after they have been installed at least once on this machine',
167
+ '(they are cached under ~/.bmad/cache/external-modules/).',
168
+ '',
169
+ 'For community or custom modules, read the module.yaml file in that',
170
+ "module's source repository directly.",
171
+ ].join('\n');
172
+ return { text, ok: false };
173
+ }
174
+ return { text: 'No modules found.', ok: false };
175
+ }
176
+
177
+ const sections = [];
178
+ // Track when a module-scoped lookup couldn't actually be rendered (yaml
179
+ // unparseable or empty after parse). The full `--list-options` output is
180
+ // tolerant of one bad entry, but `--list-options <module>` against a single
181
+ // unreadable module should still fail tooling so a CI script catches it.
182
+ let moduleScopedFailure = false;
183
+ sections.push('Available --set keys', 'Format: --set <module>.<key>=<value> (repeatable)', '');
184
+ for (const { code, yamlPath, source } of filtered) {
185
+ let parsed;
186
+ try {
187
+ parsed = yaml.parse(await fs.readFile(yamlPath, 'utf8'));
188
+ } catch {
189
+ sections.push(`${code} (${source}): could not parse module.yaml`, '');
190
+ if (moduleCode) moduleScopedFailure = true;
191
+ continue;
192
+ }
193
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
194
+ sections.push(`${code} (${source}): module.yaml is not a valid object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`, '');
195
+ if (moduleCode) moduleScopedFailure = true;
196
+ continue;
197
+ }
198
+ sections.push(formatModuleOptions(code, parsed, source));
199
+ }
200
+
201
+ if (!moduleCode) {
202
+ sections.push(
203
+ 'Community and custom modules are not listed here — read their module.yaml directly. Unknown keys still persist with a warning.',
204
+ );
205
+ }
206
+
207
+ return { text: sections.join('\n'), ok: !moduleScopedFailure };
208
+ }
209
+
210
+ module.exports = { formatOptionsList, discoverOfficialModuleYamls };
@@ -128,58 +128,102 @@ class CustomModuleManager {
128
128
  };
129
129
  }
130
130
 
131
- // HTTPS/HTTP URL: https://host/owner/repo[/tree/branch/subdir][.git]
132
- const httpsMatch = trimmed.match(/^(https?):\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
133
- if (httpsMatch) {
134
- const [, protocol, host, owner, repo, remainder] = httpsMatch;
135
- const cloneUrl = `${protocol}://${host}/${owner}/${repo}`;
136
- let subdir = null;
137
- let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
138
-
139
- if (remainder) {
140
- // Extract subdir from deep path patterns used by various Git hosts
131
+ // HTTPS/HTTP URL: generic handling for any Git host.
132
+ // We avoid host-specific parsing — `git clone` will accept whatever URL the
133
+ // user provides. We only need to (a) separate an optional browser-style
134
+ // subdir suffix from the clone URL, (b) extract any embedded ref
135
+ // (branch/tag) from deep-path URLs, and (c) derive a cache key / display
136
+ // name from the path. The original protocol (http or https) is preserved.
137
+ if (/^https?:\/\//i.test(trimmed)) {
138
+ let url;
139
+ try {
140
+ url = new URL(trimmed);
141
+ } catch {
142
+ url = null;
143
+ }
144
+
145
+ if (url && url.host) {
146
+ const host = url.host;
147
+ let repoPath = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
148
+ let subdir = null;
149
+ let urlRef = null; // branch/tag/commit extracted from deep-path URLs
150
+
151
+ // Detect browser-style deep-path patterns that embed a ref
152
+ // (branch/tag/commit) and optional subdirectory. These appear
153
+ // across many hosts:
154
+ // GitHub /<repo>/tree|blob/<ref>[/<subdir>]
155
+ // GitLab /<repo>/-/tree|blob/<ref>[/<subdir>]
156
+ // Gitea /<repo>/src/<ref>[/<subdir>]
157
+ // Gitea /<repo>/src/(branch|commit|tag)/<ref>[/<subdir>]
158
+ // Group 1 = repo path prefix, Group 2 = ref, Group 3 = subdir (optional).
141
159
  const deepPathPatterns = [
142
- { regex: /^\/(?:-\/)?tree\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // GitHub, GitLab
143
- { regex: /^\/(?:-\/)?blob\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 },
144
- { regex: /^\/src\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // Gitea/Forgejo
160
+ /^(.+?)\/(?:-\/)?(?:tree|blob)\/([^/]+)(?:\/(.+))?$/,
161
+ /^(.+?)\/src\/(?:branch\/|commit\/|tag\/)?([^/]+)(?:\/(.+))?$/,
145
162
  ];
146
- // Also match `/tree/<ref>` with no subdir
147
- const refOnlyPatterns = [/^\/(?:-\/)?tree\/([^/]+?)\/?$/, /^\/(?:-\/)?blob\/([^/]+?)\/?$/, /^\/src\/([^/]+?)\/?$/];
148
-
149
- for (const p of deepPathPatterns) {
150
- const match = remainder.match(p.regex);
163
+ for (const pattern of deepPathPatterns) {
164
+ const match = repoPath.match(pattern);
151
165
  if (match) {
152
- urlRef = match[p.refIdx];
153
- subdir = match[p.pathIdx].replace(/\/$/, '');
166
+ repoPath = match[1];
167
+ if (match[2]) urlRef = match[2];
168
+ if (match[3]) {
169
+ const cleaned = match[3].replace(/\/+$/, '');
170
+ if (cleaned) subdir = cleaned;
171
+ }
154
172
  break;
155
173
  }
156
174
  }
175
+
176
+ // Some hosts use ?path=/subdir on browse links to point at a file or
177
+ // directory. Honor it when no deep-path marker matched above.
157
178
  if (!subdir) {
158
- for (const r of refOnlyPatterns) {
159
- const match = remainder.match(r);
160
- if (match) {
161
- urlRef = match[1];
162
- break;
163
- }
179
+ const pathParam = url.searchParams.get('path');
180
+ if (pathParam) {
181
+ const cleaned = pathParam.replace(/^\/+/, '').replace(/\/+$/, '');
182
+ if (cleaned) subdir = cleaned;
164
183
  }
165
184
  }
166
- }
167
185
 
168
- // Precedence: explicit @version suffix > URL /tree/<ref> path segment.
169
- const version = versionSuffix || urlRef || null;
186
+ // Strip a single trailing .git for a stable cacheKey/displayName.
187
+ const repoPathClean = repoPath.replace(/\.git$/i, '');
188
+ if (!repoPathClean) {
189
+ return {
190
+ type: null,
191
+ cloneUrl: null,
192
+ subdir: null,
193
+ localPath: null,
194
+ cacheKey: null,
195
+ displayName: null,
196
+ isValid: false,
197
+ error: 'Not a valid Git URL or local path',
198
+ };
199
+ }
170
200
 
171
- return {
172
- type: 'url',
173
- cloneUrl,
174
- subdir,
175
- localPath: null,
176
- version,
177
- rawInput: trimmedRaw,
178
- cacheKey: `${host}/${owner}/${repo}`,
179
- displayName: `${owner}/${repo}`,
180
- isValid: true,
181
- error: null,
182
- };
201
+ const cloneUrl = `${url.protocol}//${host}/${repoPathClean}`;
202
+ const cacheKey = `${host}/${repoPathClean}`;
203
+
204
+ // Display name: prefer "<owner>/<repo>" using the last two meaningful
205
+ // path segments.
206
+ const segments = repoPathClean.split('/').filter(Boolean);
207
+ const repoSeg = segments.at(-1);
208
+ const ownerSeg = segments.at(-2);
209
+ const displayName = ownerSeg ? `${ownerSeg}/${repoSeg}` : repoSeg;
210
+
211
+ // Precedence: explicit @version suffix > URL /tree/<ref> path segment.
212
+ const version = versionSuffix || urlRef || null;
213
+
214
+ return {
215
+ type: 'url',
216
+ cloneUrl,
217
+ subdir,
218
+ localPath: null,
219
+ version,
220
+ rawInput: trimmedRaw,
221
+ cacheKey,
222
+ displayName,
223
+ isValid: true,
224
+ error: null,
225
+ };
226
+ }
183
227
  }
184
228
 
185
229
  return {
@@ -0,0 +1,330 @@
1
+ // `--set <module>.<key>=<value>` is a post-install patch. The installer runs
2
+ // its normal flow and writes `_bmad/config.toml`, `_bmad/config.user.toml`,
3
+ // and `_bmad/<module>/config.yaml`; afterwards `applySetOverrides` upserts
4
+ // each override into those files.
5
+ //
6
+ // This is intentionally NOT integrated with the prompt/template/schema
7
+ // system. Tradeoffs:
8
+ // - No `result:` template rendering: `--set bmm.project_knowledge=research`
9
+ // writes "research" verbatim. Pass `--set bmm.project_knowledge='{project-root}/research'`
10
+ // if you want the rendered form.
11
+ // - Carry-forward across installs is best-effort: declared schema keys
12
+ // persist via the existingValue path on the next interactive run; values
13
+ // for keys outside any module's schema may need to be re-passed on each
14
+ // install (or edited directly in `_bmad/config.toml`).
15
+ // - No "key not in schema" validation: whatever you assert, we write.
16
+ //
17
+ // Names that, when used as object keys, can mutate `Object.prototype` and
18
+ // cascade into every plain-object lookup in the process. The `--set` pipeline
19
+ // assigns into plain `{}` maps keyed by user input, so `--set __proto__.x=1`
20
+ // would otherwise reach `overrides.__proto__[x] = 1` and pollute every plain
21
+ // object. We reject the names at parse time and harden the maps in
22
+ // `parseSetEntries` with `Object.create(null)` for defense-in-depth.
23
+ const PROTOTYPE_POLLUTING_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
24
+
25
+ const path = require('node:path');
26
+ const fs = require('./fs-native');
27
+ const yaml = require('yaml');
28
+
29
+ /**
30
+ * Parse a single `--set <module>.<key>=<value>` entry.
31
+ * @param {string} entry - raw flag value
32
+ * @returns {{module: string, key: string, value: string}}
33
+ * @throws {Error} on malformed input
34
+ */
35
+ function parseSetEntry(entry) {
36
+ if (typeof entry !== 'string' || entry.length === 0) {
37
+ throw new Error('--set: empty entry. Expected <module>.<key>=<value>');
38
+ }
39
+ const eq = entry.indexOf('=');
40
+ if (eq === -1) {
41
+ throw new Error(`--set "${entry}": missing '='. Expected <module>.<key>=<value>`);
42
+ }
43
+ const lhs = entry.slice(0, eq);
44
+ // Note: only the LHS is trimmed. Values may legitimately contain leading
45
+ // or trailing whitespace (paths with spaces, quoted strings); module / key
46
+ // names cannot, so it's safe to be strict on the left.
47
+ const value = entry.slice(eq + 1);
48
+ const dot = lhs.indexOf('.');
49
+ if (dot === -1) {
50
+ throw new Error(`--set "${entry}": missing '.'. Expected <module>.<key>=<value>`);
51
+ }
52
+ const moduleCode = lhs.slice(0, dot).trim();
53
+ const key = lhs.slice(dot + 1).trim();
54
+ if (!moduleCode || !key) {
55
+ throw new Error(`--set "${entry}": empty module or key. Expected <module>.<key>=<value>`);
56
+ }
57
+ if (PROTOTYPE_POLLUTING_NAMES.has(moduleCode) || PROTOTYPE_POLLUTING_NAMES.has(key)) {
58
+ throw new Error(
59
+ `--set "${entry}": '__proto__', 'prototype', and 'constructor' are reserved and cannot be used as a module or key name.`,
60
+ );
61
+ }
62
+ return { module: moduleCode, key, value };
63
+ }
64
+
65
+ /**
66
+ * Parse repeated `--set` entries into a `{ module: { key: value } }` map.
67
+ * Later entries overwrite earlier ones for the same key. Both the outer
68
+ * map and the per-module inner maps are `Object.create(null)` so callers
69
+ * that bypass `parseSetEntry`'s name check still can't pollute prototypes.
70
+ *
71
+ * @param {string[]} entries
72
+ * @returns {Object<string, Object<string, string>>}
73
+ */
74
+ function parseSetEntries(entries) {
75
+ const overrides = Object.create(null);
76
+ if (!Array.isArray(entries)) return overrides;
77
+ for (const entry of entries) {
78
+ const { module: moduleCode, key, value } = parseSetEntry(entry);
79
+ if (!overrides[moduleCode]) overrides[moduleCode] = Object.create(null);
80
+ overrides[moduleCode][key] = value;
81
+ }
82
+ return overrides;
83
+ }
84
+
85
+ /**
86
+ * Encode a JS string as a TOML basic string (double-quoted with escapes).
87
+ * @param {string} value
88
+ */
89
+ function tomlString(value) {
90
+ const s = String(value);
91
+ // Per the TOML spec, basic strings escape `\`, `"`, and control characters.
92
+ return (
93
+ '"' +
94
+ s
95
+ .replaceAll('\\', '\\\\')
96
+ .replaceAll('"', String.raw`\"`)
97
+ .replaceAll('\b', String.raw`\b`)
98
+ .replaceAll('\f', String.raw`\f`)
99
+ .replaceAll('\n', String.raw`\n`)
100
+ .replaceAll('\r', String.raw`\r`)
101
+ .replaceAll('\t', String.raw`\t`) +
102
+ '"'
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Section header for a given module code.
108
+ * - `core` → `[core]`
109
+ * - `<other>` → `[modules.<other>]`
110
+ *
111
+ * Mirrors the layout `manifest-generator.writeCentralConfig` produces.
112
+ */
113
+ function sectionHeader(moduleCode) {
114
+ return moduleCode === 'core' ? '[core]' : `[modules.${moduleCode}]`;
115
+ }
116
+
117
+ /**
118
+ * Insert or update `key = value` inside a TOML section, returning the new
119
+ * file content. The format produced by the installer is regular and small
120
+ * enough that a line scanner is more reliable than pulling in a TOML
121
+ * round-tripper that would normalize the file's existing whitespace and
122
+ * comment structure.
123
+ *
124
+ * - If `[section]` exists and contains `key`, replace the value on that
125
+ * line (preserving any inline comment after the value).
126
+ * - If `[section]` exists but `key` doesn't, append `key = value` at the
127
+ * end of the section (before the next `[...]` header or EOF, skipping
128
+ * trailing blank lines so the section stays tidy).
129
+ * - If `[section]` doesn't exist, append a new section block at EOF.
130
+ *
131
+ * @param {string} content existing file content (may be empty)
132
+ * @param {string} section exact `[section]` header to target
133
+ * @param {string} key
134
+ * @param {string} valueToml already TOML-encoded value (e.g. `"foo"`)
135
+ * @returns {string} new content
136
+ */
137
+ function upsertTomlKey(content, section, key, valueToml) {
138
+ const lines = content.split('\n');
139
+ // Track whether the file already ended with a newline so we can preserve
140
+ // that. `split('\n')` on `"a\n"` yields `['a', '']`, which gives us the
141
+ // marker we need.
142
+ const hadTrailingNewline = lines.length > 0 && lines.at(-1) === '';
143
+ if (hadTrailingNewline) lines.pop();
144
+
145
+ // Locate the target section.
146
+ const sectionStart = lines.findIndex((line) => line.trim() === section);
147
+ if (sectionStart === -1) {
148
+ // Section doesn't exist — append a new block. Pad with a blank line if
149
+ // the file is non-empty so sections stay visually separated.
150
+ if (lines.length > 0 && lines.at(-1).trim() !== '') lines.push('');
151
+ lines.push(section, `${key} = ${valueToml}`);
152
+ return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
153
+ }
154
+
155
+ // Find the section's end (next `[...]` header or EOF).
156
+ let sectionEnd = lines.length;
157
+ for (let i = sectionStart + 1; i < lines.length; i++) {
158
+ if (/^\s*\[/.test(lines[i])) {
159
+ sectionEnd = i;
160
+ break;
161
+ }
162
+ }
163
+
164
+ // Look for the key inside the section. Match `<key> = ...` allowing
165
+ // optional leading whitespace; preserve the comment tail (`# ...`) if any.
166
+ const keyPattern = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*=\\s*(.*)$`);
167
+ for (let i = sectionStart + 1; i < sectionEnd; i++) {
168
+ const match = lines[i].match(keyPattern);
169
+ if (match) {
170
+ const indent = match[1];
171
+ // Preserve trailing comment if present. We split on the first `#` that
172
+ // is preceded by whitespace — TOML strings can't contain unescaped `#`
173
+ // in basic-string form so this is safe for the values we emit.
174
+ const tail = match[2];
175
+ const commentIdx = tail.search(/\s+#/);
176
+ const commentSuffix = commentIdx === -1 ? '' : tail.slice(commentIdx);
177
+ lines[i] = `${indent}${key} = ${valueToml}${commentSuffix}`;
178
+ return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
179
+ }
180
+ }
181
+
182
+ // Section exists but key doesn't. Insert before the next section header,
183
+ // skipping trailing blank lines inside the current section so the new
184
+ // entry sits with its siblings.
185
+ let insertAt = sectionEnd;
186
+ while (insertAt > sectionStart + 1 && lines[insertAt - 1].trim() === '') {
187
+ insertAt--;
188
+ }
189
+ lines.splice(insertAt, 0, `${key} = ${valueToml}`);
190
+ return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
191
+ }
192
+
193
+ function escapeRegExp(s) {
194
+ return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
195
+ }
196
+
197
+ /**
198
+ * Look up `[section] key` in a TOML file. Returns true if the file exists,
199
+ * the section is present, and `key` is set within it. Used by
200
+ * `applySetOverrides` to route an override to the file that already owns
201
+ * the key (so user-scope keys land in `config.user.toml`, team-scope keys
202
+ * land in `config.toml`).
203
+ */
204
+ async function tomlHasKey(filePath, section, key) {
205
+ if (!(await fs.pathExists(filePath))) return false;
206
+ const content = await fs.readFile(filePath, 'utf8');
207
+ const lines = content.split('\n');
208
+ const sectionStart = lines.findIndex((line) => line.trim() === section);
209
+ if (sectionStart === -1) return false;
210
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
211
+ for (let i = sectionStart + 1; i < lines.length; i++) {
212
+ if (/^\s*\[/.test(lines[i])) return false;
213
+ if (keyPattern.test(lines[i])) return true;
214
+ }
215
+ return false;
216
+ }
217
+
218
+ /**
219
+ * Apply parsed `--set` overrides to the central TOML files written by the
220
+ * installer. Called at the end of an install / quick-update.
221
+ *
222
+ * Routing per (module, key):
223
+ * 1. If `_bmad/config.user.toml` already has `[section] key`, update there
224
+ * (user-scope key like `core.user_name`, `bmm.user_skill_level`).
225
+ * 2. Otherwise update `_bmad/config.toml` (team scope, the default).
226
+ *
227
+ * The schema-correct user/team partition lives in `manifest-generator`. We
228
+ * intentionally don't re-read module schemas here — the only goal is to
229
+ * match the file the installer just wrote the key to. For brand-new keys
230
+ * (not in either file yet), team scope is the safe default.
231
+ *
232
+ * @param {Object<string, Object<string, string>>} overrides
233
+ * @param {string} bmadDir absolute path to `_bmad/`
234
+ * @returns {Promise<Array<{module:string,key:string,scope:'team'|'user',file:string}>>}
235
+ * a list of applied entries (for caller logging)
236
+ */
237
+ async function applySetOverrides(overrides, bmadDir) {
238
+ const applied = [];
239
+ if (!overrides || typeof overrides !== 'object') return applied;
240
+
241
+ const teamPath = path.join(bmadDir, 'config.toml');
242
+ const userPath = path.join(bmadDir, 'config.user.toml');
243
+
244
+ for (const moduleCode of Object.keys(overrides)) {
245
+ // Skip overrides for modules not actually installed. The installer writes
246
+ // `_bmad/<module>/config.yaml` for every installed module (including core),
247
+ // so its presence is a reliable "is this module here?" signal that works
248
+ // for both fresh installs and quick-updates without coupling to caller-
249
+ // supplied module lists.
250
+ const moduleConfigYaml = path.join(bmadDir, moduleCode, 'config.yaml');
251
+ if (!(await fs.pathExists(moduleConfigYaml))) {
252
+ continue;
253
+ }
254
+
255
+ const section = sectionHeader(moduleCode);
256
+ const moduleOverrides = overrides[moduleCode] || {};
257
+ for (const key of Object.keys(moduleOverrides)) {
258
+ const value = moduleOverrides[key];
259
+ const valueToml = tomlString(value);
260
+
261
+ const userOwnsIt = await tomlHasKey(userPath, section, key);
262
+ const targetPath = userOwnsIt ? userPath : teamPath;
263
+
264
+ // The team file always exists post-install; the user file only exists
265
+ // if the install wrote at least one user-scope key. If we're routing to
266
+ // it but it doesn't exist yet, create it with a minimal header so it
267
+ // has the same shape as installer-written user toml.
268
+ let content = '';
269
+ if (await fs.pathExists(targetPath)) {
270
+ content = await fs.readFile(targetPath, 'utf8');
271
+ } else {
272
+ content = '# Personal overrides for _bmad/config.toml.\n';
273
+ }
274
+
275
+ const next = upsertTomlKey(content, section, key, valueToml);
276
+ await fs.writeFile(targetPath, next, 'utf8');
277
+ applied.push({
278
+ module: moduleCode,
279
+ key,
280
+ scope: userOwnsIt ? 'user' : 'team',
281
+ file: path.basename(targetPath),
282
+ });
283
+ }
284
+
285
+ // Also patch the per-module yaml (`_bmad/<module>/config.yaml`). The
286
+ // installer reads this file as `_existingConfig` on subsequent runs and
287
+ // surfaces declared values as prompt defaults — under `--yes` those
288
+ // defaults are accepted, so patching here gives `--set` natural
289
+ // carry-forward for declared keys without needing schema-strict
290
+ // partition exemptions in the manifest writer. For undeclared keys the
291
+ // value lives in the per-module yaml but won't be re-emitted into
292
+ // config.toml on the next install (the schema-strict partition drops
293
+ // it); re-pass `--set` if you need it sticky.
294
+ const moduleYamlPath = path.join(bmadDir, moduleCode, 'config.yaml');
295
+ if (await fs.pathExists(moduleYamlPath)) {
296
+ try {
297
+ const text = await fs.readFile(moduleYamlPath, 'utf8');
298
+ const parsed = yaml.parse(text);
299
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
300
+ // Preserve the installer's banner header (everything up to the
301
+ // first non-comment line) so `_bmad/<module>/config.yaml` keeps
302
+ // its provenance comments after we round-trip it.
303
+ const headerLines = [];
304
+ for (const line of text.split('\n')) {
305
+ if (line.startsWith('#') || line.trim() === '') {
306
+ headerLines.push(line);
307
+ } else {
308
+ break;
309
+ }
310
+ }
311
+ for (const key of Object.keys(moduleOverrides)) {
312
+ parsed[key] = moduleOverrides[key];
313
+ }
314
+ const body = yaml.stringify(parsed, { indent: 2, lineWidth: 0, minContentWidth: 0 });
315
+ const header = headerLines.length > 0 ? headerLines.join('\n') + '\n' : '';
316
+ await fs.writeFile(moduleYamlPath, header + body, 'utf8');
317
+ }
318
+ } catch {
319
+ // Per-module yaml unparseable — skip silently. The central toml was
320
+ // already patched above, which is the user-visible state for the
321
+ // current install. Carry-forward will fail next install but the
322
+ // current install reflects the override.
323
+ }
324
+ }
325
+ }
326
+
327
+ return applied;
328
+ }
329
+
330
+ module.exports = { parseSetEntry, parseSetEntries, applySetOverrides, upsertTomlKey, tomlString };
@@ -16,6 +16,7 @@ const {
16
16
  } = require('./modules/channel-plan');
17
17
  const channelResolver = require('./modules/channel-resolver');
18
18
  const prompts = require('./prompts');
19
+ const { parseSetEntries } = require('./set-overrides');
19
20
 
20
21
  const manifest = new Manifest();
21
22
 
@@ -287,7 +288,7 @@ class UI {
287
288
  // Get tool selection
288
289
  const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
289
290
 
290
- const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
291
+ const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
291
292
  ...options,
292
293
  channelOptions,
293
294
  });
@@ -313,6 +314,7 @@ class UI {
313
314
  skipIde: toolSelection.skipIde,
314
315
  coreConfig: moduleConfigs.core || {},
315
316
  moduleConfigs: moduleConfigs,
317
+ setOverrides,
316
318
  skipPrompts: options.yes || false,
317
319
  channelOptions,
318
320
  };
@@ -364,7 +366,7 @@ class UI {
364
366
  await this._interactiveChannelGate({ options, channelOptions, selectedModules });
365
367
 
366
368
  let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
367
- const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
369
+ const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
368
370
  ...options,
369
371
  channelOptions,
370
372
  });
@@ -390,6 +392,7 @@ class UI {
390
392
  skipIde: toolSelection.skipIde,
391
393
  coreConfig: moduleConfigs.core || {},
392
394
  moduleConfigs: moduleConfigs,
395
+ setOverrides,
393
396
  skipPrompts: options.yes || false,
394
397
  channelOptions,
395
398
  };
@@ -709,6 +712,33 @@ class UI {
709
712
  */
710
713
  async collectModuleConfigs(directory, modules, options = {}) {
711
714
  const { OfficialModules } = require('./modules/official-modules');
715
+
716
+ // Parse --set up front purely to surface user-error before the install
717
+ // burns time on the network / filesystem. The actual application happens
718
+ // in installer.install() as a post-write TOML patch — see
719
+ // `tools/installer/set-overrides.js`. We also warn about overrides
720
+ // targeting modules the user didn't include, since those will silently
721
+ // miss the file the patch step looks for.
722
+ let setOverrides = {};
723
+ try {
724
+ setOverrides = parseSetEntries(options.set || []);
725
+ } catch (error) {
726
+ // install.js validated already; rethrow as-is for the user.
727
+ throw error;
728
+ }
729
+ // Drop overrides for modules that aren't in the install set so the
730
+ // post-install patch step doesn't create orphan sections in config.toml
731
+ // for modules that were never installed.
732
+ const selectedModuleSet = new Set(['core', ...modules]);
733
+ for (const moduleCode of Object.keys(setOverrides)) {
734
+ if (!selectedModuleSet.has(moduleCode)) {
735
+ await prompts.log.warn(
736
+ `--set ${moduleCode}.* — module '${moduleCode}' is not in the install set; values will be ignored. Add it to --modules to apply.`,
737
+ );
738
+ delete setOverrides[moduleCode];
739
+ }
740
+ }
741
+
712
742
  const configCollector = new OfficialModules({ channelOptions: options.channelOptions });
713
743
 
714
744
  // Seed core config from CLI options if provided
@@ -774,7 +804,7 @@ class UI {
774
804
  skipPrompts: options.yes || false,
775
805
  });
776
806
 
777
- return configCollector.collectedConfig;
807
+ return { moduleConfigs: configCollector.collectedConfig, setOverrides };
778
808
  }
779
809
 
780
810
  /**