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
@@ -0,0 +1,360 @@
1
+ /** Read-only by default; optional Python import probes never provision or repair. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const semver = require('semver');
5
+ const { DERIVED } = require('./packs');
6
+ const { readInstallManifest } = require('./install-manifest');
7
+ const { inventoryOf, collectManagedFiles, MANIFEST } = require('./update-transaction');
8
+ const { fileHash } = require('./pack-copy');
9
+ const { contentHash } = require('./installed-adapters');
10
+ const {
11
+ safeTarget,
12
+ GENERATED_MARKER,
13
+ USER_CONFIG_MARKER,
14
+ } = require('../../build/generate-adapters');
15
+
16
+ const PACKAGE_ROOT = path.resolve(__dirname, '../../..');
17
+
18
+ function inspectFile(root, file) {
19
+ try {
20
+ return { state: 'present', bytes: fs.readFileSync(safeTarget(root, file)) };
21
+ } catch (error) {
22
+ return { state: error.code === 'ENOENT' ? 'missing' : 'unreadable' };
23
+ }
24
+ }
25
+
26
+ function collectInstallationHealth({
27
+ projectDir = process.cwd(),
28
+ packageRoot = PACKAGE_ROOT,
29
+ cliVersion = require('../../../package.json').version,
30
+ nodeVersion = process.versions.node,
31
+ verifyPython = false,
32
+ pythonRunner,
33
+ } = {}) {
34
+ projectDir = path.resolve(projectDir);
35
+ const report = {
36
+ schemaVersion: 1,
37
+ projectDir,
38
+ status: 'ok',
39
+ versions: { installed: null, cli: cliVersion, published: null },
40
+ inventory: { status: 'unknown', trackedFiles: 0 },
41
+ packs: [],
42
+ integrations: [],
43
+ runtimes: [],
44
+ checks: [],
45
+ summary: { passed: 0, warnings: 0, errors: 0 },
46
+ };
47
+ function check(code, status, message, extra = {}) {
48
+ report.checks.push({ code, status, message, ...extra });
49
+ }
50
+ function finish() {
51
+ for (const entry of report.checks) {
52
+ report.summary[
53
+ entry.status === 'pass' ? 'passed' : entry.status === 'warning' ? 'warnings' : 'errors'
54
+ ]++;
55
+ }
56
+ report.status = report.summary.errors ? 'error' : report.summary.warnings ? 'warning' : 'ok';
57
+ return report;
58
+ }
59
+
60
+ const manifestFile = inspectFile(projectDir, MANIFEST);
61
+ if (manifestFile.state !== 'present') {
62
+ check(
63
+ manifestFile.state === 'missing' ? 'installation.missing' : 'manifest.unreadable',
64
+ 'error',
65
+ manifestFile.state === 'missing'
66
+ ? 'BMAD+ is not installed in this directory.'
67
+ : 'Installation manifest cannot be read safely.',
68
+ { path: MANIFEST }
69
+ );
70
+ return finish();
71
+ }
72
+ let manifest;
73
+ try {
74
+ manifest = readInstallManifest(safeTarget(projectDir, MANIFEST));
75
+ } catch (error) {
76
+ check('manifest.invalid', 'error', 'Invalid installation manifest: ' + error.message, {
77
+ path: MANIFEST,
78
+ });
79
+ return finish();
80
+ }
81
+ report.versions.installed = manifest.version;
82
+ check('manifest.valid', 'pass', 'Manifest found: v' + manifest.version);
83
+ check(
84
+ 'version.cli',
85
+ manifest.version === cliVersion ? 'pass' : 'warning',
86
+ manifest.version === cliVersion
87
+ ? 'Installed and executing CLI versions match.'
88
+ : 'Version mismatch: installed v' + manifest.version + ', executing CLI v' + cliVersion + '.'
89
+ );
90
+
91
+ const selectedPacks = [...new Set(manifest.packs)];
92
+ const validPacks = selectedPacks.filter((id) => Object.hasOwn(DERIVED.packs, id));
93
+ for (const id of selectedPacks) {
94
+ if (!validPacks.includes(id))
95
+ check('pack.unknown', 'error', 'Unknown installed pack: ' + id, { pack: id });
96
+ }
97
+ if (!selectedPacks.includes('core'))
98
+ check('pack.core-missing', 'error', 'The required Core pack is not selected.');
99
+
100
+ if (selectedPacks.includes('dev-studio')) {
101
+ try {
102
+ const studio = require('./studio');
103
+ const resolved = studio.validatePack(studio.installedPack(projectDir));
104
+ studio.resolveConfig(projectDir);
105
+ check(
106
+ 'studio.resources',
107
+ 'pass',
108
+ `Dev Studio: ${resolved.catalog.workflows.length} routes and ${resolved.resources.length} resources resolved; host execution is not verified.`
109
+ );
110
+ } catch (error) {
111
+ check(
112
+ 'studio.resources',
113
+ 'error',
114
+ 'Dev Studio cannot prepare its workflows: ' + error.message,
115
+ { path: '.agents/skills/pack-dev-studio' }
116
+ );
117
+ }
118
+ }
119
+
120
+ const inventory = inventoryOf(manifest);
121
+ const validInventory =
122
+ inventory &&
123
+ Object.values(inventory).every((hash) => typeof hash === 'string') &&
124
+ typeof manifest.fileInventory.complete === 'boolean';
125
+ report.inventory.status =
126
+ manifest.fileInventory === undefined
127
+ ? 'legacy'
128
+ : !validInventory
129
+ ? 'invalid'
130
+ : manifest.fileInventory.complete
131
+ ? 'complete'
132
+ : 'incomplete';
133
+ const owned = validInventory ? inventory : {};
134
+ report.inventory.trackedFiles = Object.keys(owned).length;
135
+
136
+ const expectedFiles = new Set(['_bmad/module.yaml', '_bmad/module-help.csv']);
137
+ for (const id of validPacks) {
138
+ const pack = DERIVED.packs[id];
139
+ report.packs.push({ id, runtimes: pack.runtimes, requiredResources: pack.requiredResources });
140
+ for (const file of pack.requiredResources) expectedFiles.add(file);
141
+ }
142
+ // Check delivery against the executing package, even if a manifest omits a file.
143
+ try {
144
+ for (const { file } of collectManagedFiles({ projectDir, packageRoot, packs: validPacks }))
145
+ expectedFiles.add(file);
146
+ } catch (error) {
147
+ check('package.unreadable', 'error', 'Cannot inspect the executing package: ' + error.message);
148
+ }
149
+ const resources = new Set([...expectedFiles, ...Object.keys(owned)]);
150
+ for (const file of [...resources].sort()) {
151
+ if (!Object.hasOwn(owned, file) && report.inventory.status === 'complete')
152
+ report.inventory.status = 'incomplete';
153
+ const observed = inspectFile(projectDir, file);
154
+ if (observed.state !== 'present') {
155
+ check(
156
+ 'file.' + observed.state,
157
+ 'error',
158
+ observed.state === 'missing'
159
+ ? 'Missing required file: ' + file
160
+ : 'File cannot be read safely: ' + file,
161
+ { path: file }
162
+ );
163
+ } else if (Object.hasOwn(owned, file)) {
164
+ const unchanged = fileHash(observed.bytes) === owned[file];
165
+ check(
166
+ unchanged ? 'file.unchanged' : 'file.modified',
167
+ unchanged ? 'pass' : 'warning',
168
+ unchanged
169
+ ? 'Managed file unchanged.'
170
+ : 'Managed file modified locally; review before updating.',
171
+ { path: file }
172
+ );
173
+ } else {
174
+ check('file.unowned', 'warning', 'File present without a recorded ownership hash.', {
175
+ path: file,
176
+ });
177
+ }
178
+ }
179
+ if (manifest.fileInventory?.complete && report.inventory.status === 'incomplete') {
180
+ check(
181
+ 'inventory.omissions',
182
+ 'warning',
183
+ 'Inventory claims completeness but omits expected files.'
184
+ );
185
+ }
186
+ check(
187
+ 'inventory.' + report.inventory.status,
188
+ report.inventory.status === 'complete'
189
+ ? 'pass'
190
+ : report.inventory.status === 'invalid'
191
+ ? 'error'
192
+ : 'warning',
193
+ 'File ownership inventory: ' + report.inventory.status + '.'
194
+ );
195
+ const config = inspectFile(projectDir, '_bmad/config.yaml');
196
+ check(
197
+ 'config.' + config.state,
198
+ config.state === 'present' ? 'pass' : 'error',
199
+ config.state === 'present'
200
+ ? 'Project configuration present.'
201
+ : 'Project configuration missing or unreadable.',
202
+ { path: '_bmad/config.yaml' }
203
+ );
204
+
205
+ const contract = DERIVED.diagnostics;
206
+ const declaredTools = Object.keys(contract.integrations);
207
+ let selectedTools = [];
208
+ if (manifest.ides === undefined) {
209
+ selectedTools = declaredTools.filter(
210
+ (tool) =>
211
+ inspectFile(projectDir, contract.integrations[tool].instructionFile).state !== 'missing'
212
+ );
213
+ check(
214
+ 'integrations.legacy',
215
+ 'warning',
216
+ 'IDE selection was not recorded; existing adapter paths were inspected.'
217
+ );
218
+ } else if (
219
+ !Array.isArray(manifest.ides) ||
220
+ manifest.ides.some((id) => typeof id !== 'string' || !declaredTools.includes(id))
221
+ ) {
222
+ check(
223
+ 'integrations.invalid',
224
+ 'error',
225
+ 'Manifest IDE selection contains invalid or unknown targets.'
226
+ );
227
+ } else selectedTools = [...new Set(manifest.ides)];
228
+
229
+ const adapterPaths =
230
+ selectedTools.length || manifest.ides === undefined
231
+ ? [
232
+ ...new Set([
233
+ DERIVED.targets.spine,
234
+ ...selectedTools.map((tool) => contract.integrations[tool].instructionFile),
235
+ ]),
236
+ ]
237
+ : [];
238
+ const ownership = new Map();
239
+ const hashes = manifest.adapterHashes;
240
+ const validHashes =
241
+ hashes === undefined ||
242
+ (hashes &&
243
+ typeof hashes === 'object' &&
244
+ !Array.isArray(hashes) &&
245
+ Object.entries(hashes).every(
246
+ ([file, hash]) =>
247
+ [DERIVED.targets.spine, ...DERIVED.targets.adapters.map((entry) => entry.file)].includes(
248
+ file
249
+ ) &&
250
+ typeof hash === 'string' &&
251
+ /^[a-f0-9]{64}$/.test(hash)
252
+ ));
253
+ if (!validHashes) check('adapters.invalid', 'error', 'Adapter ownership hashes are invalid.');
254
+ for (const file of adapterPaths) {
255
+ const observed = inspectFile(projectDir, file);
256
+ let state = observed.state;
257
+ if (state === 'present') {
258
+ const text = observed.bytes.toString('utf8');
259
+ const previous = validHashes && hashes && Object.hasOwn(hashes, file) ? hashes[file] : null;
260
+ state =
261
+ text.includes(GENERATED_MARKER) && !text.includes(USER_CONFIG_MARKER)
262
+ ? 'adopted'
263
+ : !previous || !text.includes(USER_CONFIG_MARKER)
264
+ ? 'unowned'
265
+ : contentHash(text) === previous
266
+ ? 'unchanged'
267
+ : 'modified';
268
+ }
269
+ ownership.set(file, state);
270
+ check(
271
+ 'adapter.' + state,
272
+ ['missing', 'unreadable'].includes(state)
273
+ ? 'error'
274
+ : state === 'unchanged'
275
+ ? 'pass'
276
+ : 'warning',
277
+ 'Instruction file: ' + state + '.',
278
+ { path: file }
279
+ );
280
+ }
281
+ report.integrations = selectedTools.map((tool) => ({
282
+ tool,
283
+ ...contract.integrations[tool],
284
+ ownership: ownership.get(contract.integrations[tool].instructionFile),
285
+ hostVerified: false,
286
+ }));
287
+
288
+ const nodeMinimum = contract.runtimeMinimums.node;
289
+ const nodeReady = Boolean(semver.valid(nodeVersion) && semver.gte(nodeVersion, nodeMinimum));
290
+ report.runtimes.push({
291
+ runtime: 'node',
292
+ minimum: nodeMinimum,
293
+ version: nodeVersion,
294
+ status: nodeReady ? 'verified' : 'unsupported',
295
+ });
296
+ check(
297
+ 'runtime.node',
298
+ nodeReady ? 'pass' : 'error',
299
+ 'Executing Node.js ' + nodeVersion + '; minimum ' + nodeMinimum + '.'
300
+ );
301
+ for (const id of validPacks.filter((pack) => DERIVED.packs[pack].runtimes.includes('python'))) {
302
+ const requirements = DERIVED.pythonPacks[id].requirements.join('/');
303
+ const delivery = inspectFile(packageRoot, requirements).state;
304
+ const environmentFile = '.bmad/venv/' + id + '/pyvenv.cfg';
305
+ const environment = inspectFile(projectDir, environmentFile).state;
306
+ const runtime = {
307
+ runtime: 'python',
308
+ pack: id,
309
+ minimum: contract.runtimeMinimums.python,
310
+ requirements,
311
+ packageRequirements: delivery,
312
+ environment,
313
+ imports: 'not-verified',
314
+ };
315
+ report.runtimes.push(runtime);
316
+ check(
317
+ 'runtime.requirements',
318
+ delivery === 'present' ? 'pass' : 'error',
319
+ delivery === 'present'
320
+ ? 'Python requirements are available in the executing package.'
321
+ : 'Python requirements are missing or unreadable in the executing package.',
322
+ { pack: id, path: requirements, scope: 'package' }
323
+ );
324
+ if (verifyPython) {
325
+ const { probePythonRuntime } = require('./python-health');
326
+ const probe = probePythonRuntime({
327
+ projectDir,
328
+ packId: id,
329
+ minimum: contract.runtimeMinimums.python,
330
+ verifyModules: DERIVED.pythonPacks[id].verifyModules,
331
+ runner: pythonRunner,
332
+ });
333
+ Object.assign(runtime, probe);
334
+ check(
335
+ 'runtime.python-probe',
336
+ probe.status === 'verified' ? 'pass' : 'warning',
337
+ probe.message +
338
+ (probe.status === 'verified'
339
+ ? ''
340
+ : ' Use bmad-plus install --provision-python to provision selected Python packs.'),
341
+ {
342
+ pack: id,
343
+ path: environmentFile,
344
+ }
345
+ );
346
+ } else {
347
+ check(
348
+ 'runtime.python-unverified',
349
+ 'warning',
350
+ 'Python environment ' +
351
+ environment +
352
+ '; interpreter version and imports have not been executed. Use doctor --verify-python to probe them.',
353
+ { pack: id, path: environmentFile }
354
+ );
355
+ }
356
+ }
357
+ return finish();
358
+ }
359
+
360
+ module.exports = { collectInstallationHealth };
@@ -0,0 +1,89 @@
1
+ /** Installed adapter ownership shared by install, update, and uninstall. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+ const clack = require('@clack/prompts');
6
+ const { GENERATED_MARKER, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
7
+
8
+ const INSTALLER_MARKER = 'BMAD+ \u2014 AI Agent Configuration';
9
+ const contentHash = content => crypto.createHash('sha256').update(String(content).replace(/\r\n/g, '\n')).digest('hex');
10
+
11
+ function classifyAdapter({ existing, content, previousHash, update = false, yes = false }) {
12
+ if (existing === undefined) return { action: 'write', backup: false };
13
+ const installed = existing.includes(USER_CONFIG_MARKER);
14
+ const adopted = existing.includes(GENERATED_MARKER) && !installed;
15
+ const legacy = existing.includes(INSTALLER_MARKER) && !existing.includes(GENERATED_MARKER);
16
+ if (installed && existing === content) return { action: 'same', backup: false };
17
+ const unchanged = installed && previousHash === contentHash(existing);
18
+ if (adopted || (installed && !unchanged) || (!installed && !legacy && (update || !yes))) {
19
+ return { action: 'preserve', backup: false, adopted, installed, reason: adopted ? 'repository-adopted' : 'local-instructions' };
20
+ }
21
+ return { action: 'write', backup: legacy || (!installed && yes), legacy };
22
+ }
23
+
24
+ // Refuse redirected parent directories as well as linked destination files.
25
+ function safeAdapterPath(projectDir, file) {
26
+ const root = path.resolve(projectDir);
27
+ const target = path.resolve(root, file);
28
+ const relative = path.relative(root, target);
29
+ if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
30
+ throw new Error('Adapter path outside project: ' + file);
31
+ }
32
+ let current = root;
33
+ for (const part of relative.split(path.sep)) {
34
+ current = path.join(current, part);
35
+ try {
36
+ if (fs.lstatSync(current).isSymbolicLink()) throw new Error('Adapter path is a symbolic link or junction: ' + current);
37
+ } catch (err) {
38
+ if (err.code !== 'ENOENT') throw err;
39
+ }
40
+ }
41
+ return target;
42
+ }
43
+
44
+ function writeIDEConfigs({ projectDir, files, yes = false, update = false, adapterHashes = {} }) {
45
+ const outcome = { written: [], skipped: [], backedUp: [], adapterHashes: {} };
46
+ // Preflight the complete set before writing any adapter.
47
+ for (const { file } of files) safeAdapterPath(projectDir, file);
48
+ for (const { file, content } of files) {
49
+ const target = safeAdapterPath(projectDir, file);
50
+ const previousHash = Object.hasOwn(adapterHashes, file) ? adapterHashes[file] : undefined;
51
+ let backup = false;
52
+ if (fs.existsSync(target)) {
53
+ const existing = fs.readFileSync(target, 'utf8');
54
+ const decision = classifyAdapter({ existing, content, previousHash, update, yes });
55
+ if (decision.action === 'same') {
56
+ outcome.adapterHashes[file] = contentHash(content);
57
+ continue;
58
+ }
59
+ if (decision.action === 'preserve') {
60
+ if (previousHash && decision.installed) outcome.adapterHashes[file] = previousHash;
61
+ outcome.skipped.push(file);
62
+ const compare = decision.adopted
63
+ ? ` Compare package defaults: node "${require.resolve('../../build/generate-adapters')}" --target "${path.resolve(projectDir)}" --check`
64
+ : '';
65
+ clack.log.warn(`${file}: existing project instructions preserved.${compare}`);
66
+ continue;
67
+ }
68
+ // Older installers did not record hashes. Keep their exact bytes before
69
+ // migration, including any local additions that cannot be distinguished.
70
+ backup = decision.backup;
71
+ }
72
+ if (backup) {
73
+ let suffix = '.bak';
74
+ let index = 1;
75
+ while (fs.existsSync(safeAdapterPath(projectDir, file + suffix))) suffix = `.bak.${index++}`;
76
+ fs.copyFileSync(target, safeAdapterPath(projectDir, file + suffix), fs.constants.COPYFILE_EXCL);
77
+ outcome.backedUp.push(file);
78
+ clack.log.warn(`${file}: backed up to ${file + suffix} before refreshing.`);
79
+ }
80
+ fs.mkdirSync(path.dirname(target), { recursive: true });
81
+ safeAdapterPath(projectDir, file);
82
+ fs.writeFileSync(target, content, 'utf8');
83
+ outcome.written.push(file);
84
+ outcome.adapterHashes[file] = contentHash(content);
85
+ }
86
+ return outcome;
87
+ }
88
+
89
+ module.exports = { INSTALLER_MARKER, contentHash, safeAdapterPath, writeIDEConfigs, classifyAdapter };
@@ -0,0 +1,177 @@
1
+ /** Run npm through Node with bounded output and no shell interpolation. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const childProcess = require('node:child_process');
5
+
6
+ function resolveNpmCli({
7
+ env = process.env,
8
+ execPath = process.execPath,
9
+ platform = process.platform,
10
+ realpath = fs.realpathSync,
11
+ isFile = (file) => fs.statSync(file).isFile(),
12
+ } = {}) {
13
+ const paths = platform === 'win32' ? path.win32 : path.posix;
14
+ const searchPath = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] || '';
15
+ const directories = searchPath.split(platform === 'win32' ? ';' : ':').filter(Boolean);
16
+ const nodeDir = paths.dirname(execPath);
17
+ const candidates = [
18
+ env.npm_execpath,
19
+ paths.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
20
+ paths.resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
21
+ ...directories.flatMap((dir) => [
22
+ paths.join(dir, 'npm'),
23
+ paths.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
24
+ ]),
25
+ ];
26
+ for (const candidate of candidates) {
27
+ if (!candidate) continue;
28
+ try {
29
+ const resolved = realpath(candidate);
30
+ if (paths.basename(resolved).toLowerCase() === 'npm-cli.js' && isFile(resolved))
31
+ return resolved;
32
+ } catch {
33
+ /* Try the next standard npm installation location. */
34
+ }
35
+ }
36
+ throw new Error('npm-cli.js was not found. Install Node.js with npm or repair its PATH.');
37
+ }
38
+
39
+ /** Injectable process boundary; tests never need to fetch a package. */
40
+ function createNpmRunner({
41
+ spawn = childProcess.spawn,
42
+ resolveCli = resolveNpmCli,
43
+ platform = process.platform,
44
+ execPath = process.execPath,
45
+ kill = process.kill.bind(process),
46
+ } = {}) {
47
+ return async function runNpm(
48
+ args,
49
+ { cwd, timeout = 6000, env = process.env, maxBuffer = 1024 * 1024, signal } = {}
50
+ ) {
51
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string' || arg.includes('\0'))) {
52
+ throw new Error('npm arguments must be an array of strings without NUL bytes.');
53
+ }
54
+ if (
55
+ !Number.isSafeInteger(timeout) ||
56
+ timeout <= 0 ||
57
+ !Number.isSafeInteger(maxBuffer) ||
58
+ maxBuffer <= 0
59
+ ) {
60
+ throw new Error('npm timeout and maxBuffer must be positive integers.');
61
+ }
62
+ if (signal?.aborted)
63
+ throw Object.assign(new Error('npm operation was cancelled.'), { code: 'ABORT_ERR' });
64
+ const cli = resolveCli({ env, execPath, platform });
65
+
66
+ return new Promise((resolve, reject) => {
67
+ let child;
68
+ try {
69
+ child = spawn(execPath, [cli, ...args], {
70
+ cwd,
71
+ env,
72
+ shell: false,
73
+ windowsHide: true,
74
+ detached: platform !== 'win32',
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ });
77
+ } catch (error) {
78
+ reject(new Error('Could not start npm.', { cause: error }));
79
+ return;
80
+ }
81
+ let settled = false;
82
+ let bytes = 0;
83
+ const stdout = [];
84
+ const stderr = [];
85
+ let timer;
86
+ const finish = (error, output) => {
87
+ if (settled) return;
88
+ settled = true;
89
+ clearTimeout(timer);
90
+ signal?.removeEventListener('abort', abort);
91
+ if (error) {
92
+ error.stdout = Buffer.concat(stdout).toString('utf8');
93
+ error.stderr = Buffer.concat(stderr).toString('utf8');
94
+ const diagnostic = [error.stdout.trim(), error.stderr.trim()].filter(Boolean).join('\n');
95
+ if (diagnostic) error.message += '\n' + diagnostic;
96
+ reject(error);
97
+ } else resolve(output);
98
+ };
99
+ const killChild = () => {
100
+ try {
101
+ child.kill('SIGKILL');
102
+ } catch {
103
+ /* The process may already have exited. */
104
+ }
105
+ };
106
+ const stop = () => {
107
+ if (!Number.isInteger(child.pid) || child.pid <= 0) return killChild();
108
+ if (platform !== 'win32') {
109
+ // A detached npm owns this process group, including its updater child.
110
+ try {
111
+ kill(-child.pid, 'SIGKILL');
112
+ } catch {
113
+ killChild();
114
+ }
115
+ return;
116
+ }
117
+ try {
118
+ const terminator = spawn('taskkill.exe', ['/pid', String(child.pid), '/t', '/f'], {
119
+ shell: false,
120
+ windowsHide: true,
121
+ stdio: 'ignore',
122
+ env,
123
+ });
124
+ terminator.once('error', killChild);
125
+ terminator.once('close', (code) => {
126
+ if (code !== 0) killChild();
127
+ });
128
+ } catch {
129
+ killChild();
130
+ }
131
+ };
132
+ const cancel = (message, code) => {
133
+ if (settled) return;
134
+ stop();
135
+ finish(Object.assign(new Error(message), { code }));
136
+ };
137
+ const abort = () =>
138
+ cancel('npm operation was cancelled; child termination requested.', 'ABORT_ERR');
139
+ const collect = (chunk, save) => {
140
+ if (settled) return;
141
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
142
+ bytes += buffer.length;
143
+ if (bytes > maxBuffer)
144
+ return cancel('npm output exceeded the configured buffer limit.', 'ENOBUFS');
145
+ (save ? stdout : stderr).push(buffer);
146
+ };
147
+ child.stdout.on('data', (chunk) => collect(chunk, true));
148
+ child.stderr.on('data', (chunk) => collect(chunk, false));
149
+ child.once('error', (error) => finish(new Error('Could not run npm.', { cause: error })));
150
+ child.once('close', (code, exitSignal) => {
151
+ if (code !== 0 || exitSignal) {
152
+ finish(
153
+ Object.assign(new Error(`npm exited unsuccessfully (${exitSignal || code}).`), {
154
+ code: 'NPM_EXIT',
155
+ exitCode: code,
156
+ signal: exitSignal,
157
+ })
158
+ );
159
+ } else finish(null, Buffer.concat(stdout).toString('utf8'));
160
+ });
161
+ timer = setTimeout(
162
+ () =>
163
+ cancel(
164
+ `npm exceeded its ${timeout} ms deadline; child termination requested.`,
165
+ 'ETIMEDOUT'
166
+ ),
167
+ timeout
168
+ );
169
+ timer.unref?.();
170
+ signal?.addEventListener('abort', abort, { once: true });
171
+ if (signal?.aborted) abort();
172
+ });
173
+ };
174
+ }
175
+
176
+ const runNpm = createNpmRunner();
177
+ module.exports = { runNpm, resolveNpmCli, createNpmRunner };