bmad-plus 0.12.2 → 0.14.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +96 -461
  3. package/package.json +12 -5
  4. package/readme-international/README.de.md +42 -9
  5. package/readme-international/README.es.md +43 -10
  6. package/readme-international/README.fr.md +42 -9
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  10. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  11. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  12. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  13. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  14. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  15. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  70. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  71. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  72. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  73. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  74. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  75. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  76. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  77. package/src/bmad-plus/packs/pack-memory/README.md +6 -0
  78. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
  79. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  80. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  81. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  82. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  83. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  84. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  85. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  86. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
  87. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
  88. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  89. package/tools/bmad-plus-npx.js +4 -2
  90. package/tools/build/adapters.config.js +60 -51
  91. package/tools/build/check-counts.js +52 -54
  92. package/tools/build/check-install-contract.js +450 -0
  93. package/tools/build/generate-adapters.js +252 -56
  94. package/tools/build/generate.js +364 -14
  95. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  96. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  97. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  98. package/tools/build/generated-adapters/AGENTS.md +20 -7
  99. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  100. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  101. package/tools/build/generated-adapters/GEMINI.md +20 -7
  102. package/tools/build/module.template.yaml +82 -0
  103. package/tools/build/pack-delivery.js +78 -0
  104. package/tools/cli/bmad-plus-cli.js +16 -1
  105. package/tools/cli/commands/doctor.js +49 -217
  106. package/tools/cli/commands/install.js +109 -164
  107. package/tools/cli/commands/studio.js +68 -0
  108. package/tools/cli/commands/uninstall.js +173 -65
  109. package/tools/cli/commands/update-check.js +31 -0
  110. package/tools/cli/commands/update-policy.js +39 -0
  111. package/tools/cli/commands/update.js +102 -113
  112. package/tools/cli/i18n.js +60 -0
  113. package/tools/cli/lib/ide-config.js +4 -261
  114. package/tools/cli/lib/install-manifest.js +17 -0
  115. package/tools/cli/lib/installation-health.js +360 -0
  116. package/tools/cli/lib/installed-adapters.js +89 -0
  117. package/tools/cli/lib/npm-runner.js +177 -0
  118. package/tools/cli/lib/pack-copy.js +62 -66
  119. package/tools/cli/lib/packs.js +546 -3
  120. package/tools/cli/lib/python-health.js +233 -0
  121. package/tools/cli/lib/python-provision.js +2 -2
  122. package/tools/cli/lib/studio.js +310 -0
  123. package/tools/cli/lib/update-check.js +153 -0
  124. package/tools/cli/lib/update-dispatch.js +182 -0
  125. package/tools/cli/lib/update-policy.js +90 -0
  126. package/tools/cli/lib/update-transaction.js +334 -0
  127. package/tools/release/publication-content.js +901 -0
@@ -1,233 +1,65 @@
1
- /**
2
- * BMAD+ Doctor Command
3
- * Checks installation integrity and reports issues
4
- *
5
- * Author: Laurent Rochetta
6
- */
7
-
8
- const path = require('node:path');
9
- const fs = require('node:fs');
1
+ /** Human and JSON output share one report; runtime execution is explicitly opt-in. */
10
2
  const clack = require('@clack/prompts');
11
3
  const pc = require('picocolors');
12
- const { t } = require('../i18n');
13
- const { PACKS, EXPECTED_AGENTS } = require('../lib/packs');
4
+ const { collectInstallationHealth } = require('../lib/installation-health');
14
5
 
