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.
Files changed (63) hide show
  1. package/CHANGELOG.md +108 -30
  2. package/README.md +23 -23
  3. package/SECURITY.md +9 -1
  4. package/osint-agent-package/README.md +13 -0
  5. package/osint-agent-package/SETUP_KEYS.md +13 -0
  6. package/package.json +32 -8
  7. package/readme-international/README.de.md +133 -542
  8. package/readme-international/README.es.md +135 -561
  9. package/readme-international/README.fr.md +134 -558
  10. package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
  11. package/src/bmad-plus/packs/pack-memory/README.md +1 -1
  12. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
  13. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
  14. package/src/bmad-plus/packs/pack-shield/README.md +2 -0
  15. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
  16. package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
  17. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
  18. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +61 -1
  19. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +11 -0
  20. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +455 -137
  21. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +230 -20
  22. package/tools/build/adapters.config.js +1 -1
  23. package/tools/build/generate-adapters.js +186 -60
  24. package/tools/build/generate.js +49 -13
  25. package/tools/cli/bmad-plus-cli.js +3 -6
  26. package/tools/cli/commands/autoconfig.js +62 -22
  27. package/tools/cli/commands/install.js +620 -356
  28. package/tools/cli/commands/memory.js +40 -13
  29. package/tools/cli/commands/scan.js +94 -30
  30. package/tools/cli/commands/uat.js +50 -5
  31. package/tools/cli/commands/uninstall.js +65 -16
  32. package/tools/cli/commands/update-check.js +13 -4
  33. package/tools/cli/commands/update-policy.js +14 -5
  34. package/tools/cli/commands/update.js +96 -21
  35. package/tools/cli/i18n.js +120 -68
  36. package/tools/cli/lib/ide-config.js +3 -1
  37. package/tools/cli/lib/install-manifest.js +32 -7
  38. package/tools/cli/lib/installation-health.js +33 -1
  39. package/tools/cli/lib/installed-adapters.js +29 -6
  40. package/tools/cli/lib/memory-init.js +19 -4
  41. package/tools/cli/lib/packs.js +17 -28
  42. package/tools/cli/lib/path-hash.js +5 -1
  43. package/tools/cli/lib/python-provision.js +51 -12
  44. package/tools/cli/lib/stack-detect.js +51 -25
  45. package/tools/cli/lib/uat.js +139 -1
  46. package/tools/cli/lib/update-check.js +132 -34
  47. package/tools/cli/lib/update-dispatch.js +2 -2
  48. package/tools/cli/lib/update-policy.js +57 -19
  49. package/tools/cli/lib/update-transaction.js +439 -85
  50. package/tools/build/README.md +0 -78
  51. package/tools/build/check-counts.js +0 -626
  52. package/tools/build/check-install-contract.js +0 -648
  53. package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
  54. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
  55. package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
  56. package/tools/build/generated-adapters/AGENTS.md +0 -132
  57. package/tools/build/generated-adapters/CLAUDE.md +0 -135
  58. package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
  59. package/tools/build/generated-adapters/GEMINI.md +0 -139
  60. package/tools/build/generated-adapters/README.md +0 -79
  61. package/tools/maintain/upstream-candidate.js +0 -456
  62. package/tools/release/publication-content.js +0 -904
  63. package/tools/release/supply-chain.js +0 -282
@@ -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 { readUpdatePolicy, validatePolicy, authorizeTarget, isExactVersion, writeProjectJson } = require('./update-policy');
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 (!value || typeof value !== 'object' || Array.isArray(value) || !isExactVersion(value.version)) {
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 (!engines || typeof engines !== 'object' || Array.isArray(engines) ||
18
- (engines.node !== undefined && (typeof engines.node !== 'string' ||
19
- !engines.node.trim() || !semver.validRange(engines.node)))) {
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
- '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 });
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 (!cache || cache.schemaVersion !== 1 || cache.package !== 'bmad-plus' ||
44
- cache.registry !== policy.registry || cache.channel !== policy.channel) return null;
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 { return null; }
84
+ } catch {
85
+ return null;
86
+ }
52
87
  }
53
88
 
54
89
  function saveCache(projectDir, cache) {
55
- try { writeProjectJson(projectDir, CACHE_FILE, cache); } catch { /* A read-only cache is nonfatal. */ }
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, runningVersion = require('../../../package.json').version, refresh = false, offline = false,
60
- now = Date.now, queryRelease = queryNpmRelease, checkReadiness, nodeVersion = process.version,
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, 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,
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 (!isExactVersion(runningVersion) || !Number.isFinite(time) ||
70
- !Number.isFinite(new Date(time).getTime()) || !semver.valid(nodeVersion)) {
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(safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json'));
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 { ...result, status: 'invalid-installation', reason: 'missing-or-invalid-installation-manifest' };
142
+ return {
143
+ ...result,
144
+ status: 'invalid-installation',
145
+ reason: 'missing-or-invalid-installation-manifest',
146
+ };
79
147
  }
80
148
  let policy;
81
- try { policy = readUpdatePolicy(projectDir); } catch {
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(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() };
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, { ...cache, schemaVersion: 1, package: 'bmad-plus',
110
- registry: policy.registry, channel: policy.channel, failedAt: new Date(time).toISOString() });
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 = !release.engines.node || semver.satisfies(nodeVersion, release.engines.node);
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 = comparison > 0 ? null : comparison === 0 ? 'installed-matches-channel' : 'installed-ahead-of-channel';
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({ projectDir, targetVersion: release.version, policy });
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 ? 'ready-for-authorized-update' : readiness?.reason || 'ownership-not-ready';
148
- } catch { result.reason = 'ownership-readiness-failed'; }
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
- path.basename(actual).startsWith('bmad-update-exec-') &&
50
- !fs.lstatSync(directory).isSymbolicLink(),
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', channel: 'latest', allowedRange: null,
12
- allowPrerelease: false, registry: OFFICIAL_REGISTRY,
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 typeof version === 'string' &&
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 (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.');
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', { encoding: 'utf8', flag: 'wx' });
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 { fs.unlinkSync(temporary); } catch { /* Preserve the original write error if cleanup fails. */ }
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 { policy = validatePolicy(value); } catch { return { allowed: false, reason: 'invalid-policy' }; }
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') 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 })) {
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, POLICY_FILE, OFFICIAL_REGISTRY, isExactVersion,
89
- validatePolicy, readUpdatePolicy, writeUpdatePolicy, authorizeTarget, writeProjectJson,
119
+ DEFAULT_POLICY,
120
+ POLICY_FILE,
121
+ OFFICIAL_REGISTRY,
122
+ isExactVersion,
123
+ validatePolicy,
124
+ readUpdatePolicy,
125
+ writeUpdatePolicy,
126
+ authorizeTarget,
127
+ writeProjectJson,
90
128
  };