bmad-plus 0.17.0 → 0.18.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.
- package/CHANGELOG.md +108 -30
- package/README.md +23 -23
- package/SECURITY.md +9 -1
- package/osint-agent-package/README.md +13 -0
- package/osint-agent-package/SETUP_KEYS.md +13 -0
- package/package.json +32 -8
- package/readme-international/README.de.md +133 -542
- package/readme-international/README.es.md +135 -561
- package/readme-international/README.fr.md +134 -558
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
- package/src/bmad-plus/packs/pack-memory/README.md +1 -1
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
- package/src/bmad-plus/packs/pack-shield/README.md +2 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
- package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +61 -1
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +11 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +455 -137
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +230 -20
- package/tools/build/adapters.config.js +1 -1
- package/tools/build/generate-adapters.js +186 -60
- package/tools/build/generate.js +49 -13
- package/tools/cli/bmad-plus-cli.js +3 -6
- package/tools/cli/commands/autoconfig.js +62 -22
- package/tools/cli/commands/install.js +620 -356
- package/tools/cli/commands/memory.js +40 -13
- package/tools/cli/commands/scan.js +94 -30
- package/tools/cli/commands/uat.js +50 -5
- package/tools/cli/commands/uninstall.js +65 -16
- package/tools/cli/commands/update-check.js +13 -4
- package/tools/cli/commands/update-policy.js +14 -5
- package/tools/cli/commands/update.js +96 -21
- package/tools/cli/i18n.js +120 -68
- package/tools/cli/lib/ide-config.js +3 -1
- package/tools/cli/lib/install-manifest.js +32 -7
- package/tools/cli/lib/installation-health.js +33 -1
- package/tools/cli/lib/installed-adapters.js +29 -6
- package/tools/cli/lib/memory-init.js +19 -4
- package/tools/cli/lib/packs.js +17 -28
- package/tools/cli/lib/path-hash.js +5 -1
- package/tools/cli/lib/python-provision.js +51 -12
- package/tools/cli/lib/stack-detect.js +51 -25
- package/tools/cli/lib/uat.js +139 -1
- package/tools/cli/lib/update-check.js +132 -34
- package/tools/cli/lib/update-dispatch.js +2 -2
- package/tools/cli/lib/update-policy.js +57 -19
- package/tools/cli/lib/update-transaction.js +439 -85
- package/tools/build/README.md +0 -78
- package/tools/build/check-counts.js +0 -626
- package/tools/build/check-install-contract.js +0 -648
- package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
- package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
- package/tools/build/generated-adapters/AGENTS.md +0 -132
- package/tools/build/generated-adapters/CLAUDE.md +0 -135
- package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
- package/tools/build/generated-adapters/GEMINI.md +0 -139
- package/tools/build/generated-adapters/README.md +0 -79
- package/tools/maintain/upstream-candidate.js +0 -456
- package/tools/release/publication-content.js +0 -904
- package/tools/release/supply-chain.js +0 -282
package/tools/cli/lib/uat.js
CHANGED
|
@@ -472,8 +472,144 @@ function languageCode(value, strings = STRINGS) {
|
|
|
472
472
|
return Object.keys(strings).find((code) => code.toLowerCase().split('-')[0] === base) || null;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* What every built page guarantees a tester, by construction. Each entry names a
|
|
477
|
+
* behaviour the DOM suite exercises (tests/unit/uat-page.test.js) and the literal
|
|
478
|
+
* the template must carry for it. A template that lost one does not build: the
|
|
479
|
+
* FormaPro incident of 2026-09-25 (answers typed before any run existed, then lost
|
|
480
|
+
* on reload, with a "saved" status nobody had verified) came from exactly such a loss.
|
|
481
|
+
*/
|
|
482
|
+
const PAGE_GUARANTEES = [
|
|
483
|
+
{
|
|
484
|
+
id: 'hidden-before-start',
|
|
485
|
+
what: 'no answer field is shown before a run exists, whatever a class says about display',
|
|
486
|
+
// The rule must also be the LAST rule of the sheet: a later !important display rule would beat it.
|
|
487
|
+
markers: [
|
|
488
|
+
'[hidden] { display: none !important; }\n</style>',
|
|
489
|
+
'<main class="steps" id="steps" hidden>',
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: 'no-answer-before-run',
|
|
494
|
+
what: 'a tick or a note before the run starts is refused and explained, never discarded in silence',
|
|
495
|
+
markers: [
|
|
496
|
+
'function requireRun(',
|
|
497
|
+
'T.startFirst',
|
|
498
|
+
'!requireRun(event)',
|
|
499
|
+
'if (!requireRun()) return;',
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: 'verified-local-write',
|
|
504
|
+
what: 'a write to this browser counts only once it reads back; a refused write returns false',
|
|
505
|
+
markers: ['return localStorage.getItem(k) === v;', 'function saveLocal()'],
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'honest-save-status',
|
|
509
|
+
what: 'the status line reports the verified outcome, and names a failed save',
|
|
510
|
+
markers: ['function showSaveState(', 'T.saveFailed', 'T.remoteFailed', 'T.saving'],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: 'save-on-every-change',
|
|
514
|
+
what: 'every tick and every keystroke in a note is saved at once, without waiting for blur, and a save never moves the cursor',
|
|
515
|
+
markers: [
|
|
516
|
+
'addEventListener("change"',
|
|
517
|
+
'addEventListener("input"',
|
|
518
|
+
'document.activeElement !== field',
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: 'restore-before-capabilities',
|
|
523
|
+
what: 'the browser copy is restored synchronously, before any optional capability answers',
|
|
524
|
+
markers: ['function restoreLocal()'],
|
|
525
|
+
order: ['restoreLocal();', 'probe()'],
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: 'newer-copy-wins',
|
|
529
|
+
what: 'an older remote copy never overwrites a newer local one; a copy without a date is the oldest of all',
|
|
530
|
+
markers: ['function connectDb()', 'remoteNewer', 'function at(', 'function adopt('],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: 'revision-guard',
|
|
534
|
+
what: 'a run that answered another revision is offered, explained and carried by an explicit rule, never poured in blindly',
|
|
535
|
+
markers: ['function sameRevision(', 'function carry(', 'T.otherRevision', 'T.continueHere'],
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: 'other-tab-notice',
|
|
539
|
+
what: 'two tabs on one run converge on the latest change, and the page says so',
|
|
540
|
+
markers: ['addEventListener("storage"', 'T.otherTab'],
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: 'unreadable-draft-kept',
|
|
544
|
+
what: 'a saved run that cannot be read is reported and exportable, never deleted',
|
|
545
|
+
markers: ['id="draft-problem"', 'T.draftUnreadable', 'id="btn-save-raw"'],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: 'progress-accessible',
|
|
549
|
+
what: 'the share of lines answered is exposed with a progressbar role and value; answered is not passed',
|
|
550
|
+
markers: ['role="progressbar"', 'aria-valuenow', 'T.percentAnswered'],
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: 'storage-explained',
|
|
554
|
+
what: 'the page says where answers live and what can make them disappear',
|
|
555
|
+
markers: ['id="storage-note"', 'id="mode-note"'],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 'finished-is-not-accepted',
|
|
559
|
+
what: 'finishing keeps the date and says it is not an acceptance',
|
|
560
|
+
markers: ['T.finishedNote', 'run.finishedAt = new Date().toISOString();'],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: 'unload-guard',
|
|
564
|
+
what: 'leaving with an unsaved run is questioned; pending remote writes are flushed',
|
|
565
|
+
markers: ['addEventListener("beforeunload"', 'addEventListener("pagehide"'],
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: 'utf8-and-escaped-diacritics',
|
|
569
|
+
what: 'the page declares UTF-8 and writes the combining-mark range as escapes',
|
|
570
|
+
markers: ['<meta charset="utf-8">', '\\u0300-\\u036f'],
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 'export-is-the-run',
|
|
574
|
+
what: 'the exported JSON is the run as answered, and nothing is exported before a run exists',
|
|
575
|
+
markers: ['download(runId + ".json", JSON.stringify(run, null, 2))'],
|
|
576
|
+
},
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Which guarantees a template carries: every marker present, in the required order where one
|
|
581
|
+
* is set. Checked on the TEMPLATE, never on the built page: a recipe's own text must not be
|
|
582
|
+
* able to satisfy or defeat a marker.
|
|
583
|
+
*/
|
|
584
|
+
function pageGuarantees(template) {
|
|
585
|
+
const missing = [];
|
|
586
|
+
for (const guarantee of PAGE_GUARANTEES) {
|
|
587
|
+
const lost = guarantee.markers.filter((marker) => !template.includes(marker));
|
|
588
|
+
if (!lost.length && guarantee.order) {
|
|
589
|
+
const positions = guarantee.order.map((marker) => template.indexOf(marker));
|
|
590
|
+
if (positions.some((at) => at < 0) || positions.some((at, i) => i && at < positions[i - 1]))
|
|
591
|
+
lost.push(`order ${guarantee.order.join(' → ')}`);
|
|
592
|
+
}
|
|
593
|
+
if (lost.length) missing.push({ id: guarantee.id, what: guarantee.what, lost });
|
|
594
|
+
}
|
|
595
|
+
const carried = PAGE_GUARANTEES.map((g) => g.id).filter(
|
|
596
|
+
(id) => !missing.some((entry) => entry.id === id)
|
|
597
|
+
);
|
|
598
|
+
return { ok: missing.length === 0, carried, missing };
|
|
599
|
+
}
|
|
600
|
+
|
|
475
601
|
function buildPage(spec, options = {}) {
|
|
476
602
|
const template = options.template || fs.readFileSync(TEMPLATE, 'utf8');
|
|
603
|
+
// A page missing one guarantee is not a page: refusing here is what keeps a tester's
|
|
604
|
+
// answers safe in an installation that has no test suite of its own.
|
|
605
|
+
const guarantees = pageGuarantees(template);
|
|
606
|
+
if (!guarantees.ok) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`the page template (${options.template ? 'the template given' : TEMPLATE}) lost a guarantee — not built: ${guarantees.missing
|
|
609
|
+
.map((entry) => `${entry.id}: ${entry.what} — missing ${entry.lost.join(', ')}`)
|
|
610
|
+
.join('; ')}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
477
613
|
const hash = specHash(spec);
|
|
478
614
|
const json = canonical(spec).replace(/<\//g, '<\\/');
|
|
479
615
|
const strings = options.strings || STRINGS;
|
|
@@ -502,7 +638,7 @@ function buildPage(spec, options = {}) {
|
|
|
502
638
|
cause: error,
|
|
503
639
|
});
|
|
504
640
|
}
|
|
505
|
-
return { html, sha256: hash, language };
|
|
641
|
+
return { html, sha256: hash, language, guarantees: guarantees.carried };
|
|
506
642
|
}
|
|
507
643
|
|
|
508
644
|
// ── Runs: legacy adapter, normalisation, reading ──────────────────────────────
|
|
@@ -873,6 +1009,8 @@ module.exports = {
|
|
|
873
1009
|
lintSpec,
|
|
874
1010
|
screenLabels,
|
|
875
1011
|
buildPage,
|
|
1012
|
+
PAGE_GUARANTEES,
|
|
1013
|
+
pageGuarantees,
|
|
876
1014
|
languageCode,
|
|
877
1015
|
normalizeRun,
|
|
878
1016
|
readRuns,
|
|
@@ -3,20 +3,37 @@ const fs = require('node:fs');
|
|
|
3
3
|
const semver = require('semver');
|
|
4
4
|
const { safeAdapterPath } = require('./installed-adapters');
|
|
5
5
|
const { readInstallManifest } = require('./install-manifest');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
readUpdatePolicy,
|
|
8
|
+
validatePolicy,
|
|
9
|
+
authorizeTarget,
|
|
10
|
+
isExactVersion,
|
|
11
|
+
writeProjectJson,
|
|
12
|
+
} = require('./update-policy');
|
|
7
13
|
|
|
8
14
|
const CACHE_FILE = '.bmad/update-check.json';
|
|
9
15
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
16
|
const FAILURE_BACKOFF_MS = 60 * 60 * 1000;
|
|
11
17
|
|
|
12
18
|
function validateRelease(value) {
|
|
13
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
!value ||
|
|
21
|
+
typeof value !== 'object' ||
|
|
22
|
+
Array.isArray(value) ||
|
|
23
|
+
!isExactVersion(value.version)
|
|
24
|
+
) {
|
|
14
25
|
throw new Error('Registry returned an invalid exact version.');
|
|
15
26
|
}
|
|
16
27
|
const engines = value.engines === undefined ? {} : value.engines;
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
if (
|
|
29
|
+
!engines ||
|
|
30
|
+
typeof engines !== 'object' ||
|
|
31
|
+
Array.isArray(engines) ||
|
|
32
|
+
(engines.node !== undefined &&
|
|
33
|
+
(typeof engines.node !== 'string' ||
|
|
34
|
+
!engines.node.trim() ||
|
|
35
|
+
!semver.validRange(engines.node)))
|
|
36
|
+
) {
|
|
20
37
|
throw new Error('Registry returned an invalid Node engine requirement.');
|
|
21
38
|
}
|
|
22
39
|
return { version: value.version, engines: engines.node ? { node: engines.node } : {} };
|
|
@@ -25,10 +42,20 @@ function validateRelease(value) {
|
|
|
25
42
|
async function queryNpmRelease({ projectDir, channel, registry }) {
|
|
26
43
|
validatePolicy({ channel, registry });
|
|
27
44
|
const { runNpm } = require('./npm-runner');
|
|
28
|
-
const stdout = await runNpm(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
const stdout = await runNpm(
|
|
46
|
+
[
|
|
47
|
+
'view',
|
|
48
|
+
`bmad-plus@${channel}`,
|
|
49
|
+
'version',
|
|
50
|
+
'engines',
|
|
51
|
+
'--json',
|
|
52
|
+
`--registry=${registry}`,
|
|
53
|
+
'--prefer-online',
|
|
54
|
+
'--fetch-timeout=4000',
|
|
55
|
+
'--fetch-retries=0',
|
|
56
|
+
],
|
|
57
|
+
{ cwd: projectDir, timeout: 6000, maxBuffer: 64 * 1024 }
|
|
58
|
+
);
|
|
32
59
|
return validateRelease(JSON.parse(stdout));
|
|
33
60
|
}
|
|
34
61
|
|
|
@@ -40,45 +67,88 @@ function timestamp(value, now) {
|
|
|
40
67
|
function readCache(projectDir, policy, now) {
|
|
41
68
|
try {
|
|
42
69
|
const cache = JSON.parse(fs.readFileSync(safeAdapterPath(projectDir, CACHE_FILE), 'utf8'));
|
|
43
|
-
if (
|
|
44
|
-
|
|
70
|
+
if (
|
|
71
|
+
!cache ||
|
|
72
|
+
cache.schemaVersion !== 1 ||
|
|
73
|
+
cache.package !== 'bmad-plus' ||
|
|
74
|
+
cache.registry !== policy.registry ||
|
|
75
|
+
cache.channel !== policy.channel
|
|
76
|
+
)
|
|
77
|
+
return null;
|
|
45
78
|
if (cache.release) {
|
|
46
79
|
cache.release = validateRelease(cache.release);
|
|
47
80
|
if (timestamp(cache.checkedAt, now) === null) return null;
|
|
48
81
|
}
|
|
49
82
|
if (cache.failedAt !== undefined && timestamp(cache.failedAt, now) === null) return null;
|
|
50
83
|
return cache;
|
|
51
|
-
} catch {
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
function saveCache(projectDir, cache) {
|
|
55
|
-
try {
|
|
90
|
+
try {
|
|
91
|
+
writeProjectJson(projectDir, CACHE_FILE, cache);
|
|
92
|
+
} catch {
|
|
93
|
+
/* A read-only cache is nonfatal. */
|
|
94
|
+
}
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
async function checkForUpdate({
|
|
59
|
-
projectDir,
|
|
60
|
-
|
|
98
|
+
projectDir,
|
|
99
|
+
runningVersion = require('../../../package.json').version,
|
|
100
|
+
refresh = false,
|
|
101
|
+
offline = false,
|
|
102
|
+
now = Date.now,
|
|
103
|
+
queryRelease = queryNpmRelease,
|
|
104
|
+
checkReadiness,
|
|
105
|
+
nodeVersion = process.version,
|
|
61
106
|
}) {
|
|
62
107
|
const time = typeof now === 'function' ? now() : now;
|
|
63
108
|
const result = {
|
|
64
|
-
schemaVersion: 1,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
installedVersion: null,
|
|
111
|
+
runningVersion,
|
|
112
|
+
latestVersion: null,
|
|
113
|
+
targetVersion: null,
|
|
114
|
+
channel: null,
|
|
115
|
+
status: 'unknown',
|
|
116
|
+
checkedAt: null,
|
|
117
|
+
source: 'none',
|
|
118
|
+
stale: false,
|
|
119
|
+
updateAvailable: null,
|
|
120
|
+
releaseEligible: false,
|
|
121
|
+
versionEligible: false,
|
|
122
|
+
canAutoApply: false,
|
|
123
|
+
engineCompatible: null,
|
|
124
|
+
engines: {},
|
|
125
|
+
reason: null,
|
|
68
126
|
};
|
|
69
|
-
if (
|
|
70
|
-
|
|
127
|
+
if (
|
|
128
|
+
!isExactVersion(runningVersion) ||
|
|
129
|
+
!Number.isFinite(time) ||
|
|
130
|
+
!Number.isFinite(new Date(time).getTime()) ||
|
|
131
|
+
!semver.valid(nodeVersion)
|
|
132
|
+
) {
|
|
71
133
|
return { ...result, status: 'invalid-runtime', reason: 'invalid-running-version-or-clock' };
|
|
72
134
|
}
|
|
73
135
|
try {
|
|
74
|
-
const manifest = readInstallManifest(
|
|
136
|
+
const manifest = readInstallManifest(
|
|
137
|
+
safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json')
|
|
138
|
+
);
|
|
75
139
|
if (!isExactVersion(manifest.version)) throw new Error('Invalid installed version');
|
|
76
140
|
result.installedVersion = manifest.version;
|
|
77
141
|
} catch {
|
|
78
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
...result,
|
|
144
|
+
status: 'invalid-installation',
|
|
145
|
+
reason: 'missing-or-invalid-installation-manifest',
|
|
146
|
+
};
|
|
79
147
|
}
|
|
80
148
|
let policy;
|
|
81
|
-
try {
|
|
149
|
+
try {
|
|
150
|
+
policy = readUpdatePolicy(projectDir);
|
|
151
|
+
} catch {
|
|
82
152
|
return { ...result, status: 'invalid-policy', reason: 'unreadable-or-invalid-update-policy' };
|
|
83
153
|
}
|
|
84
154
|
result.channel = policy.channel;
|
|
@@ -98,16 +168,30 @@ async function checkForUpdate({
|
|
|
98
168
|
result.checkedAt = cache.checkedAt;
|
|
99
169
|
} else {
|
|
100
170
|
try {
|
|
101
|
-
release = validateRelease(
|
|
102
|
-
|
|
103
|
-
|
|
171
|
+
release = validateRelease(
|
|
172
|
+
await queryRelease({ projectDir, channel: policy.channel, registry: policy.registry })
|
|
173
|
+
);
|
|
174
|
+
cache = {
|
|
175
|
+
schemaVersion: 1,
|
|
176
|
+
package: 'bmad-plus',
|
|
177
|
+
registry: policy.registry,
|
|
178
|
+
channel: policy.channel,
|
|
179
|
+
release,
|
|
180
|
+
checkedAt: new Date(time).toISOString(),
|
|
181
|
+
};
|
|
104
182
|
saveCache(projectDir, cache);
|
|
105
183
|
result.source = 'registry';
|
|
106
184
|
result.checkedAt = cache.checkedAt;
|
|
107
185
|
} catch {
|
|
108
186
|
result.reason = 'registry-unavailable';
|
|
109
|
-
saveCache(projectDir, {
|
|
110
|
-
|
|
187
|
+
saveCache(projectDir, {
|
|
188
|
+
...cache,
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
package: 'bmad-plus',
|
|
191
|
+
registry: policy.registry,
|
|
192
|
+
channel: policy.channel,
|
|
193
|
+
failedAt: new Date(time).toISOString(),
|
|
194
|
+
});
|
|
111
195
|
}
|
|
112
196
|
}
|
|
113
197
|
if (!release) {
|
|
@@ -125,11 +209,17 @@ async function checkForUpdate({
|
|
|
125
209
|
result.targetVersion = release.version;
|
|
126
210
|
result.latestVersion = policy.channel === 'latest' ? release.version : null;
|
|
127
211
|
result.engines = release.engines;
|
|
128
|
-
result.engineCompatible =
|
|
212
|
+
result.engineCompatible =
|
|
213
|
+
!release.engines.node || semver.satisfies(nodeVersion, release.engines.node);
|
|
129
214
|
const comparison = semver.compare(release.version, result.installedVersion);
|
|
130
215
|
result.updateAvailable = comparison > 0;
|
|
131
216
|
result.status = comparison > 0 ? 'update-available' : comparison === 0 ? 'current' : 'ahead';
|
|
132
|
-
result.reason =
|
|
217
|
+
result.reason =
|
|
218
|
+
comparison > 0
|
|
219
|
+
? null
|
|
220
|
+
: comparison === 0
|
|
221
|
+
? 'installed-matches-channel'
|
|
222
|
+
: 'installed-ahead-of-channel';
|
|
133
223
|
if (comparison <= 0) return result;
|
|
134
224
|
if (!result.engineCompatible) return { ...result, reason: 'incompatible-node-version' };
|
|
135
225
|
if (!policy.allowPrerelease && semver.prerelease(release.version)) {
|
|
@@ -142,10 +232,18 @@ async function checkForUpdate({
|
|
|
142
232
|
if (authorization.allowed) {
|
|
143
233
|
if (!checkReadiness) return { ...result, reason: 'ownership-readiness-not-checked' };
|
|
144
234
|
try {
|
|
145
|
-
const readiness = await checkReadiness({
|
|
235
|
+
const readiness = await checkReadiness({
|
|
236
|
+
projectDir,
|
|
237
|
+
targetVersion: release.version,
|
|
238
|
+
policy,
|
|
239
|
+
});
|
|
146
240
|
result.canAutoApply = readiness?.ready === true;
|
|
147
|
-
result.reason = result.canAutoApply
|
|
148
|
-
|
|
241
|
+
result.reason = result.canAutoApply
|
|
242
|
+
? 'ready-for-authorized-update'
|
|
243
|
+
: readiness?.reason || 'ownership-not-ready';
|
|
244
|
+
} catch {
|
|
245
|
+
result.reason = 'ownership-readiness-failed';
|
|
246
|
+
}
|
|
149
247
|
}
|
|
150
248
|
return result;
|
|
151
249
|
}
|
|
@@ -46,8 +46,8 @@ async function withExecutionDirectory(action) {
|
|
|
46
46
|
const actual = fs.realpathSync(directory);
|
|
47
47
|
assert(
|
|
48
48
|
path.dirname(actual) === parent &&
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
path.basename(actual).startsWith('bmad-update-exec-') &&
|
|
50
|
+
!fs.lstatSync(directory).isSymbolicLink(),
|
|
51
51
|
'Refusing to remove an unexpected updater temporary directory.'
|
|
52
52
|
);
|
|
53
53
|
fs.rmSync(actual, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -8,14 +8,19 @@ const { safeAdapterPath } = require('./installed-adapters');
|
|
|
8
8
|
const POLICY_FILE = '_bmad/update-policy.json';
|
|
9
9
|
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
10
10
|
const DEFAULT_POLICY = Object.freeze({
|
|
11
|
-
mode: 'notify',
|
|
12
|
-
|
|
11
|
+
mode: 'notify',
|
|
12
|
+
channel: 'latest',
|
|
13
|
+
allowedRange: null,
|
|
14
|
+
allowPrerelease: false,
|
|
15
|
+
registry: OFFICIAL_REGISTRY,
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
function isExactVersion(version) {
|
|
16
|
-
return
|
|
19
|
+
return (
|
|
20
|
+
typeof version === 'string' &&
|
|
17
21
|
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
|
|
18
|
-
semver.valid(version) !== null
|
|
22
|
+
semver.valid(version) !== null
|
|
23
|
+
);
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function validatePolicy(value) {
|
|
@@ -27,13 +32,21 @@ function validatePolicy(value) {
|
|
|
27
32
|
}
|
|
28
33
|
const policy = { ...DEFAULT_POLICY, ...value };
|
|
29
34
|
delete policy.schemaVersion;
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (
|
|
36
|
+
Object.keys(policy).some((key) => !Object.hasOwn(DEFAULT_POLICY, key)) ||
|
|
37
|
+
!['off', 'notify', 'auto'].includes(policy.mode) ||
|
|
38
|
+
typeof policy.channel !== 'string' ||
|
|
39
|
+
!/^[a-z][a-z0-9_-]{0,31}$/.test(policy.channel) ||
|
|
40
|
+
typeof policy.allowPrerelease !== 'boolean' ||
|
|
41
|
+
policy.registry !== OFFICIAL_REGISTRY ||
|
|
42
|
+
(policy.allowedRange !== null &&
|
|
43
|
+
(typeof policy.allowedRange !== 'string' ||
|
|
44
|
+
!policy.allowedRange.trim() ||
|
|
45
|
+
!semver.validRange(policy.allowedRange)))
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Invalid update policy: check mode, channel, range, prerelease flag and official registry.'
|
|
49
|
+
);
|
|
37
50
|
}
|
|
38
51
|
if (policy.mode === 'auto' && policy.allowedRange === null) {
|
|
39
52
|
throw new Error('Automatic updates require an explicit allowedRange.');
|
|
@@ -58,11 +71,18 @@ function writeProjectJson(projectDir, relativeFile, data) {
|
|
|
58
71
|
const temporaryName = `${relativeFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
59
72
|
const temporary = safeAdapterPath(projectDir, temporaryName);
|
|
60
73
|
try {
|
|
61
|
-
fs.writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', {
|
|
74
|
+
fs.writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
flag: 'wx',
|
|
77
|
+
});
|
|
62
78
|
safeAdapterPath(projectDir, relativeFile);
|
|
63
79
|
fs.renameSync(temporary, file);
|
|
64
80
|
} finally {
|
|
65
|
-
try {
|
|
81
|
+
try {
|
|
82
|
+
fs.unlinkSync(temporary);
|
|
83
|
+
} catch {
|
|
84
|
+
/* Preserve the original write error if cleanup fails. */
|
|
85
|
+
}
|
|
66
86
|
}
|
|
67
87
|
}
|
|
68
88
|
|
|
@@ -74,17 +94,35 @@ function writeUpdatePolicy(projectDir, value) {
|
|
|
74
94
|
|
|
75
95
|
function authorizeTarget(value, version) {
|
|
76
96
|
let policy;
|
|
77
|
-
try {
|
|
97
|
+
try {
|
|
98
|
+
policy = validatePolicy(value);
|
|
99
|
+
} catch {
|
|
100
|
+
return { allowed: false, reason: 'invalid-policy' };
|
|
101
|
+
}
|
|
78
102
|
if (!isExactVersion(version)) return { allowed: false, reason: 'invalid-target-version' };
|
|
79
|
-
if (policy.mode !== 'auto')
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
if (policy.mode !== 'auto')
|
|
104
|
+
return {
|
|
105
|
+
allowed: false,
|
|
106
|
+
reason: policy.mode === 'off' ? 'updates-disabled' : 'approval-required',
|
|
107
|
+
};
|
|
108
|
+
if (!policy.allowPrerelease && semver.prerelease(version))
|
|
109
|
+
return { allowed: false, reason: 'prerelease-not-allowed' };
|
|
110
|
+
if (
|
|
111
|
+
!semver.satisfies(version, policy.allowedRange, { includePrerelease: policy.allowPrerelease })
|
|
112
|
+
) {
|
|
82
113
|
return { allowed: false, reason: 'outside-allowed-range' };
|
|
83
114
|
}
|
|
84
115
|
return { allowed: true, reason: 'authorized-policy' };
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
module.exports = {
|
|
88
|
-
DEFAULT_POLICY,
|
|
89
|
-
|
|
119
|
+
DEFAULT_POLICY,
|
|
120
|
+
POLICY_FILE,
|
|
121
|
+
OFFICIAL_REGISTRY,
|
|
122
|
+
isExactVersion,
|
|
123
|
+
validatePolicy,
|
|
124
|
+
readUpdatePolicy,
|
|
125
|
+
writeUpdatePolicy,
|
|
126
|
+
authorizeTarget,
|
|
127
|
+
writeProjectJson,
|
|
90
128
|
};
|