bmad-plus 0.12.1 → 0.13.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 (176) hide show
  1. package/CHANGELOG.md +637 -580
  2. package/README.md +123 -120
  3. package/osint-agent-package/agents/osint-investigator.md +12 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  10. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  11. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  12. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  13. package/package.json +106 -91
  14. package/readme-international/README.de.md +625 -594
  15. package/readme-international/README.es.md +642 -611
  16. package/readme-international/README.fr.md +640 -609
  17. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +2 -0
  18. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  19. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  20. package/src/bmad-plus/module.yaml +333 -283
  21. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  22. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  23. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  24. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  25. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  83. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  84. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  87. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  88. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  89. package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
  90. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  91. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  92. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  93. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  94. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  95. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  96. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  97. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  99. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  100. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +303 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +409 -0
  106. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  107. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  108. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  109. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  110. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  111. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  112. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  135. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  136. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +1 -1
  137. package/tools/bmad-plus-npx.js +4 -2
  138. package/tools/build/adapters.config.js +60 -51
  139. package/tools/build/check-counts.js +626 -0
  140. package/tools/build/check-install-contract.js +298 -0
  141. package/tools/build/generate-adapters.js +252 -56
  142. package/tools/build/generate.js +187 -10
  143. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  144. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  145. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  146. package/tools/build/generated-adapters/AGENTS.md +20 -7
  147. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  148. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  149. package/tools/build/generated-adapters/GEMINI.md +20 -7
  150. package/tools/build/module.template.yaml +82 -0
  151. package/tools/cli/bmad-plus-cli.js +16 -1
  152. package/tools/cli/commands/autoconfig.js +470 -470
  153. package/tools/cli/commands/doctor.js +205 -233
  154. package/tools/cli/commands/install.js +543 -501
  155. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  156. package/tools/cli/commands/memory.js +195 -195
  157. package/tools/cli/commands/scan.js +348 -348
  158. package/tools/cli/commands/uninstall.js +209 -101
  159. package/tools/cli/commands/update-check.js +31 -0
  160. package/tools/cli/commands/update-policy.js +39 -0
  161. package/tools/cli/commands/update.js +123 -134
  162. package/tools/cli/i18n.js +905 -845
  163. package/tools/cli/lib/README-memory-journal.md +125 -125
  164. package/tools/cli/lib/ide-config.js +10 -259
  165. package/tools/cli/lib/install-manifest.js +17 -0
  166. package/tools/cli/lib/installed-adapters.js +89 -0
  167. package/tools/cli/lib/npm-runner.js +177 -0
  168. package/tools/cli/lib/pack-copy.js +62 -66
  169. package/tools/cli/lib/packs.js +437 -3
  170. package/tools/cli/lib/python-provision.js +508 -508
  171. package/tools/cli/lib/stack-detect.js +102 -102
  172. package/tools/cli/lib/update-check.js +153 -0
  173. package/tools/cli/lib/update-dispatch.js +182 -0
  174. package/tools/cli/lib/update-policy.js +90 -0
  175. package/tools/cli/lib/update-transaction.js +334 -0
  176. package/tools/cli/lib/validate.js +50 -50