15
6
  module.exports = {
16
7
  command: 'doctor',
17
- description: 'Check BMAD+ installation integrity',
8
+ description: 'Check BMAD+ installation integrity and declared capabilities',
18
9
  options: [
19
10
  ['-d, --directory <path>', 'Project directory (default: current directory)'],
20
11
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
12
+ ['--json', 'Print the diagnostic report as JSON (read-only unless a probe is requested)'],
13
+ ['--verify-python', 'Execute installed Python interpreters to verify versions and imports'],
21
14
  ],
22
- action: async (options) => {
23
- const projectDir = path.resolve(options.directory || process.cwd());
24
- const packageJson = require('../../../package.json');
25
-
26
- clack.intro(pc.bgBlue(pc.white(` BMAD+ Doctor v${packageJson.version} `)));
27
-
28
- let checks = 0;
29
- let passed = 0;
30
- let warnings = 0;
31
- let errors = 0;
32
-
33
- // ── Check 1: Installation manifest ──
34
- checks++;
35
- const manifestPath = path.join(projectDir, '_bmad', '.bmad-plus-install.json');
36
- if (!fs.existsSync(manifestPath)) {
37
- clack.log.error('❌ BMAD+ is not installed in this directory');
38
- clack.outro(pc.red('Run `npx bmad-plus install` first.'));
15
+ action: async (options = {}) => {
16
+ const report = collectInstallationHealth({
17
+ projectDir: options.directory,
18
+ verifyPython: Boolean(options.verifyPython),
19
+ });
20
+ process.exitCode = report.summary.errors ? 1 : 0;
21
+ if (options.json) {
22
+ console.log(JSON.stringify(report, null, 2));
39
23
  return;
40
24
  }
41
-
42
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
43
- const lang = options.lang || manifest.uiLanguage || 'en';
44
- clack.log.success(`✅ Manifest found — v${manifest.version}`);
45
- passed++;
46
-
47
- // ── Check 2: Version comparison ──
48
- checks++;
49
- if (manifest.version === packageJson.version) {
50
- clack.log.success(`✅ Version up to date (v${manifest.version})`);
51
- passed++;
52
- } else {
53
- clack.log.warn(`⚠️ Version mismatch: installed v${manifest.version} latest v${packageJson.version}`);
54
- clack.log.info(' Run `npx bmad-plus update` to upgrade');
55
- warnings++;
25
+ clack.intro(pc.bgBlue(pc.white(' BMAD+ Doctor v' + report.versions.cli + ' ')));
26
+ for (const entry of report.checks.filter(
27
+ (item) => item.status !== 'pass' || item.code === 'manifest.valid'
28
+ )) {
29
+ const log =
30
+ entry.status === 'error'
31
+ ? clack.log.error
32
+ : entry.status === 'warning'
33
+ ? clack.log.warn
34
+ : clack.log.success;
35
+ log(
36
+ entry.message +
37
+ (entry.path && !entry.message.includes(entry.path) ? ' (' + entry.path + ')' : '')
38
+ );
56
39
  }
57
-
58
- // ── Check 3: Agent files ──
59
- checks++;
60
- const agentsDir = path.join(projectDir, '.agents', 'skills');
61
- if (fs.existsSync(agentsDir)) {
62
- const agentDirs = fs.readdirSync(agentsDir, { withFileTypes: true })
63
- .filter(d => d.isDirectory())
64
- .map(d => d.name);
65
- clack.log.success(`✅ ${agentDirs.length} agent/skill directories found`);
66
- passed++;
67
-
68
- // Check each expected agent from manifest packs using shared EXPECTED_AGENTS module
69
- for (const pack of (manifest.packs || ['core'])) {
70
- const entry = EXPECTED_AGENTS[pack];
71
- if (!entry) continue;
72
-
73
- // Check individual agent directories
74
- for (const agent of (entry.agents || [])) {
75
- checks++;
76
- const agentPath = path.join(agentsDir, agent);
77
- if (fs.existsSync(agentPath)) {
78
- passed++;
79
- } else {
80
- clack.log.warn(`⚠️ Missing agent: ${agent} (pack: ${pack})`);
81
- warnings++;
82
- }
83
- }
84
-
85
- // Check pack directory if applicable
86
- if (entry.packDir) {
87
- checks++;
88
- const packPath = path.join(agentsDir, entry.packDir);
89
- if (fs.existsSync(packPath)) {
90
- passed++;
91
-
92
- // These packs bundle their agents as files inside packDir — verify
93
- // them there (not as loose directories, which would falsely warn).
94
- for (const agentFile of (entry.packAgents || [])) {
95
- checks++;
96
- if (fs.existsSync(path.join(packPath, agentFile))) {
97
- passed++;
98
- } else {
99
- clack.log.warn(`⚠️ Missing agent: ${agentFile} (pack: ${pack})`);
100
- warnings++;
101
- }
102
- }
103
- } else {
104
- clack.log.warn(`⚠️ Missing pack directory: ${entry.packDir} (pack: ${pack})`);
105
- warnings++;
106
- }
107
- }
108
- }
109
- } else {
110
- clack.log.error('❌ No .agents/skills/ directory found');
111
- errors++;
112
- }
113
-
114
- // ── Check 4: Config files ──
115
- checks++;
116
- const configPath = path.join(projectDir, '_bmad', 'config.yaml');
117
- if (fs.existsSync(configPath)) {
118
- clack.log.success('✅ config.yaml present');
119
- passed++;
120
- } else {
121
- clack.log.error('❌ config.yaml missing');
122
- errors++;
123
- }
124
-
125
- // ── Check 5: Module config ──
126
- checks++;
127
- const moduleYaml = path.join(projectDir, '_bmad', 'module.yaml');
128
- if (fs.existsSync(moduleYaml)) {
129
- clack.log.success('✅ module.yaml present');
130
- passed++;
131
- } else {
132
- clack.log.warn('⚠️ module.yaml missing');
133
- warnings++;
40
+ if (report.versions.installed) {
41
+ clack.note(
42
+ [
43
+ report.summary.passed + '/' + report.checks.length + ' checks passed',
44
+ report.summary.errors + ' error(s), ' + report.summary.warnings + ' warning(s)',
45
+ 'Installed: v' + report.versions.installed + ' | Executing CLI: v' + report.versions.cli,
46
+ 'Ownership inventory: ' + report.inventory.status,
47
+ 'Integrations use host-managed execution; host capabilities were not probed.',
48
+ ].join('\n'),
49
+ 'BMAD+ Health Report'
50
+ );
134
51
  }
135
-
136
- // ── Check 6: IDE configs ──
137
- checks++;
138
- const ideFiles = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md'].filter(f =>
139
- fs.existsSync(path.join(projectDir, f))
52
+ clack.log.info(
53
+ 'Published version is not checked here. Use `bmad-plus update-check --json` to check npm.'
54
+ );
55
+ clack.outro(
56
+ report.status === 'ok'
57
+ ? pc.green('Installation checks passed.')
58
+ : pc.yellow(
59
+ options.verifyPython
60
+ ? 'Review the reported evidence. Python import probes ran; no provisioning was requested.'
61
+ : 'Review the reported evidence before changing files. Doctor did not modify the project.'
62
+ )
140
63
  );
141
- if (ideFiles.length > 0) {
142
- clack.log.success(`✅ ${ideFiles.length} IDE config(s): ${ideFiles.join(', ')}`);
143
- passed++;
144
- } else {
145
- clack.log.warn('⚠️ No IDE config files found');
146
- warnings++;
147
- }
148
-
149
- // ── Check 7: Output directories ──
150
- checks++;
151
- const outputDir = path.join(projectDir, '_bmad-output');
152
- if (fs.existsSync(outputDir)) {
153
- clack.log.success('✅ _bmad-output/ directory exists');
154
- passed++;
155
- } else {
156
- clack.log.warn('⚠️ _bmad-output/ not found');
157
- warnings++;
158
- }
159
-
160
- // ── Check 8: Skills integrity (SKILL.md exists in pack dirs) ──
161
- if (manifest.packs && manifest.packs.includes('seo')) {
162
- checks++;
163
- const seoSkill = path.join(agentsDir, 'pack-seo', 'SKILL.md');
164
- if (fs.existsSync(seoSkill)) {
165
- clack.log.success('✅ SEO Engine SKILL.md present');
166
- passed++;
167
- } else {
168
- clack.log.error('❌ SEO Engine SKILL.md missing — /seo audit will not work');
169
- errors++;
170
- }
171
- }
172
-
173
- // ── Check 9: PACKS ↔ module.yaml sync ──
174
- checks++;
175
- try {
176
- const yaml = require('js-yaml');
177
- const moduleYamlSrc = path.join(__dirname, '..', '..', '..', 'src', 'bmad-plus', 'module.yaml');
178
- if (fs.existsSync(moduleYamlSrc)) {
179
- const moduleContent = yaml.load(fs.readFileSync(moduleYamlSrc, 'utf8'));
180
- const modulePackIds = Object.keys(moduleContent.packs || {});
181
-
182
- // Use shared PACKS module instead of fragile regex on install.js source
183
- const cliPackIds = Object.keys(PACKS);
184
-
185
- // Find mismatches
186
- const missingInCLI = modulePackIds.filter(p => !cliPackIds.includes(p));
187
- const missingInModule = cliPackIds.filter(p => !modulePackIds.includes(p));
188
-
189
- if (missingInCLI.length === 0 && missingInModule.length === 0) {
190
- clack.log.success(`✅ PACKS ↔ module.yaml in sync (${modulePackIds.length} packs)`);
191
- passed++;
192
- } else {
193
- if (missingInCLI.length > 0) {
194
- clack.log.warn(`⚠️ Packs in module.yaml but missing from CLI PACKS: ${missingInCLI.join(', ')}`);
195
- }
196
- if (missingInModule.length > 0) {
197
- clack.log.warn(`⚠️ Packs in CLI PACKS but missing from module.yaml: ${missingInModule.join(', ')}`);
198
- }
199
- warnings++;
200
- }
201
- } else {
202
- clack.log.info(pc.dim('ℹ️ PACKS↔module.yaml check skipped (source not available in npx context)'));
203
- passed++; // Not a failure — just unavailable
204
- }
205
- } catch (e) {
206
- clack.log.info(pc.dim(`ℹ️ PACKS↔module.yaml check skipped: ${e.message}`));
207
- passed++; // Graceful skip
208
- }
209
-
210
- // ── Summary ──
211
- const summaryColor = errors > 0 ? pc.red : warnings > 0 ? pc.yellow : pc.green;
212
- const summaryIcon = errors > 0 ? '❌' : warnings > 0 ? '⚠️' : '✅';
213
-
214
- clack.note([
215
- `${summaryIcon} ${passed}/${checks} checks passed`,
216
- errors > 0 ? `❌ ${errors} error(s)` : null,
217
- warnings > 0 ? `⚠️ ${warnings} warning(s)` : null,
218
- '',
219
- `📦 Version: v${manifest.version}`,
220
- `📅 Installed: ${manifest.installed.split('T')[0]}`,
221
- `🗣️ Language: ${manifest.language || 'N/A'}`,
222
- `📦 Packs: ${(manifest.packs || ['core']).join(', ')}`,
223
- ].filter(Boolean).join('\n'), '🩺 BMAD+ Health Report');
224
-
225
- clack.outro(summaryColor(
226
- errors > 0
227
- ? 'Issues found — run `npx bmad-plus install` to fix'
228
- : warnings > 0
229
- ? 'Minor issues found — consider running `npx bmad-plus update`'
230
- : 'Everything looks great! 🎉'
231
- ));
232
64
  },
233
65
  };
@@ -12,59 +12,18 @@ const fsExtra = require('fs-extra');
12
12
  const clack = require('@clack/prompts');
13
13
  const pc = require('picocolors');
14
14
  const { t, getLanguageOptions, getCommLanguageOptions } = require('../i18n');
15
- const { PACKS } = require('../lib/packs');
16
- const { copyPackFiles } = require('../lib/pack-copy');
15
+ const { PACKS, DERIVED } = require('../lib/packs');
16
+ const { copyPackFiles, copyManagedFile } = require('../lib/pack-copy');
17
+ const { acquireProjectLock, inventoryOf, collectManagedFiles } = require('../lib/update-transaction');
17
18
  const { initMemory } = require('../lib/memory-init');
18
- const { generateIDEConfig, IDE_CONFIGS } = require('../lib/ide-config');
19
+ const { generateUserFiles, IDE_CONFIGS } = require('../../build/generate-adapters');
20
+ const { writeIDEConfigs, safeAdapterPath } = require('../lib/installed-adapters');
21
+ const { readInstallManifest } = require('../lib/install-manifest');
22
+ const yaml = require('js-yaml');
19
23
  const { validateUserName } = require('../lib/validate');
20
24
  const { provisionPack } = require('../lib/python-provision');
21
25
 
22
- // Packs whose runtime includes Python (registry.yaml: runtime includes "python").
23
- // Provisioned only when the user opts in with --provision-python, so the default
24
- // install (and the test suite) never spawns Python. Kills SEO "broken on arrival".
25
- // Paths are resolved relative to the PACKAGE root (projectRoot below), so they
26
- // must live under a directory shipped in package.json "files". The SEO toolkit
27
- // is embarked in-pack (src/bmad-plus/packs/pack-seo) precisely so npm consumers
28
- // receive the scripts + requirements the SKILL.md commands them to run.
29
- const PYTHON_PACKS = {
30
- seo: {
31
- requirements: ['src', 'bmad-plus', 'packs', 'pack-seo', 'requirements.txt'],
32
- verifyModules: ['requests', 'bs4', 'defusedxml', 'lxml'],
33
- },
34
- memory: {
35
- // NOTE: mcp-server is not in package.json "files" → this still warn-skips for
36
- // npm consumers (memory RAG provisioning is a separate, pre-existing gap).
37
- requirements: ['mcp-server', 'requirements.txt'],
38
- verifyModules: ['chromadb'],
39
- },
40
- };
41
-
42
- // Pack definitions are imported from the shared module: require('../lib/packs').PACKS
43
-
44
- // ── Root-config ownership markers (PROD-01) ──
45
- //
46
- // install writes CLAUDE.md / GEMINI.md / AGENTS.md, but TWO producers own those
47
- // same paths, each stamping its own marker:
48
- // - lib/ide-config.js → INSTALLER_MARKER (this installer's template)
49
- // - tools/build/generate-adapters.js → GENERATED_MARKER (registry-generated spine)
50
- // Recognizing only the first is what let `install --yes` clobber a generated spine.
51
- const INSTALLER_MARKER = 'BMAD+ — AI Agent Configuration';
52
- const GENERATOR_MARKER_FALLBACK = 'AUTO-GENERATED by tools/build/generate-adapters.js';
53
-
54
- /**
55
- * The marker the adapter generator stamps on every root file it owns.
56
- * Read from the generator's own export so the two cannot drift apart — a
57
- * hand-typed copy is how INSTALLER_MARKER drifted from the spine in the first
58
- * place. The fallback only covers trees published without the build tooling;
59
- * tests/unit/install-spine.test.js pins both values to their real producer.
60
- */
61
- function resolveGeneratorMarker() {
62
- try {
63
- return require('../../build/generate-adapters').GENERATED_MARKER || GENERATOR_MARKER_FALLBACK;
64
- } catch {
65
- return GENERATOR_MARKER_FALLBACK;
66
- }
67
- }
26
+ const PYTHON_PACKS = DERIVED.pythonPacks;
68
27
 
69
28
  module.exports = {
70
29
  command: 'install',
@@ -74,12 +33,34 @@ module.exports = {
74
33
  ['-p, --packs <packs>', 'Comma-separated pack IDs: core,osint,all (default: interactive)'],
75
34
  ['-y, --yes', 'Accept all defaults, skip prompts'],
76
35
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it (overrides auto-detection)'],
77
- ['--tools <tools>', 'Comma-separated IDE IDs (default: auto-detect)'],
78
- ['--provision-python', 'Provision an isolated Python env for packs that need one (seo, memory)'],
36
+ ['--tools <tools>', 'Comma-separated IDE IDs, all, or none (default: auto-detect)'],
37
+ ['--mode <mode>', 'Execution mode: manual, autopilot, hybrid (default: manual)'],
38
+ ['--provision-python', 'Provision isolated environments for selected Python packs'],
79
39
  ],
80
40
  action: async (options) => {
41
+ // A closed/piped stdin cannot answer prompts. Require explicit defaults
42
+ // before creating files, instead of silently exiting halfway through UI.
43
+ if (!options.yes && !process.stdin.isTTY) {
44
+ const i = t(options.lang || 'en');
45
+ clack.log.error(i.noninteractive_requires_yes('install'));
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+
81
50
  const projectDir = path.resolve(options.directory || process.cwd());
82
51
  const bmadSrc = path.join(__dirname, '..', '..', '..', 'src', 'bmad-plus');
52
+ const previousManifestPath = safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json');
53
+ const previousManifestBytes = fs.existsSync(previousManifestPath) ? fs.readFileSync(previousManifestPath) : null;
54
+ let previousManifest = null;
55
+ let manifestError;
56
+ if (fs.existsSync(previousManifestPath)) {
57
+ try {
58
+ previousManifest = readInstallManifest(previousManifestPath);
59
+ } catch (error) {
60
+ manifestError = error;
61
+ }
62
+ }
63
+ if (options.mode && !['manual', 'autopilot', 'hybrid'].includes(options.mode)) throw new Error('Unknown execution mode: ' + options.mode);
83
64
 
84
65
  // ── Step 0: Language Selection ──
85
66
  const pkgJson = require('../../../package.json');
@@ -151,6 +132,8 @@ module.exports = {
151
132
  if (options.tools) {
152
133
  if (options.tools === 'none' || options.tools === 'skip') {
153
134
  detectedIDEs = [];
135
+ } else if (options.tools === 'all') {
136
+ detectedIDEs = Object.keys(IDE_CONFIGS);
154
137
  } else {
155
138
  detectedIDEs = options.tools.split(',').map(t => t.trim()).filter(t => IDE_CONFIGS[t]);
156
139
  }
@@ -196,6 +179,7 @@ module.exports = {
196
179
  // ── Step 3: User Config ──
197
180
  let userName = process.env.USER || process.env.USERNAME || 'Developer';
198
181
  let commLang = 'French';
182
+ let execMode = options.mode || 'manual';
199
183
 
200
184
  if (!options.yes) {
201
185
  const userConfig = await clack.group({
@@ -230,6 +214,37 @@ module.exports = {
230
214
  for (const w of warnings) clack.log.warn(w);
231
215
  userName = validatedName;
232
216
  commLang = userConfig.commLang;
217
+ execMode = options.mode || userConfig.execMode;
218
+ }
219
+
220
+ const releaseLock = acquireProjectLock(projectDir);
221
+ try {
222
+ const { safeTarget } = require('../../build/generate-adapters');
223
+ const plannedFiles = collectManagedFiles({ projectDir, packs: selectedPacks });
224
+ if (detectedIDEs.length) plannedFiles.push(...generateUserFiles(DERIVED, {
225
+ packs: selectedPacks, tools: detectedIDEs, userName, language: commLang,
226
+ }));
227
+ for (const { file } of plannedFiles) safeTarget(projectDir, file);
228
+ safeTarget(projectDir, '_bmad/config.yaml');
229
+ safeTarget(projectDir, '_bmad/.bmad-plus-install.json');
230
+ for (const directory of ['.agents/skills', '.agents/data', '_bmad', '_bmad-output/discovery', '_bmad-output/build', 'docs']) {
231
+ safeTarget(projectDir, directory + '/.bmad-preflight');
232
+ }
233
+ const currentManifestBytes = fs.existsSync(previousManifestPath) ? fs.readFileSync(previousManifestPath) : null;
234
+ if (previousManifestBytes === null ? currentManifestBytes !== null :
235
+ currentManifestBytes === null || !previousManifestBytes.equals(currentManifestBytes)) {
236
+ throw new Error('Installation changed while preparing install. Please retry.');
237
+ }
238
+ // Preserve the unreadable manifest before repair; do not trust its ownership
239
+ // hashes. The adapter writer will retain locally edited user instructions.
240
+ if (manifestError) {
241
+ const backupBase = '_bmad/.bmad-plus-install.json.corrupt.bak';
242
+ let backupFile = backupBase;
243
+ let index = 1;
244
+ while (fs.existsSync(safeAdapterPath(projectDir, backupFile))) backupFile = `${backupBase}.${index++}`;
245
+ const backupPath = safeAdapterPath(projectDir, backupFile);
246
+ fs.copyFileSync(safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json'), backupPath, fs.constants.COPYFILE_EXCL);
247
+ clack.log.warn(`${i.manifest_invalid(manifestError.message)}\nBackup: ${backupPath}`);
233
248
  }
234
249
 
235
250
  // ── Step 4: Install Files ──
@@ -248,6 +263,13 @@ module.exports = {
248
263
  let copiedAgents = 0;
249
264
  let copiedSkills = 0;
250
265
  let copiedFiles = 0;
266
+ const previousInventory = inventoryOf(previousManifest || {}) || {};
267
+ const fileInventory = { ...previousInventory };
268
+ let inventoryComplete = true;
269
+ const onFileConflict = file => {
270
+ if (!previousInventory[file]) inventoryComplete = false;
271
+ clack.log.warn(`${file}: locally changed or unowned content preserved.`);
272
+ };
251
273
 
252
274
  const projectRoot = path.join(bmadSrc, '..', '..');
253
275
 
@@ -261,6 +283,7 @@ module.exports = {
261
283
  targetDataDir,
262
284
  projectRoot,
263
285
  pack,
286
+ projectDir, inventory: fileInventory, previousInventory, onConflict: onFileConflict,
264
287
  });
265
288
  copiedAgents += result.copiedAgents;
266
289
  copiedSkills += result.copiedSkills;
@@ -311,35 +334,39 @@ module.exports = {
311
334
  // Copy module config
312
335
  const moduleYaml = path.join(bmadSrc, 'module.yaml');
313
336
  if (fs.existsSync(moduleYaml)) {
314
- fsExtra.copySync(moduleYaml, path.join(targetBmadDir, 'module.yaml'));
337
+ copyManagedFile({ source: moduleYaml, target: path.join(targetBmadDir, 'module.yaml'),
338
+ projectDir, inventory: fileInventory, previousInventory, onConflict: onFileConflict });
315
339
  copiedFiles++;
316
340
  }
317
341
 
318
342
  const helpCsv = path.join(bmadSrc, 'module-help.csv');
319
343
  if (fs.existsSync(helpCsv)) {
320
- fsExtra.copySync(helpCsv, path.join(targetBmadDir, 'module-help.csv'));
344
+ copyManagedFile({ source: helpCsv, target: path.join(targetBmadDir, 'module-help.csv'),
345
+ projectDir, inventory: fileInventory, previousInventory, onConflict: onFileConflict });
321
346
  copiedFiles++;
322
347
  }
323
348
 
324
349
  spinner.stop(i.installed_summary(copiedAgents, copiedSkills, copiedFiles));
325
350
 
326
351
  // ── Step 5: Generate IDE Configs ──
352
+ let adapterHashes = previousManifest?.adapterHashes || {};
327
353
  if (detectedIDEs.length > 0) {
328
354
  const ideSpinner = clack.spinner();
329
355
  ideSpinner.start(i.configuring_ides);
330
356
 
331
- writeIDEConfigs({
357
+ const outcome = writeIDEConfigs({
332
358
  projectDir,
333
- detectedIDEs,
334
- configContent: generateIDEConfig(userName, commLang, selectedPacks),
359
+ files: generateUserFiles(DERIVED, { packs: selectedPacks, userName, language: commLang, tools: detectedIDEs }),
360
+ adapterHashes,
335
361
  yes: options.yes,
336
362
  });
363
+ adapterHashes = { ...adapterHashes, ...outcome.adapterHashes };
337
364
 
338
365
  ideSpinner.stop(i.ide_configured(detectedIDEs.length));
339
366
  }
340
367
 
341
368
  // ── Step 6: Create config.yaml ──
342
- const configYaml = generateConfigYaml(userName, commLang, projectDir);
369
+ const configYaml = generateConfigYaml(userName, commLang, projectDir, execMode);
343
370
  const configPath = path.join(targetBmadDir, 'config.yaml');
344
371
  fs.writeFileSync(configPath, configYaml, 'utf8');
345
372
 
@@ -357,6 +384,9 @@ module.exports = {
357
384
  installed: new Date().toISOString(),
358
385
  packs: selectedPacks,
359
386
  ides: detectedIDEs,
387
+ adapterHashes,
388
+ fileInventory: { schemaVersion: 1, complete: inventoryComplete, files: fileInventory },
389
+ executionMode: execMode,
360
390
  user: userName,
361
391
  language: commLang,
362
392
  };
@@ -484,115 +514,30 @@ module.exports = {
484
514
  clack.note(agentGuide.join('\n'), i.guide_title);
485
515
 
486
516
  clack.outro(pc.green(i.guide_ready));
517
+ } finally {
518
+ releaseLock();
519
+ }
487
520
  },
488
521
  // Exported for functional testing (not part of the public API)
489
- _internal: {
490
- INSTALLER_MARKER,
491
- GENERATOR_MARKER_FALLBACK,
492
- resolveGeneratorMarker,
493
- writeIDEConfigs,
494
- },
522
+ _internal: { writeIDEConfigs, generateConfigYaml },
495
523
  };
496
524
 
497
-
498
- // ── Helpers ──
499
-
500
- /**
501
- * Write the IDE config files, never destroying one this installer does not own.
502
- *
503
- * An existing config falls into one of three classes, tested in this order:
504
- *
505
- * 1. GENERATOR-MANAGED (carries the adapter generator's marker) — SKIPPED, even
506
- * under --yes. Such a file is a pure function of registry.yaml +
507
- * tools/build/adapters.config.js, and the installer template is a strict
508
- * subset of it (no registry facts), so overwriting is a downgrade that also
509
- * breaks the `generate-adapters.js --check` drift gate. A .bak does not
510
- * rescue it: every tool reads the live path, not the backup. The repair is
511
- * regeneration, not merging so install defers and points at the generator.
512
- * 2. HAND-AUTHORED (no BMAD+ marker) — kept as-is interactively; backed up then
513
- * overwritten under --yes (NODE-02).
514
- * 3. INSTALLER-GENERATED (carries INSTALLER_MARKER) refreshed in place.
515
- *
516
- * @param {object} args
517
- * @param {string} args.projectDir - Absolute path to the target project
518
- * @param {string[]} args.detectedIDEs - IDE ids to configure (keys of IDE_CONFIGS)
519
- * @param {string} args.configContent - Rendered config body to write
520
- * @param {boolean} [args.yes] - Non-interactive mode (--yes)
521
- * @returns {{written: string[], skipped: string[], backedUp: string[]}} config file names per outcome
522
- */
523
- function writeIDEConfigs({ projectDir, detectedIDEs, configContent, yes }) {
524
- const generatorMarker = resolveGeneratorMarker();
525
- // Several IDEs share one file (gemini-cli + antigravity → GEMINI.md): dedupe
526
- // outcomes by file name so a shared file is reported — and warned about — once.
527
- const outcome = { written: new Set(), skipped: new Set(), backedUp: new Set() };
528
-
529
- for (const ideId of detectedIDEs) {
530
- const ide = IDE_CONFIGS[ideId];
531
- if (!ide) continue;
532
-
533
- const configPath = path.join(projectDir, ide.configFile);
534
-
535
- if (fs.existsSync(configPath)) {
536
- const existing = fs.readFileSync(configPath, 'utf8');
537
-
538
- // PROD-01: the adapter generator owns this file — defer to it.
539
- if (existing.includes(generatorMarker)) {
540
- if (!outcome.skipped.has(ide.configFile)) {
541
- clack.log.warn(
542
- `⚠️ ${ide.configFile} is generated by tools/build/generate-adapters.js — left untouched. ` +
543
- `Run \`node tools/build/generate-adapters.js --adopt\` to refresh it from registry.yaml.`
544
- );
545
- }
546
- outcome.skipped.add(ide.configFile);
547
- continue;
548
- }
549
-
550
- // NODE-02: never clobber a hand-authored config without protecting it.
551
- if (!existing.includes(INSTALLER_MARKER)) {
552
- if (!yes) {
553
- // Interactive: keep the user's file untouched.
554
- if (!outcome.skipped.has(ide.configFile)) {
555
- clack.log.warn(`⚠️ ${ide.configFile} already exists and was not created by BMAD+ — kept your file (skipped). Re-run with --yes to back it up and overwrite.`);
556
- }
557
- outcome.skipped.add(ide.configFile);
558
- continue;
559
- }
560
- // Non-interactive: back up before overwriting, never destroy data.
561
- if (!outcome.backedUp.has(ide.configFile)) {
562
- let backupPath = `${configPath}.bak`;
563
- if (fs.existsSync(backupPath)) backupPath = `${configPath}.${Date.now()}.bak`;
564
- fs.copyFileSync(configPath, backupPath);
565
- clack.log.warn(`⚠️ Backed up existing ${ide.configFile} → ${path.basename(backupPath)}`);
566
- outcome.backedUp.add(ide.configFile);
567
- }
568
- }
569
- }
570
-
571
- fs.writeFileSync(configPath, configContent, 'utf8');
572
- outcome.written.add(ide.configFile);
573
- }
574
-
575
- return {
576
- written: [...outcome.written],
577
- skipped: [...outcome.skipped],
578
- backedUp: [...outcome.backedUp],
579
- };
580
- }
581
-
582
- function generateConfigYaml(userName, language, projectDir) {
583
- const projectName = path.basename(projectDir);
584
- return `# BMAD+ Project Configuration
585
- # Generated by bmad-plus install
586
-
587
- user_name: "${userName}"
588
- communication_language: "${language}"
589
- document_output_language: "${language}"
590
- output_folder: "_bmad-output"
591
- project_name: "${projectName}"
592
-
593
- # Execution settings
594
- execution_mode: "manual"
595
- auto_role_activation: true
596
- parallel_execution: true
597
- `;
525
+ function generateConfigYaml(userName, language, projectDir, execMode = 'manual') {
526
+ if (!['manual', 'autopilot', 'hybrid'].includes(execMode)) throw new Error('Unknown execution mode: ' + execMode);
527
+ return '# BMAD+ Project Configuration\n# Generated by bmad-plus install\n\n' + yaml.dump({
528
+ user_name: userName,
529
+ communication_language: language,
530
+ document_output_language: language,
531
+ output_folder: '_bmad-output',
532
+ project_name: path.basename(projectDir),
533
+ execution_mode: execMode,
534
+ auto_role_activation: true,
535
+ parallel_execution: true,
536
+ checkpoints: {
537
+ discovery: 'require_approval',
538
+ architecture: 'require_approval',
539
+ story: execMode === 'autopilot' ? 'notify_only' : 'require_approval',
540
+ delivery: 'require_approval',
541
+ },
542
+ }, { lineWidth: -1, forceQuotes: true, quotingType: '"' });
598
543
  }