bmad-plus 0.12.2 → 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 (44) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +36 -8
  3. package/package.json +6 -4
  4. package/readme-international/README.de.md +37 -8
  5. package/readme-international/README.es.md +38 -9
  6. package/readme-international/README.fr.md +37 -8
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +2 -0
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  10. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  11. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  12. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +1 -1
  13. package/tools/bmad-plus-npx.js +4 -2
  14. package/tools/build/adapters.config.js +60 -51
  15. package/tools/build/check-counts.js +52 -54
  16. package/tools/build/check-install-contract.js +298 -0
  17. package/tools/build/generate-adapters.js +252 -56
  18. package/tools/build/generate.js +187 -10
  19. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  20. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  21. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  22. package/tools/build/generated-adapters/AGENTS.md +20 -7
  23. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  24. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  25. package/tools/build/generated-adapters/GEMINI.md +20 -7
  26. package/tools/build/module.template.yaml +82 -0
  27. package/tools/cli/bmad-plus-cli.js +16 -1
  28. package/tools/cli/commands/doctor.js +12 -40
  29. package/tools/cli/commands/install.js +108 -163
  30. package/tools/cli/commands/uninstall.js +173 -65
  31. package/tools/cli/commands/update-check.js +31 -0
  32. package/tools/cli/commands/update-policy.js +39 -0
  33. package/tools/cli/commands/update.js +102 -113
  34. package/tools/cli/i18n.js +60 -0
  35. package/tools/cli/lib/ide-config.js +4 -261
  36. package/tools/cli/lib/install-manifest.js +17 -0
  37. package/tools/cli/lib/installed-adapters.js +89 -0
  38. package/tools/cli/lib/npm-runner.js +177 -0
  39. package/tools/cli/lib/pack-copy.js +62 -66
  40. package/tools/cli/lib/packs.js +437 -3
  41. package/tools/cli/lib/update-check.js +153 -0
  42. package/tools/cli/lib/update-dispatch.js +182 -0
  43. package/tools/cli/lib/update-policy.js +90 -0
  44. package/tools/cli/lib/update-transaction.js +334 -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
+ };