@@ -0,0 +1,334 @@
1
+ /** Planned, locked framework updates with exact ownership and recoverable backups. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+ const semver = require('semver');
6
+ const { PACKS, DERIVED } = require('./packs');
7
+ const { listPackFiles, fileHash } = require('./pack-copy');
8
+ const { contentHash, classifyAdapter, INSTALLER_MARKER } = require('./installed-adapters');
9
+ const { generateUserFiles, IDE_CONFIGS, GENERATED_MARKER, USER_CONFIG_MARKER, safeTarget } = require('../../build/generate-adapters');
10
+ const { readInstallManifest } = require('./install-manifest');
11
+ const { isExactVersion } = require('./update-policy');
12
+
13
+ const MANIFEST = '_bmad/.bmad-plus-install.json';
14
+ const LOCK = '.bmad/update.lock';
15
+ const MODULE_FILES = ['_bmad/module.yaml', '_bmad/module-help.csv'];
16
+ const DEFAULT_PACKAGE_ROOT = path.resolve(__dirname, '../../..');
17
+ const relativeFile = (root, target) => path.relative(root, target).split(path.sep).join('/');
18
+ const currentBytes = (root, file) => {
19
+ const target = safeTarget(root, file);
20
+ return fs.existsSync(target) ? fs.readFileSync(target) : null;
21
+ };
22
+ const currentHash = (root, file) => {
23
+ const content = currentBytes(root, file);
24
+ return content === null ? null : fileHash(content);
25
+ };
26
+ const isManagedFile = file => typeof file === 'string' && !file.includes('\\') &&
27
+ path.posix.normalize(file) === file && (MODULE_FILES.includes(file) ||
28
+ file.startsWith('.agents/skills/') || file.startsWith('.agents/data/'));
29
+
30
+ function inventoryOf(manifest) {
31
+ const inventory = manifest.fileInventory;
32
+ if (!inventory || inventory.schemaVersion !== 1 || !inventory.files ||
33
+ typeof inventory.files !== 'object' || Array.isArray(inventory.files)) return null;
34
+ for (const [file, hash] of Object.entries(inventory.files)) {
35
+ if (!isManagedFile(file) || !/^[a-f0-9]{64}$/.test(hash)) return null;
36
+ }
37
+ return inventory.files;
38
+ }
39
+
40
+ function hasCompleteInventory(manifest, inventory) {
41
+ return Boolean(inventory && manifest.fileInventory.complete === true && MODULE_FILES.every(file => typeof inventory[file] === 'string'));
42
+ }
43
+
44
+ function collectManagedFiles({ projectDir, packageRoot = DEFAULT_PACKAGE_ROOT, packs }) {
45
+ const files = new Map();
46
+ const bmadSrc = path.join(packageRoot, 'src', 'bmad-plus');
47
+ for (const id of packs) {
48
+ if (!Object.hasOwn(PACKS, id)) throw new Error('Unknown installed pack: ' + id);
49
+ const entries = listPackFiles({ bmadSrc, projectRoot: packageRoot,
50
+ targetAgentsDir: path.join(projectDir, '.agents', 'skills'),
51
+ targetDataDir: path.join(projectDir, '.agents', 'data'), pack: PACKS[id] });
52
+ for (const entry of entries.files) files.set(relativeFile(projectDir, entry.target), entry.source);
53
+ }
54
+ for (const file of MODULE_FILES) files.set(file, path.join(bmadSrc, path.basename(file)));
55
+ return [...files].map(([file, source]) => {
56
+ safeTarget(packageRoot, relativeFile(packageRoot, source));
57
+ return { file, content: fs.readFileSync(source), kind: 'pack' };
58
+ });
59
+ }
60
+
61
+ function installedTools(projectDir, manifest) {
62
+ if (Array.isArray(manifest.ides)) return manifest.ides.filter(id => Object.hasOwn(IDE_CONFIGS, id));
63
+ const tools = Object.keys(IDE_CONFIGS).filter(id => fs.existsSync(safeTarget(projectDir, IDE_CONFIGS[id].configFile)));
64
+ const spine = currentBytes(projectDir, DERIVED.targets.spine)?.toString('utf8');
65
+ if (spine?.includes(INSTALLER_MARKER) && !spine.includes(GENERATED_MARKER) && !tools.includes('codex-cli')) tools.push('codex-cli');
66
+ return tools;
67
+ }
68
+
69
+ function evaluateUpdateReadiness({ projectDir, manifest }) {
70
+ try {
71
+ manifest ||= readInstallManifest(safeTarget(projectDir, MANIFEST));
72
+ const inventory = inventoryOf(manifest);
73
+ if (!hasCompleteInventory(manifest, inventory)) return { ready: false, legacy: true, conflicts: [], reason: 'Complete file ownership inventory is required.' };
74
+ const conflicts = [];
75
+ if (fs.existsSync(safeTarget(projectDir, LOCK))) conflicts.push({ file: LOCK, reason: 'update-in-progress' });
76
+ for (const [file, hash] of Object.entries(inventory)) {
77
+ const current = currentHash(projectDir, file);
78
+ if (current !== null && current !== hash) conflicts.push({ file, reason: 'modified-managed-file' });
79
+ }
80
+ const tools = installedTools(projectDir, manifest);
81
+ const expected = tools.length ? generateUserFiles(DERIVED, { packs: manifest.packs, tools }) : [];
82
+ for (const { file } of expected) {
83
+ const bytes = currentBytes(projectDir, file);
84
+ if (bytes !== null && (!bytes.toString('utf8').includes(USER_CONFIG_MARKER) ||
85
+ contentHash(bytes.toString('utf8')) !== manifest.adapterHashes?.[file])) {
86
+ conflicts.push({ file, reason: 'unowned-or-modified-adapter' });
87
+ }
88
+ }
89
+ return { ready: conflicts.length === 0, legacy: false, conflicts,
90
+ reason: conflicts.length ? 'Local changes or an active update require manual review.' : 'Managed files are unchanged.' };
91
+ } catch (error) {
92
+ return { ready: false, legacy: false, conflicts: [], reason: error.message };
93
+ }
94
+ }
95
+
96
+ /** Read-only plan: every destination is checked before the first mutation. */
97
+ function planUpdate({ projectDir, packageRoot = DEFAULT_PACKAGE_ROOT, manifest, version, auto = false }) {
98
+ projectDir = path.resolve(projectDir);
99
+ const snapshot = currentBytes(projectDir, MANIFEST);
100
+ const live = readInstallManifest(safeTarget(projectDir, MANIFEST));
101
+ if (manifest && JSON.stringify(live) !== JSON.stringify(manifest)) throw new Error('Installation manifest changed; check again.');
102
+ manifest = live;
103
+ if (!isExactVersion(version) || !isExactVersion(manifest.version)) throw new Error('Update versions must be strict semantic versions.');
104
+ if (semver.lt(version, manifest.version)) throw new Error('Downgrade refused: ' + manifest.version + ' -> ' + version);
105
+ const owned = inventoryOf(manifest);
106
+ let legacy = !hasCompleteInventory(manifest, owned);
107
+ const nextHashes = { ...(owned || {}) };
108
+ const nextAdapterHashes = { ...(manifest.adapterHashes || {}) };
109
+ const tools = installedTools(projectDir, manifest);
110
+ const expected = collectManagedFiles({ projectDir, packageRoot, packs: manifest.packs });
111
+ if (tools.length) expected.push(...generateUserFiles(DERIVED, {
112
+ packs: manifest.packs, tools, userName: manifest.user || 'Developer', language: manifest.language || 'English',
113
+ }).map(entry => ({ file: entry.file, content: Buffer.from(entry.content), kind: 'adapter' })));
114
+ const observed = new Map([[MANIFEST, fileHash(snapshot)]]);
115
+ const files = [];
116
+ const conflicts = [];
117
+ for (const [file, hash] of Object.entries(owned || {})) {
118
+ const current = currentHash(projectDir, file);
119
+ observed.set(file, current);
120
+ if (!expected.some(entry => entry.file === file) && current !== null && current !== hash) {
121
+ conflicts.push({ file, reason: 'modified-managed-file' });
122
+ }
123
+ }
124
+ for (const entry of expected) {
125
+ const before = currentBytes(projectDir, entry.file);
126
+ const beforeHash = before === null ? null : fileHash(before);
127
+ observed.set(entry.file, beforeHash);
128
+ const afterHash = fileHash(entry.content);
129
+ let backupLegacy = false;
130
+ if (entry.kind === 'adapter') {
131
+ const decision = classifyAdapter({ existing: before?.toString('utf8'), content: entry.content.toString('utf8'),
132
+ previousHash: manifest.adapterHashes?.[entry.file], update: true });
133
+ if (decision.action === 'preserve') {
134
+ conflicts.push({ file: entry.file, reason: decision.reason });
135
+ continue;
136
+ }
137
+ backupLegacy = decision.backup;
138
+ if (decision.legacy) legacy = true;
139
+ nextAdapterHashes[entry.file] = contentHash(entry.content.toString('utf8'));
140
+ } else {
141
+ if (before !== null && owned?.[entry.file] && beforeHash !== owned[entry.file]) {
142
+ conflicts.push({ file: entry.file, reason: 'modified-managed-file' });
143
+ continue;
144
+ }
145
+ if (before !== null && !owned?.[entry.file] && beforeHash !== afterHash) {
146
+ if (owned) {
147
+ conflicts.push({ file: entry.file, reason: 'unowned-file' });
148
+ continue;
149
+ }
150
+ legacy = true;
151
+ }
152
+ nextHashes[entry.file] = afterHash;
153
+ }
154
+ if (beforeHash !== afterHash) files.push({ ...entry, beforeHash, afterHash, backupLegacy });
155
+ }
156
+ const nextManifest = { ...manifest, version, lastUpdated: new Date().toISOString(), ides: tools,
157
+ adapterHashes: nextAdapterHashes, fileInventory: { schemaVersion: 1,
158
+ complete: !conflicts.some(entry => entry.reason === 'unowned-file'), files: nextHashes } };
159
+ const manifestContent = Buffer.from(JSON.stringify(nextManifest, null, 2));
160
+ files.push({ file: MANIFEST, kind: 'manifest', content: manifestContent,
161
+ beforeHash: fileHash(snapshot), afterHash: fileHash(manifestContent) });
162
+ for (const { file } of files) safeTarget(projectDir, file);
163
+ if (auto && (legacy || conflicts.length)) throw new Error('Automatic update refused: ' + (legacy ? 'legacy ownership inventory' : 'local file conflicts'));
164
+ return { projectDir, fromVersion: manifest.version, toVersion: version, files, conflicts, legacy,
165
+ inventoryComplete: !legacy, nextManifest, observed: [...observed], auto };
166
+ }
167
+
168
+ function acquireProjectLock(projectDir, { receiptId = null } = {}) {
169
+ const target = safeTarget(projectDir, LOCK);
170
+ fs.mkdirSync(path.dirname(target), { recursive: true });
171
+ const id = crypto.randomUUID();
172
+ let fd;
173
+ try { fd = fs.openSync(target, 'wx'); } catch (error) {
174
+ if (error.code === 'EEXIST') throw new Error('Another update is in progress: ' + target, { cause: error });
175
+ throw error;
176
+ }
177
+ try { fs.writeFileSync(fd, JSON.stringify({ id, pid: process.pid, receiptId })); } catch (error) {
178
+ fs.closeSync(fd);
179
+ fs.unlinkSync(target);
180
+ throw error;
181
+ }
182
+ fs.closeSync(fd);
183
+ return () => {
184
+ if (fs.existsSync(safeTarget(projectDir, LOCK)) && JSON.parse(fs.readFileSync(target, 'utf8')).id === id) fs.unlinkSync(target);
185
+ };
186
+ }
187
+
188
+ function atomicWrite(projectDir, file, content) {
189
+ const target = safeTarget(projectDir, file);
190
+ fs.mkdirSync(path.dirname(target), { recursive: true });
191
+ const temporaryFile = file + '.update-' + crypto.randomUUID() + '.tmp';
192
+ const temporary = safeTarget(projectDir, temporaryFile);
193
+ try {
194
+ fs.writeFileSync(temporary, content, { flag: 'wx' });
195
+ safeTarget(projectDir, file);
196
+ fs.renameSync(temporary, target);
197
+ } finally {
198
+ if (fs.existsSync(safeTarget(projectDir, temporaryFile))) fs.unlinkSync(temporary);
199
+ }
200
+ }
201
+
202
+ function rollback(projectDir, receipt) {
203
+ const failures = [];
204
+ // Restore the manifest last, after restoring the content it describes.
205
+ const entries = [...receipt.entries.filter(entry => entry.file !== MANIFEST).reverse(),
206
+ ...receipt.entries.filter(entry => entry.file === MANIFEST)];
207
+ for (const entry of entries) {
208
+ try {
209
+ const current = currentHash(projectDir, entry.file);
210
+ if (current === entry.beforeHash) continue;
211
+ if (current !== entry.afterHash && current !== null) throw new Error('File changed since the update; retained');
212
+ if (entry.beforeHash === null) {
213
+ if (current !== null) fs.unlinkSync(safeTarget(projectDir, entry.file));
214
+ } else {
215
+ if (entry.file === MANIFEST && failures.length) throw new Error('Content recovery is incomplete; manifest retained');
216
+ const backup = currentBytes(projectDir, entry.backup);
217
+ if (!backup || fileHash(backup) !== entry.beforeHash) throw new Error('Backup is missing or does not match its recorded hash');
218
+ atomicWrite(projectDir, entry.file, backup);
219
+ }
220
+ } catch (error) { failures.push({ file: entry.file, reason: error.message }); }
221
+ }
222
+ return failures;
223
+ }
224
+
225
+ function applyPlan(plan, { reauthorize } = {}) {
226
+ const { projectDir } = plan;
227
+ const id = crypto.randomUUID();
228
+ const release = acquireProjectLock(projectDir, { receiptId: id });
229
+ const receiptFile = '.bmad/updates/' + id + '/receipt.json';
230
+ let receipt;
231
+ try {
232
+ if (plan.auto) {
233
+ if (typeof reauthorize !== 'function') throw new Error('Automatic update requires policy reauthorization under the project lock.');
234
+ reauthorize();
235
+ }
236
+ for (const [file, hash] of plan.observed) {
237
+ if (currentHash(projectDir, file) !== hash) throw new Error('File changed after preflight: ' + file);
238
+ }
239
+ receipt = { schemaVersion: 1, id, status: 'prepared', fromVersion: plan.fromVersion, toVersion: plan.toVersion,
240
+ createdAt: new Date().toISOString(), conflicts: plan.conflicts, entries: [] };
241
+ for (const [index, entry] of plan.files.entries()) {
242
+ let backup = null;
243
+ if (entry.beforeHash !== null) {
244
+ backup = '.bmad/updates/' + id + '/backups/' + index;
245
+ const target = safeTarget(projectDir, backup);
246
+ fs.mkdirSync(path.dirname(target), { recursive: true });
247
+ fs.copyFileSync(safeTarget(projectDir, entry.file), target, fs.constants.COPYFILE_EXCL);
248
+ if (fileHash(fs.readFileSync(target)) !== entry.beforeHash) throw new Error('File changed while backing up: ' + entry.file);
249
+ }
250
+ if (entry.backupLegacy) {
251
+ let legacyBackup = entry.file + '.bak';
252
+ let index = 1;
253
+ while (fs.existsSync(safeTarget(projectDir, legacyBackup))) legacyBackup = entry.file + '.bak.' + index++;
254
+ fs.copyFileSync(safeTarget(projectDir, entry.file), safeTarget(projectDir, legacyBackup), fs.constants.COPYFILE_EXCL);
255
+ }
256
+ receipt.entries.push({ file: entry.file, beforeHash: entry.beforeHash, afterHash: entry.afterHash, backup });
257
+ }
258
+ atomicWrite(projectDir, receiptFile, JSON.stringify(receipt, null, 2));
259
+ for (const entry of plan.files) {
260
+ if (currentHash(projectDir, entry.file) !== entry.beforeHash) throw new Error('File changed during update: ' + entry.file);
261
+ atomicWrite(projectDir, entry.file, entry.content);
262
+ if (currentHash(projectDir, entry.file) !== entry.afterHash) throw new Error('Written file verification failed: ' + entry.file);
263
+ }
264
+ receipt.status = 'committed';
265
+ atomicWrite(projectDir, receiptFile, JSON.stringify(receipt, null, 2));
266
+ const manifest = readInstallManifest(safeTarget(projectDir, MANIFEST));
267
+ return { manifest, receiptPath: path.join(projectDir, receiptFile), receiptId: id,
268
+ changedFiles: plan.files.filter(entry => entry.kind !== 'manifest').map(entry => entry.file), conflicts: plan.conflicts };
269
+ } catch (error) {
270
+ if (receipt) {
271
+ const failures = rollback(projectDir, receipt);
272
+ receipt.status = failures.length ? 'recovery-required' : 'rolled-back';
273
+ receipt.failures = failures;
274
+ try { atomicWrite(projectDir, receiptFile, JSON.stringify(receipt, null, 2)); } catch (writeError) {
275
+ error.message += '; recovery receipt could not be saved: ' + writeError.message;
276
+ }
277
+ error.message += '; ' + receipt.status + '. Recovery: bmad-plus update --restore ' + id + ' --directory "' + projectDir + '" --yes';
278
+ }
279
+ throw error;
280
+ } finally { release(); }
281
+ }
282
+
283
+ function restoreUpdate({ projectDir, receiptId }) {
284
+ if (!/^[a-f0-9-]{36}$/.test(receiptId)) throw new Error('Invalid update receipt ID');
285
+ const receiptFile = '.bmad/updates/' + receiptId + '/receipt.json';
286
+ const receipt = JSON.parse(currentBytes(projectDir, receiptFile)?.toString('utf8') || 'null');
287
+ if (!receipt || receipt.schemaVersion !== 1 || receipt.id !== receiptId || !Array.isArray(receipt.entries)) throw new Error('Invalid update receipt');
288
+ const adapterPaths = new Set([DERIVED.targets.spine, ...DERIVED.targets.adapters.map(entry => entry.file)]);
289
+ for (const entry of receipt.entries) {
290
+ if (typeof entry.file !== 'string' || !(entry.file === MANIFEST || isManagedFile(entry.file) || adapterPaths.has(entry.file)) ||
291
+ !/^[a-f0-9]{64}$/.test(entry.afterHash) || (entry.beforeHash !== null && !/^[a-f0-9]{64}$/.test(entry.beforeHash))) throw new Error('Invalid recovery file entry');
292
+ safeTarget(projectDir, entry.file);
293
+ if (entry.beforeHash === null ? entry.backup !== null :
294
+ typeof entry.backup !== 'string' || !entry.backup.startsWith('.bmad/updates/' + receiptId + '/backups/') ||
295
+ path.posix.normalize(entry.backup) !== entry.backup || entry.backup.includes('\\')) throw new Error('Invalid backup entry');
296
+ if (entry.backup) safeTarget(projectDir, entry.backup);
297
+ }
298
+ const manifestEntry = receipt.entries.find(entry => entry.file === MANIFEST);
299
+ const installedHash = currentHash(projectDir, MANIFEST);
300
+ if (!manifestEntry || (installedHash !== manifestEntry.beforeHash && installedHash !== manifestEntry.afterHash)) throw new Error('Installation changed since this receipt; recovery refused.');
301
+ const lockPath = safeTarget(projectDir, LOCK);
302
+ if (fs.existsSync(lockPath)) {
303
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
304
+ if (lock.receiptId !== receiptId || !Number.isInteger(lock.pid) || lock.pid <= 0) throw new Error('Another update owns the project lock.');
305
+ try {
306
+ process.kill(lock.pid, 0);
307
+ throw new Error('The recorded update process is still running.');
308
+ } catch (error) {
309
+ if (error.code !== 'ESRCH') throw error;
310
+ }
311
+ fs.unlinkSync(lockPath);
312
+ }
313
+ const release = acquireProjectLock(projectDir);
314
+ try {
315
+ // Validate every required byte before starting a manual recovery.
316
+ for (const entry of receipt.entries) {
317
+ const current = currentHash(projectDir, entry.file);
318
+ if (current !== entry.beforeHash && current !== entry.afterHash && current !== null) throw new Error('File changed since the update: ' + entry.file);
319
+ if (entry.beforeHash !== null) {
320
+ const backup = currentBytes(projectDir, entry.backup);
321
+ if (backup === null || fileHash(backup) !== entry.beforeHash) throw new Error('Backup is missing or corrupt: ' + entry.file);
322
+ }
323
+ }
324
+ const failures = rollback(projectDir, receipt);
325
+ receipt.status = failures.length ? 'recovery-required' : 'restored';
326
+ receipt.failures = failures;
327
+ atomicWrite(projectDir, receiptFile, JSON.stringify(receipt, null, 2));
328
+ if (failures.length) throw new Error('Recovery incomplete: ' + failures.map(entry => entry.file + ': ' + entry.reason).join('; '));
329
+ return { receiptPath: path.join(projectDir, receiptFile), restoredVersion: readInstallManifest(safeTarget(projectDir, MANIFEST)).version };
330
+ } finally { release(); }
331
+ }
332
+
333
+ module.exports = { planUpdate, applyPlan, restoreUpdate, acquireProjectLock, evaluateUpdateReadiness,
334
+ collectManagedFiles, inventoryOf, MANIFEST, LOCK };
@@ -1,50 +1,50 @@
1
- /**
2
- * BMAD+ User Input Validation Utilities
3
- * Extracted from install.js for modularity and reuse.
4
- */
5
-
6
- // Shell/quote metacharacters that are dangerous in user-provided names.
7
- // Includes single/double quotes so a name cannot break out of a quoted context.
8
- const SHELL_META = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/;
9
- // Global variant used to strip EVERY occurrence during sanitization.
10
- // (A non-global regex in String.replace only removes the first match.)
11
- const SHELL_META_GLOBAL = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/g;
12
-
13
- /**
14
- * Validate and sanitize a user name.
15
- * Rules:
16
- * - Must not be empty
17
- * - Must not exceed 100 characters
18
- * - Must not contain shell metacharacters
19
- *
20
- * @param {string} rawName - Raw user-provided name
21
- * @param {string} fallback - Fallback name if validation fails
22
- * @returns {{ name: string, warnings: string[] }} Sanitized name and any warnings
23
- */
24
- function validateUserName(rawName, fallback) {
25
- const warnings = [];
26
-
27
- if (!rawName || rawName.trim().length === 0) {
28
- warnings.push('Name cannot be empty. Using default.');
29
- return { name: fallback, warnings };
30
- }
31
-
32
- if (rawName.length > 100) {
33
- warnings.push('Name too long (>100 chars). Truncating.');
34
- return { name: rawName.slice(0, 100), warnings };
35
- }
36
-
37
- if (SHELL_META.test(rawName)) {
38
- const sanitized = rawName.replace(SHELL_META_GLOBAL, '').trim() || 'Developer';
39
- warnings.push('Name contains shell metacharacters. Using sanitized version.');
40
- return { name: sanitized, warnings };
41
- }
42
-
43
- return { name: rawName, warnings };
44
- }
45
-
46
- module.exports = {
47
- validateUserName,
48
- SHELL_META,
49
- SHELL_META_GLOBAL,
50
- };
1
+ /**
2
+ * BMAD+ User Input Validation Utilities
3
+ * Extracted from install.js for modularity and reuse.
4
+ */
5
+
6
+ // Shell/quote metacharacters that are dangerous in user-provided names.
7
+ // Includes single/double quotes so a name cannot break out of a quoted context.
8
+ const SHELL_META = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/;
9
+ // Global variant used to strip EVERY occurrence during sanitization.
10
+ // (A non-global regex in String.replace only removes the first match.)
11
+ const SHELL_META_GLOBAL = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/g;
12
+
13
+ /**
14
+ * Validate and sanitize a user name.
15
+ * Rules:
16
+ * - Must not be empty
17
+ * - Must not exceed 100 characters
18
+ * - Must not contain shell metacharacters
19
+ *
20
+ * @param {string} rawName - Raw user-provided name
21
+ * @param {string} fallback - Fallback name if validation fails
22
+ * @returns {{ name: string, warnings: string[] }} Sanitized name and any warnings
23
+ */
24
+ function validateUserName(rawName, fallback) {
25
+ const warnings = [];
26
+
27
+ if (!rawName || rawName.trim().length === 0) {
28
+ warnings.push('Name cannot be empty. Using default.');
29
+ return { name: fallback, warnings };
30
+ }
31
+
32
+ if (rawName.length > 100) {
33
+ warnings.push('Name too long (>100 chars). Truncating.');
34
+ return { name: rawName.slice(0, 100), warnings };
35
+ }
36
+
37
+ if (SHELL_META.test(rawName)) {
38
+ const sanitized = rawName.replace(SHELL_META_GLOBAL, '').trim() || 'Developer';
39
+ warnings.push('Name contains shell metacharacters. Using sanitized version.');
40
+ return { name: sanitized, warnings };
41
+ }
42
+
43
+ return { name: rawName, warnings };
44
+ }
45
+
46
+ module.exports = {
47
+ validateUserName,
48
+ SHELL_META,
49
+ SHELL_META_GLOBAL,
50
+ };