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,153 @@
1
+ /** Bounded release discovery. This module never downloads or applies a package. */
2
+ const fs = require('node:fs');
3
+ const semver = require('semver');
4
+ const { safeAdapterPath } = require('./installed-adapters');
5
+ const { readInstallManifest } = require('./install-manifest');
6
+ const { readUpdatePolicy, validatePolicy, authorizeTarget, isExactVersion, writeProjectJson } = require('./update-policy');
7
+
8
+ const CACHE_FILE = '.bmad/update-check.json';
9
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
10
+ const FAILURE_BACKOFF_MS = 60 * 60 * 1000;
11
+
12
+ function validateRelease(value) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !isExactVersion(value.version)) {
14
+ throw new Error('Registry returned an invalid exact version.');
15
+ }
16
+ const engines = value.engines === undefined ? {} : value.engines;
17
+ if (!engines || typeof engines !== 'object' || Array.isArray(engines) ||
18
+ (engines.node !== undefined && (typeof engines.node !== 'string' ||
19
+ !engines.node.trim() || !semver.validRange(engines.node)))) {
20
+ throw new Error('Registry returned an invalid Node engine requirement.');
21
+ }
22
+ return { version: value.version, engines: engines.node ? { node: engines.node } : {} };
23
+ }
24
+
25
+ async function queryNpmRelease({ projectDir, channel, registry }) {
26
+ validatePolicy({ channel, registry });
27
+ const { runNpm } = require('./npm-runner');
28
+ const stdout = await runNpm([
29
+ 'view', `bmad-plus@${channel}`, 'version', 'engines', '--json', `--registry=${registry}`,
30
+ '--prefer-online', '--fetch-timeout=4000', '--fetch-retries=0',
31
+ ], { cwd: projectDir, timeout: 6000, maxBuffer: 64 * 1024 });
32
+ return validateRelease(JSON.parse(stdout));
33
+ }
34
+
35
+ function timestamp(value, now) {
36
+ const time = typeof value === 'string' ? Date.parse(value) : NaN;
37
+ return Number.isFinite(time) && time <= now ? time : null;
38
+ }
39
+
40
+ function readCache(projectDir, policy, now) {
41
+ try {
42
+ const cache = JSON.parse(fs.readFileSync(safeAdapterPath(projectDir, CACHE_FILE), 'utf8'));
43
+ if (!cache || cache.schemaVersion !== 1 || cache.package !== 'bmad-plus' ||
44
+ cache.registry !== policy.registry || cache.channel !== policy.channel) return null;
45
+ if (cache.release) {
46
+ cache.release = validateRelease(cache.release);
47
+ if (timestamp(cache.checkedAt, now) === null) return null;
48
+ }
49
+ if (cache.failedAt !== undefined && timestamp(cache.failedAt, now) === null) return null;
50
+ return cache;
51
+ } catch { return null; }
52
+ }
53
+
54
+ function saveCache(projectDir, cache) {
55
+ try { writeProjectJson(projectDir, CACHE_FILE, cache); } catch { /* A read-only cache is nonfatal. */ }
56
+ }
57
+
58
+ async function checkForUpdate({
59
+ projectDir, runningVersion = require('../../../package.json').version, refresh = false, offline = false,
60
+ now = Date.now, queryRelease = queryNpmRelease, checkReadiness, nodeVersion = process.version,
61
+ }) {
62
+ const time = typeof now === 'function' ? now() : now;
63
+ const result = {
64
+ schemaVersion: 1, installedVersion: null, runningVersion, latestVersion: null,
65
+ targetVersion: null, channel: null, status: 'unknown', checkedAt: null,
66
+ source: 'none', stale: false, updateAvailable: null, releaseEligible: false,
67
+ versionEligible: false, canAutoApply: false, engineCompatible: null, engines: {}, reason: null,
68
+ };
69
+ if (!isExactVersion(runningVersion) || !Number.isFinite(time) ||
70
+ !Number.isFinite(new Date(time).getTime()) || !semver.valid(nodeVersion)) {
71
+ return { ...result, status: 'invalid-runtime', reason: 'invalid-running-version-or-clock' };
72
+ }
73
+ try {
74
+ const manifest = readInstallManifest(safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json'));
75
+ if (!isExactVersion(manifest.version)) throw new Error('Invalid installed version');
76
+ result.installedVersion = manifest.version;
77
+ } catch {
78
+ return { ...result, status: 'invalid-installation', reason: 'missing-or-invalid-installation-manifest' };
79
+ }
80
+ let policy;
81
+ try { policy = readUpdatePolicy(projectDir); } catch {
82
+ return { ...result, status: 'invalid-policy', reason: 'unreadable-or-invalid-update-policy' };
83
+ }
84
+ result.channel = policy.channel;
85
+ if (policy.mode === 'off') return { ...result, status: 'off', reason: 'updates-disabled' };
86
+
87
+ let cache = readCache(projectDir, policy, time);
88
+ const cachedAt = cache?.release ? timestamp(cache.checkedAt, time) : null;
89
+ const failedAt = cache?.failedAt === undefined ? null : timestamp(cache.failedAt, time);
90
+ let release;
91
+ if (offline) {
92
+ result.reason = 'offline';
93
+ } else if (!refresh && failedAt !== null && time - failedAt < FAILURE_BACKOFF_MS) {
94
+ result.reason = 'registry-retry-backoff';
95
+ } else if (!refresh && cachedAt !== null && failedAt === null && time - cachedAt < CACHE_TTL_MS) {
96
+ release = cache.release;
97
+ result.source = 'cache';
98
+ result.checkedAt = cache.checkedAt;
99
+ } else {
100
+ try {
101
+ release = validateRelease(await queryRelease({ projectDir, channel: policy.channel, registry: policy.registry }));
102
+ cache = { schemaVersion: 1, package: 'bmad-plus', registry: policy.registry, channel: policy.channel,
103
+ release, checkedAt: new Date(time).toISOString() };
104
+ saveCache(projectDir, cache);
105
+ result.source = 'registry';
106
+ result.checkedAt = cache.checkedAt;
107
+ } catch {
108
+ result.reason = 'registry-unavailable';
109
+ saveCache(projectDir, { ...cache, schemaVersion: 1, package: 'bmad-plus',
110
+ registry: policy.registry, channel: policy.channel, failedAt: new Date(time).toISOString() });
111
+ }
112
+ }
113
+ if (!release) {
114
+ if (cache?.release) {
115
+ result.targetVersion = cache.release.version;
116
+ result.latestVersion = policy.channel === 'latest' ? cache.release.version : null;
117
+ result.engines = cache.release.engines;
118
+ result.checkedAt = cache.checkedAt;
119
+ result.source = 'stale-cache';
120
+ result.stale = true;
121
+ }
122
+ return result;
123
+ }
124
+
125
+ result.targetVersion = release.version;
126
+ result.latestVersion = policy.channel === 'latest' ? release.version : null;
127
+ result.engines = release.engines;
128
+ result.engineCompatible = !release.engines.node || semver.satisfies(nodeVersion, release.engines.node);
129
+ const comparison = semver.compare(release.version, result.installedVersion);
130
+ result.updateAvailable = comparison > 0;
131
+ result.status = comparison > 0 ? 'update-available' : comparison === 0 ? 'current' : 'ahead';
132
+ result.reason = comparison > 0 ? null : comparison === 0 ? 'installed-matches-channel' : 'installed-ahead-of-channel';
133
+ if (comparison <= 0) return result;
134
+ if (!result.engineCompatible) return { ...result, reason: 'incompatible-node-version' };
135
+ if (!policy.allowPrerelease && semver.prerelease(release.version)) {
136
+ return { ...result, reason: 'prerelease-not-allowed' };
137
+ }
138
+ result.releaseEligible = true;
139
+ const authorization = authorizeTarget(policy, release.version);
140
+ result.versionEligible = authorization.allowed;
141
+ result.reason = authorization.reason;
142
+ if (authorization.allowed) {
143
+ if (!checkReadiness) return { ...result, reason: 'ownership-readiness-not-checked' };
144
+ try {
145
+ const readiness = await checkReadiness({ projectDir, targetVersion: release.version, policy });
146
+ result.canAutoApply = readiness?.ready === true;
147
+ result.reason = result.canAutoApply ? 'ready-for-authorized-update' : readiness?.reason || 'ownership-not-ready';
148
+ } catch { result.reason = 'ownership-readiness-failed'; }
149
+ }
150
+ return result;
151
+ }
152
+
153
+ module.exports = { CACHE_FILE, CACHE_TTL_MS, FAILURE_BACKOFF_MS, checkForUpdate, queryNpmRelease };
@@ -0,0 +1,182 @@
1
+ /** Discover and execute one exact, approved BMAD+ release. */
2
+ const path = require('node:path');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const assert = require('node:assert/strict');
6
+ const semver = require('semver');
7
+ const {
8
+ OFFICIAL_REGISTRY,
9
+ isExactVersion,
10
+ readUpdatePolicy,
11
+ authorizeTarget,
12
+ } = require('./update-policy');
13
+
14
+ function readProjectManifest(projectDir) {
15
+ const { safeAdapterPath } = require('./installed-adapters');
16
+ const { readInstallManifest } = require('./install-manifest');
17
+ return readInstallManifest(safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json'));
18
+ }
19
+
20
+ function refusal(reason, detail) {
21
+ return Object.assign(new Error(`Update refused: ${detail || reason}.`), {
22
+ code: 'UPDATE_REFUSED',
23
+ reason,
24
+ });
25
+ }
26
+
27
+ function requirePolicy(policy, targetVersion, auto) {
28
+ if (policy.registry !== OFFICIAL_REGISTRY) throw refusal('unsupported-registry');
29
+ if (policy.mode === 'off') throw refusal('updates-disabled');
30
+ if (semver.prerelease(targetVersion) && !policy.allowPrerelease)
31
+ throw refusal('prerelease-not-allowed');
32
+ if (auto) {
33
+ const authorization = authorizeTarget(policy, targetVersion);
34
+ if (!authorization.allowed) throw refusal(authorization.reason);
35
+ }
36
+ }
37
+
38
+ async function withExecutionDirectory(action) {
39
+ const parent = fs.realpathSync(os.tmpdir());
40
+ const directory = fs.mkdtempSync(path.join(parent, 'bmad-update-exec-'));
41
+ try {
42
+ // npm exec may otherwise select a project-local .cmd shim. npm's Windows
43
+ // shim fails on metacharacters in its own path; target argv is escaped by npm.
44
+ return await action(directory);
45
+ } finally {
46
+ const actual = fs.realpathSync(directory);
47
+ assert(
48
+ path.dirname(actual) === parent &&
49
+ path.basename(actual).startsWith('bmad-update-exec-') &&
50
+ !fs.lstatSync(directory).isSymbolicLink(),
51
+ 'Refusing to remove an unexpected updater temporary directory.'
52
+ );
53
+ fs.rmSync(actual, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
54
+ }
55
+ }
56
+
57
+ async function runLatestUpdate({ projectDir, auto = false, yes = false }, injectedDeps = {}) {
58
+ if (typeof auto !== 'boolean' || typeof yes !== 'boolean')
59
+ throw refusal('invalid-confirmation-options');
60
+ const directory = path.resolve(projectDir || process.cwd());
61
+ const checkForUpdate = injectedDeps.checkForUpdate || require('./update-check').checkForUpdate;
62
+ const runNpm = injectedDeps.runNpm || require('./npm-runner').runNpm;
63
+ const readManifest = injectedDeps.readManifest || readProjectManifest;
64
+ const readPolicy = injectedDeps.readPolicy || readUpdatePolicy;
65
+ const platform = injectedDeps.platform || process.platform;
66
+ const nodeVersion = injectedDeps.nodeVersion || process.version;
67
+ const executeInDirectory = injectedDeps.withExecutionDirectory || withExecutionDirectory;
68
+ const manifest = readManifest(directory);
69
+ if (!isExactVersion(manifest.version)) throw refusal('invalid-installation');
70
+ const initialPolicy = readPolicy(directory);
71
+ if (initialPolicy.mode === 'off') throw refusal('updates-disabled');
72
+ if (auto && initialPolicy.mode !== 'auto') throw refusal('automatic-updates-not-authorized');
73
+
74
+ const check = await checkForUpdate({ projectDir: directory, refresh: true });
75
+ if (!check || check.source !== 'registry' || check.stale !== false)
76
+ throw refusal('fresh-metadata-required');
77
+ const targetVersion = check.targetVersion;
78
+ if (!isExactVersion(targetVersion)) throw refusal('invalid-target-version');
79
+ if (check.channel !== initialPolicy.channel) throw refusal('policy-channel-changed');
80
+ if (check.installedVersion !== manifest.version)
81
+ throw refusal('installation-changed-during-discovery');
82
+ if (check.status === 'ahead' || semver.lt(targetVersion, manifest.version))
83
+ throw refusal('downgrade-not-allowed');
84
+ if (check.status === 'current' && semver.eq(targetVersion, manifest.version)) {
85
+ return { status: 'current', targetVersion, installedVersion: manifest.version, updated: false };
86
+ }
87
+ if (
88
+ check.status !== 'update-available' ||
89
+ check.updateAvailable !== true ||
90
+ check.releaseEligible !== true ||
91
+ check.engineCompatible !== true ||
92
+ !semver.gt(targetVersion, manifest.version)
93
+ ) {
94
+ throw refusal(check.reason || 'release-not-eligible');
95
+ }
96
+ const engine = check.engines?.node;
97
+ if (
98
+ !check.engines ||
99
+ typeof check.engines !== 'object' ||
100
+ Array.isArray(check.engines) ||
101
+ (engine !== undefined &&
102
+ (typeof engine !== 'string' ||
103
+ !semver.validRange(engine) ||
104
+ !semver.satisfies(nodeVersion, engine)))
105
+ ) {
106
+ throw refusal('incompatible-or-invalid-node-engine');
107
+ }
108
+ requirePolicy(initialPolicy, targetVersion, auto);
109
+
110
+ const args = [
111
+ 'exec',
112
+ '--yes',
113
+ `--package=bmad-plus@${targetVersion}`,
114
+ `--registry=${OFFICIAL_REGISTRY}`,
115
+ `--script-shell=${platform === 'win32' ? 'cmd.exe' : 'sh'}`,
116
+ '--',
117
+ 'bmad-plus',
118
+ 'update',
119
+ '--yes',
120
+ ...(auto ? ['--auto'] : []),
121
+ '--directory',
122
+ directory,
123
+ '--expected-version',
124
+ targetVersion,
125
+ ];
126
+ if (!auto && !yes) {
127
+ return {
128
+ status: 'approval-required',
129
+ reason: 'Confirm this exact update with --yes.',
130
+ installedVersion: manifest.version,
131
+ targetVersion,
132
+ updated: false,
133
+ command: ['npm', ...args],
134
+ };
135
+ }
136
+ if (auto) {
137
+ const evaluateReadiness =
138
+ injectedDeps.evaluateReadiness || require('./update-transaction').evaluateUpdateReadiness;
139
+ const readiness = await evaluateReadiness({ projectDir: directory, manifest });
140
+ if (readiness?.ready !== true || readiness.legacy || readiness.conflicts?.length) {
141
+ throw refusal(
142
+ 'ownership-preflight-failed',
143
+ readiness?.reason || 'The installation is not ready for an automatic update'
144
+ );
145
+ }
146
+ }
147
+ // Re-read authorization after asynchronous work, including potential revocation.
148
+ const finalPolicy = readPolicy(directory);
149
+ requirePolicy(finalPolicy, targetVersion, auto);
150
+ if (finalPolicy.channel !== check.channel) throw refusal('policy-channel-changed');
151
+ const beforeExec = readManifest(directory);
152
+ if (beforeExec.version !== manifest.version)
153
+ throw refusal('installation-changed-before-execution');
154
+ const output = await executeInDirectory((cwd) =>
155
+ runNpm(args, { cwd, timeout: 120000, maxBuffer: 4 * 1024 * 1024 })
156
+ );
157
+ const updated = readManifest(directory);
158
+ if (updated.version !== targetVersion) {
159
+ throw Object.assign(
160
+ new Error(
161
+ `Updater finished without the expected manifest version ${targetVersion}. Inspect the project before retrying.\n${output}`
162
+ ),
163
+ {
164
+ code: 'UPDATE_UNVERIFIED',
165
+ reason: 'manifest-version-mismatch',
166
+ targetVersion,
167
+ stdout: output,
168
+ }
169
+ );
170
+ }
171
+ return {
172
+ status: 'updated',
173
+ installedVersion: updated.version,
174
+ previousVersion: manifest.version,
175
+ targetVersion,
176
+ updated: true,
177
+ reloadInstructions: true,
178
+ output,
179
+ };
180
+ }
181
+
182
+ module.exports = { runLatestUpdate };
@@ -0,0 +1,90 @@
1
+ /** Explicit project policy for release discovery and automatic application. */
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 { safeAdapterPath } = require('./installed-adapters');
7
+
8
+ const POLICY_FILE = '_bmad/update-policy.json';
9
+ const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
10
+ const DEFAULT_POLICY = Object.freeze({
11
+ mode: 'notify', channel: 'latest', allowedRange: null,
12
+ allowPrerelease: false, registry: OFFICIAL_REGISTRY,
13
+ });
14
+
15
+ function isExactVersion(version) {
16
+ return typeof version === 'string' &&
17
+ /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
18
+ semver.valid(version) !== null;
19
+ }
20
+
21
+ function validatePolicy(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ throw new Error('Update policy must be an object.');
24
+ }
25
+ if (value.schemaVersion !== undefined && value.schemaVersion !== 1) {
26
+ throw new Error('Unsupported update policy schemaVersion.');
27
+ }
28
+ const policy = { ...DEFAULT_POLICY, ...value };
29
+ delete policy.schemaVersion;
30
+ if (Object.keys(policy).some(key => !Object.hasOwn(DEFAULT_POLICY, key)) ||
31
+ !['off', 'notify', 'auto'].includes(policy.mode) ||
32
+ typeof policy.channel !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(policy.channel) ||
33
+ typeof policy.allowPrerelease !== 'boolean' || policy.registry !== OFFICIAL_REGISTRY ||
34
+ (policy.allowedRange !== null && (typeof policy.allowedRange !== 'string' ||
35
+ !policy.allowedRange.trim() || !semver.validRange(policy.allowedRange)))) {
36
+ throw new Error('Invalid update policy: check mode, channel, range, prerelease flag and official registry.');
37
+ }
38
+ if (policy.mode === 'auto' && policy.allowedRange === null) {
39
+ throw new Error('Automatic updates require an explicit allowedRange.');
40
+ }
41
+ return policy;
42
+ }
43
+
44
+ function readUpdatePolicy(projectDir) {
45
+ const file = safeAdapterPath(projectDir, POLICY_FILE);
46
+ try {
47
+ return validatePolicy(JSON.parse(fs.readFileSync(file, 'utf8')));
48
+ } catch (error) {
49
+ if (error.code === 'ENOENT') return { ...DEFAULT_POLICY };
50
+ throw new Error(`Update policy is unreadable or invalid: ${error.message}`, { cause: error });
51
+ }
52
+ }
53
+
54
+ /** Atomic project-local JSON write; never follow linked destinations or parents. */
55
+ function writeProjectJson(projectDir, relativeFile, data) {
56
+ const file = safeAdapterPath(projectDir, relativeFile);
57
+ fs.mkdirSync(path.dirname(file), { recursive: true });
58
+ const temporaryName = `${relativeFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
59
+ const temporary = safeAdapterPath(projectDir, temporaryName);
60
+ try {
61
+ fs.writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
62
+ safeAdapterPath(projectDir, relativeFile);
63
+ fs.renameSync(temporary, file);
64
+ } finally {
65
+ try { fs.unlinkSync(temporary); } catch { /* Preserve the original write error if cleanup fails. */ }
66
+ }
67
+ }
68
+
69
+ function writeUpdatePolicy(projectDir, value) {
70
+ const policy = validatePolicy(value);
71
+ writeProjectJson(projectDir, POLICY_FILE, { schemaVersion: 1, ...policy });
72
+ return policy;
73
+ }
74
+
75
+ function authorizeTarget(value, version) {
76
+ let policy;
77
+ try { policy = validatePolicy(value); } catch { return { allowed: false, reason: 'invalid-policy' }; }
78
+ if (!isExactVersion(version)) return { allowed: false, reason: 'invalid-target-version' };
79
+ if (policy.mode !== 'auto') return { allowed: false, reason: policy.mode === 'off' ? 'updates-disabled' : 'approval-required' };
80
+ if (!policy.allowPrerelease && semver.prerelease(version)) return { allowed: false, reason: 'prerelease-not-allowed' };
81
+ if (!semver.satisfies(version, policy.allowedRange, { includePrerelease: policy.allowPrerelease })) {
82
+ return { allowed: false, reason: 'outside-allowed-range' };
83
+ }
84
+ return { allowed: true, reason: 'authorized-policy' };
85
+ }
86
+
87
+ module.exports = {
88
+ DEFAULT_POLICY, POLICY_FILE, OFFICIAL_REGISTRY, isExactVersion,
89
+ validatePolicy, readUpdatePolicy, writeUpdatePolicy, authorizeTarget, writeProjectJson,
90
+ };