@sfmc-bds/cli 0.2.5 → 0.3.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 (41) hide show
  1. package/dist/command-surface.d.ts.map +1 -1
  2. package/dist/command-surface.js +5 -1
  3. package/dist/command-surface.js.map +2 -2
  4. package/dist/commands.d.ts.map +1 -1
  5. package/dist/commands.js +20 -79
  6. package/dist/commands.js.map +2 -2
  7. package/dist/help-text.d.ts.map +1 -1
  8. package/dist/help-text.js +2 -0
  9. package/dist/help-text.js.map +2 -2
  10. package/dist/i18n/locales/en.d.ts +33 -0
  11. package/dist/i18n/locales/en.d.ts.map +1 -1
  12. package/dist/i18n/locales/en.js +33 -0
  13. package/dist/i18n/locales/en.js.map +2 -2
  14. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  15. package/dist/i18n/locales/zh-CN.js +33 -0
  16. package/dist/i18n/locales/zh-CN.js.map +2 -2
  17. package/dist/module-commands.d.ts.map +1 -1
  18. package/dist/module-commands.js +8 -0
  19. package/dist/module-commands.js.map +2 -2
  20. package/dist/module-update/index.d.ts +18 -0
  21. package/dist/module-update/index.d.ts.map +1 -0
  22. package/dist/module-update/index.js +223 -0
  23. package/dist/module-update/index.js.map +7 -0
  24. package/dist/repl.d.ts.map +1 -1
  25. package/dist/repl.js +11 -0
  26. package/dist/repl.js.map +2 -2
  27. package/dist/services.d.ts +29 -1
  28. package/dist/services.d.ts.map +1 -1
  29. package/dist/services.js +79 -1
  30. package/dist/services.js.map +2 -2
  31. package/dist/wizard.d.ts.map +1 -1
  32. package/dist/wizard.js +2 -0
  33. package/dist/wizard.js.map +2 -2
  34. package/package.json +1 -1
  35. package/scripts/module-install/fetch-module.mjs +164 -76
  36. package/scripts/module-install/lib/module-update-config.mjs +83 -0
  37. package/scripts/module-install/lib/module-update.mjs +507 -0
  38. package/scripts/module-install/lib/pins.mjs +436 -0
  39. package/scripts/module-install/lib/registry-cache.mjs +83 -0
  40. package/scripts/module-install/lib/semver.mjs +124 -0
  41. package/scripts/module-install/test/module-update.test.mjs +182 -0
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * 安装成功后会把 sapi/manifest.json 投影写入 modules/catalog.json,
7
7
  * 并按 enabledByDefault 更新 modules/module-lock.json。
8
+ * --preserve-lock 时,若 lock 里已有该模块,则保留当前启停。
9
+ * 成功后写入 modules/module-pins.json。
8
10
  *
9
11
  * Usage:
10
12
  * node packages/cli/scripts/module-install/fetch-module.mjs search
@@ -38,76 +40,29 @@ import { removeCatalogEntry, upsertCatalogEntry } from "./lib/catalog.mjs";
38
40
  import { seedModuleConfig } from "./lib/config-seeding.mjs";
39
41
  import { exists } from "./lib/io.mjs";
40
42
  import { isSchemeFrom, normalizeBarePathFrom, normalizeLinkFrom } from "./lib/link-from.mjs";
41
- import { removeModuleLock, setModuleLockEnabled } from "./lib/lock.mjs";
43
+ import { readLock, removeModuleLock, setModuleLockEnabled } from "./lib/lock.mjs";
44
+ import { QUIET_SKIP_REASONS, applyModuleUpgrades, planModuleUpdates, resyncInstalledCatalog } from "./lib/module-update.mjs";
42
45
  import { folderFromNpmPackageName } from "./lib/npm-resolver.mjs";
43
46
  import { loadPackageCatalogEntry } from "./lib/packages.mjs";
44
47
  import { PACKAGES_DIR, ROOT } from "./lib/paths.mjs";
45
- import { parseRegistryIndex } from "./lib/registry-index.mjs";
48
+ import { recordInstallPin, removePin, shouldPreserveLock } from "./lib/pins.mjs";
49
+ import { DEFAULT_REGISTRY_REPO, DEFAULT_REGISTRY_TAG, resolveRegistryIndex } from "./lib/registry-cache.mjs";
46
50
 
47
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
48
51
  const TARGET = PACKAGES_DIR;
49
52
 
50
- const DEFAULT_REGISTRY_REPO = "Tanya7z/sfmc-modules";
51
- const DEFAULT_REGISTRY_TAG = "main";
52
- const DEFAULT_REGISTRY_INDEX_URL = `https://raw.githubusercontent.com/${DEFAULT_REGISTRY_REPO}/${DEFAULT_REGISTRY_TAG}/index.json`;
53
- const REGISTRY_CACHE_PATH = path.join(__dirname, ".sfmc-registry-cache.json");
54
- const REGISTRY_CACHE_TTL_MS = 60 * 60 * 1000;
55
-
56
53
  /**
57
- * @typedef {{ repo: string, tag: string }} RegistryEntry
58
- * @typedef {Record<string, RegistryEntry>} RegistryIndex
59
- * @typedef {{ fetchedAt: number, index: RegistryIndex }} RegistryCache
54
+ * 仅在直接执行本文件时跑 main。被 mod update 动态 import 时不要再解析命令行。
60
55
  */
61
-
62
- function readCache() {
56
+ function isFetchModuleEntry() {
57
+ const entry = process.argv[1];
58
+ if (!entry) return false;
63
59
  try {
64
- return JSON.parse(fs.readFileSync(REGISTRY_CACHE_PATH, "utf8"));
60
+ const thisFile = fs.realpathSync.native(fileURLToPath(import.meta.url));
61
+ const entryFile = fs.realpathSync.native(path.resolve(entry));
62
+ const norm = (value) => (process.platform === "win32" ? value.toLowerCase() : value);
63
+ return norm(thisFile) === norm(entryFile);
65
64
  } catch {
66
- return null;
67
- }
68
- }
69
-
70
- /**
71
- * @param {{ fetchedAt: number; index: Record<string, import("./lib/registry-index.mjs").RegistryEntry>; }} cache
72
- */
73
- function writeCache(cache) {
74
- try {
75
- fs.writeFileSync(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
76
- } catch {
77
- /* best-effort */
78
- }
79
- }
80
-
81
- async function fetchRegistryIndexFresh() {
82
- const res = await fetch(DEFAULT_REGISTRY_INDEX_URL, { headers: { "User-Agent": "sfmc-fetch-module" } });
83
- if (!res.ok) throw new Error(`HTTP ${res.status} for ${DEFAULT_REGISTRY_INDEX_URL}`);
84
- return parseRegistryIndex(await res.json());
85
- }
86
-
87
- async function resolveRegistryIndex() {
88
- const cache = readCache();
89
- if (cache && Date.now() - cache.fetchedAt < REGISTRY_CACHE_TTL_MS) {
90
- try {
91
- const fresh = await fetchRegistryIndexFresh();
92
- writeCache({ fetchedAt: Date.now(), index: fresh });
93
- return { index: fresh, stale: false };
94
- } catch {
95
- return { index: cache.index, stale: false };
96
- }
97
- }
98
- try {
99
- const fresh = await fetchRegistryIndexFresh();
100
- writeCache({ fetchedAt: Date.now(), index: fresh });
101
- return { index: fresh, stale: false };
102
- } catch (e) {
103
- const message = e instanceof Error ? e.message : String(e);
104
- if (cache) {
105
- console.warn(
106
- `[fetch-module] registry offline (${message}); using cached index from ${new Date(cache.fetchedAt).toISOString()}`
107
- );
108
- return { index: cache.index, stale: true };
109
- }
110
- throw new Error(`registry unreachable and no cache: ${message}. Pass --from explicitly to skip the registry.`);
65
+ return false;
111
66
  }
112
67
  }
113
68
 
@@ -177,7 +132,7 @@ function die(msg, code = 1) {
177
132
  * @param {string | any[]} args
178
133
  */
179
134
  function parseArgs(args) {
180
- const flags = { from: null, sha256: null, link: false };
135
+ const flags = { from: null, sha256: null, link: false, preserveLock: false };
181
136
  /** @type {string[]} */
182
137
  const positional = [];
183
138
  for (let i = 0; i < args.length; i++) {
@@ -185,6 +140,7 @@ function parseArgs(args) {
185
140
  if (a === "--from") flags.from = args[++i];
186
141
  else if (a === "--sha256") flags.sha256 = args[++i];
187
142
  else if (a === "--link") flags.link = true;
143
+ else if (a === "--preserve-lock") flags.preserveLock = true;
188
144
  else if (a.startsWith("--from=")) flags.from = a.slice("--from=".length);
189
145
  else if (a.startsWith("--")) die(`unknown flag: ${a}`);
190
146
  else positional.push(a);
@@ -274,7 +230,22 @@ async function fetchToBuffer(url) {
274
230
  }
275
231
 
276
232
  /**
277
- * 安装落盘后同步 catalog + lock;并对 copy/zip 产物做路径/命名规范化
233
+ * 把安装旗标收成 afterInstall 能看懂的来源信息。
234
+ * @param {{ from?: string, link?: boolean, preserveLock?: boolean } | undefined} flags
235
+ * @param {{ skipNormalize?: boolean }} [extra]
236
+ */
237
+ function installOpts(flags, extra = {}) {
238
+ return {
239
+ preserveLock: !!flags?.preserveLock,
240
+ from: flags?.from || "",
241
+ link: !!flags?.link,
242
+ ...extra,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * 安装落盘后同步 catalog + lock;并对 copy/zip 产物做路径/命名规范化。
248
+ * preserveLock 时保留服主已经写入的启停状态。
278
249
  * @param {string} folder
279
250
  */
280
251
  function afterInstall(folder, opts = {}) {
@@ -306,8 +277,13 @@ function afterInstall(folder, opts = {}) {
306
277
  }
307
278
 
308
279
  const entry = upsertCatalogEntry(folder);
309
- setModuleLockEnabled(entry.id, entry.enabledByDefault !== false);
310
- console.log(`[fetch-module] catalog+lock: ${entry.id} (enabled=${entry.enabledByDefault !== false})`);
280
+ const keepLock = shouldPreserveLock(!!opts.preserveLock, Object.prototype.hasOwnProperty.call(readLock().modules, entry.id));
281
+ if (keepLock) {
282
+ console.log(`[fetch-module] lock preserved: ${entry.id}`);
283
+ } else {
284
+ setModuleLockEnabled(entry.id, entry.enabledByDefault !== false);
285
+ console.log(`[fetch-module] catalog+lock: ${entry.id} (enabled=${entry.enabledByDefault !== false})`);
286
+ }
311
287
  /* v3 状态透传:只读 semantic 字段已通过 projectCatalogEntry 投影到 catalog。
312
288
  * 模块作者想用 v3 时只需在 sapi/manifest.json 写 `"schemaVersion": 3` 与 semantic 块;
313
289
  * fetch-module 不会再追问也不会主动注入。 */
@@ -323,6 +299,11 @@ function afterInstall(folder, opts = {}) {
323
299
  console.warn(`[fetch-module] check warning: ${warning}`);
324
300
  }
325
301
  console.log(`[fetch-module] ${check.summary}`);
302
+ try {
303
+ recordInstallPin(ROOT, folder, { from: opts.from || "", link: !!opts.link });
304
+ } catch (err) {
305
+ console.warn(`[fetch-module] pin warn: ${err instanceof Error ? err.message : String(err)}`);
306
+ }
326
307
  return entry;
327
308
  }
328
309
 
@@ -450,7 +431,7 @@ async function fromLocal(id, source, flags) {
450
431
  * @param {string} pkgName
451
432
  * @param {any} flags
452
433
  */
453
- async function fromRegistry(id, pkgName) {
434
+ async function fromRegistry(id, pkgName, flags = {}) {
454
435
  const dir = await ensureTarget(id);
455
436
  const os = await import("node:os");
456
437
  const tmpPrefix = await fsp.mkdtemp(path.join(os.tmpdir(), "sfmc-pnpm-install-"));
@@ -472,7 +453,7 @@ async function fromRegistry(id, pkgName) {
472
453
  }
473
454
  console.log(`[fetch-module] installed ${id} from registry (${pkgName})`);
474
455
  console.log(`[fetch-module] target: ${dir}`);
475
- afterInstall(id);
456
+ afterInstall(id, installOpts(flags));
476
457
  }
477
458
 
478
459
  /**
@@ -585,7 +566,7 @@ async function installLocalArtifact(id, absPath, flags, opts = {}) {
585
566
  console.log(`[fetch-module] installed ${id} from ${absPath}`);
586
567
  console.log(`[fetch-module] sha256: ${actual}`);
587
568
  console.log(`[fetch-module] target: ${dir}`);
588
- afterInstall(id);
569
+ afterInstall(id, installOpts(flags));
589
570
  }
590
571
 
591
572
  /**
@@ -629,13 +610,13 @@ async function fromDir(id, source, flags = {}) {
629
610
  console.log(`[fetch-module] linked ${id} → ${srcDir}`);
630
611
  console.log(`[fetch-module] mode: ${process.platform === "win32" ? "junction" : "symlink"}`);
631
612
  console.log(`[fetch-module] target: ${dest}`);
632
- afterInstall(id, { skipNormalize: true });
613
+ afterInstall(id, installOpts(flags, { skipNormalize: true }));
633
614
  } else {
634
615
  const dir = await ensureTarget(id);
635
616
  await copyDir(srcDir, dir);
636
617
  console.log(`[fetch-module] installed ${id} from dir ${srcDir}`);
637
618
  console.log(`[fetch-module] target: ${dir}`);
638
- afterInstall(id);
619
+ afterInstall(id, installOpts(flags));
639
620
  }
640
621
  }
641
622
 
@@ -711,7 +692,7 @@ async function fromGithub(id, source, flags) {
711
692
  console.log(`[fetch-module] installed ${id} v${version} from ${owner}/${repo}@${rel.tag_name ?? tag}`);
712
693
  console.log(`[fetch-module] sha256: ${actual}`);
713
694
  console.log(`[fetch-module] target: ${dir}`);
714
- afterInstall(id);
695
+ afterInstall(id, installOpts(flags));
715
696
  }
716
697
 
717
698
  /**
@@ -933,7 +914,7 @@ async function installOne(id, flags) {
933
914
  if (from.startsWith("local:") || from === "local") return fromLocal(id, from, perFlags);
934
915
  if (from.startsWith("tgz:")) return fromTgz(id, from, perFlags);
935
916
  if (from.startsWith("zip:")) return fromZip(id, from, perFlags);
936
- if (from.startsWith("npm:")) return fromRegistry(id, from.slice("npm:".length));
917
+ if (from.startsWith("npm:")) return fromRegistry(id, from.slice("npm:".length), perFlags);
937
918
  if (from.startsWith("dir:")) {
938
919
  const base = from.slice("dir:".length);
939
920
  const candidate = path.join(base, id);
@@ -964,6 +945,9 @@ async function uninstallOne(id) {
964
945
  console.log(`[fetch-module] uninstalled ${id} (no package dir; catalog/lock cleaned)`);
965
946
  }
966
947
  if (removed) console.log(`[fetch-module] catalog removed: ${removed.id}`);
948
+ if (removePin(ROOT, removed?.id || id)) {
949
+ console.log(`[fetch-module] pin removed: ${removed?.id || id}`);
950
+ }
967
951
  }
968
952
 
969
953
  function printHelp() {
@@ -972,9 +956,11 @@ function printHelp() {
972
956
  Commands:
973
957
  search list first-party registry
974
958
  list [--from github:owner/repo@tag] list release assets (default: first-party)
975
- install <id> [id2 ...] [--from ...] [--link]
959
+ install <id> [id2 ...] [--from ...] [--link] [--preserve-lock]
976
960
  install one or more modules + sync catalog/lock
977
- uninstall <id> [id2 ...] remove package dir + catalog/lock entries
961
+ update [id ...] [--check] [--yes] [--allow-major] [--startup]
962
+ plan or apply module updates
963
+ uninstall <id> [id2 ...] remove package dir + catalog/lock/pin entries
978
964
 
979
965
  Sources (按优先级):
980
966
  npm:@scope/name npm registry(默认;install <id> → @sfmc-bds/module-<id>)
@@ -985,11 +971,106 @@ Sources (按优先级):
985
971
  github:owner/repo[@tag] GitHub Release(兼容旧 first-party)
986
972
 
987
973
  Flags:
988
- --link with dir: or local:<dir> — junction (Windows) / symlink (POSIX) into
989
- modules/packages/<id> instead of copying. Still syncs catalog/lock.
974
+ --link with dir: or local:<dir> — junction (Windows) / symlink (POSIX)
975
+ --preserve-lock keep the current module-lock enabled bit
990
976
  `);
991
977
  }
992
978
 
979
+ /**
980
+ * 升级时保留启停锁。供 mod update 调用,不走全新安装的默认启用。
981
+ * @param {string} id
982
+ * @param {string} spec
983
+ */
984
+ export async function installPreservingLock(id, spec) {
985
+ await installOne(id, { from: spec, sha256: null, link: false, preserveLock: true });
986
+ }
987
+
988
+ /**
989
+ * @param {string[]} args
990
+ */
991
+ function parseUpdateArgs(args) {
992
+ const flags = { check: false, yes: false, allowMajor: false, startup: false };
993
+ /** @type {string[]} */
994
+ const positional = [];
995
+ for (const a of args) {
996
+ if (a === "--check") flags.check = true;
997
+ else if (a === "--yes") flags.yes = true;
998
+ else if (a === "--allow-major") flags.allowMajor = true;
999
+ else if (a === "--startup") flags.startup = true;
1000
+ else if (a.startsWith("--")) die(`unknown flag: ${a}`);
1001
+ else positional.push(a);
1002
+ }
1003
+ return { flags, positional };
1004
+ }
1005
+
1006
+ /**
1007
+ * @param {import("./lib/module-update.mjs").ModuleSkip} item
1008
+ */
1009
+ function formatSkip(item) {
1010
+ const versions = item.fromVersion || item.toVersion ? ` ${item.fromVersion || "?"} -> ${item.toVersion || "?"}` : "";
1011
+ const detail = item.detail ? ` (${item.detail})` : "";
1012
+ return ` skip ${item.id}${versions} ${item.reason}${detail}`;
1013
+ }
1014
+
1015
+ /**
1016
+ * @param {string[]} args
1017
+ */
1018
+ async function runUpdateCommand(args) {
1019
+ const { flags, positional } = parseUpdateArgs(args);
1020
+ const plan = await planModuleUpdates({
1021
+ root: ROOT,
1022
+ startup: flags.startup,
1023
+ allowMajor: flags.allowMajor,
1024
+ ids: positional,
1025
+ });
1026
+ if (plan.configSkipped === "disabled") {
1027
+ console.log("[module-update] disabled");
1028
+ return;
1029
+ }
1030
+ if (plan.configSkipped === "check-off") {
1031
+ console.log("[module-update] check skipped");
1032
+ return;
1033
+ }
1034
+ const showAll = flags.check && !flags.startup;
1035
+ for (const item of plan.skipped) {
1036
+ if (!showAll && QUIET_SKIP_REASONS.has(item.reason)) continue;
1037
+ console.log(formatSkip(item));
1038
+ }
1039
+ for (const item of plan.upgrades) {
1040
+ console.log(` upgrade ${item.id} ${item.fromVersion || "?"} -> ${item.toVersion}`);
1041
+ }
1042
+ if (plan.upgrades.length === 0) console.log("[module-update] nothing to apply");
1043
+ const apply = flags.startup ? plan.applyOnStart : !flags.check;
1044
+ if (!apply || plan.upgrades.length === 0) return;
1045
+ if (!flags.yes && !flags.startup) {
1046
+ if (!process.stdin.isTTY) die("non-interactive update needs --yes");
1047
+ const ok = await new Promise((resolve) => {
1048
+ process.stdout.write("Apply module updates? [y/N] ");
1049
+ process.stdin.setEncoding("utf8");
1050
+ process.stdin.once("data", (buf) => {
1051
+ const answer = String(buf).trim().toLowerCase();
1052
+ resolve(answer === "y" || answer === "yes");
1053
+ });
1054
+ });
1055
+ if (!ok) {
1056
+ console.log("[module-update] cancelled");
1057
+ return;
1058
+ }
1059
+ }
1060
+ const result = await applyModuleUpgrades(plan.upgrades, {
1061
+ root: ROOT,
1062
+ failMode: plan.failMode,
1063
+ install: installPreservingLock,
1064
+ resync: resyncInstalledCatalog,
1065
+ });
1066
+ for (const item of result.applied) {
1067
+ console.log(`[module-update] applied ${item.id} ${item.fromVersion || "?"} -> ${item.toVersion}`);
1068
+ }
1069
+ for (const item of result.skipped) console.log(formatSkip(item));
1070
+ for (const item of result.failed) console.log(`[module-update] failed ${item.id}: ${item.message}`);
1071
+ if (result.failed.length > 0 && plan.failMode === "abort") process.exitCode = 1;
1072
+ }
1073
+
993
1074
  async function main() {
994
1075
  const [, , verb, ...rest] = process.argv;
995
1076
  if (!verb) {
@@ -1034,6 +1115,11 @@ async function main() {
1034
1115
  return;
1035
1116
  }
1036
1117
 
1118
+ if (verb === "update") {
1119
+ await runUpdateCommand(rest);
1120
+ return;
1121
+ }
1122
+
1037
1123
  if (verb !== "install") die(`unknown verb: ${verb}`);
1038
1124
 
1039
1125
  const { flags, positional } = parseArgs(rest);
@@ -1085,4 +1171,6 @@ async function main() {
1085
1171
  }
1086
1172
  }
1087
1173
 
1088
- main().catch((e) => die(e?.message ?? String(e)));
1174
+ if (isFetchModuleEntry()) {
1175
+ main().catch((e) => die(e?.message ?? String(e)));
1176
+ }
@@ -0,0 +1,83 @@
1
+ // @ts-check
2
+ /**
3
+ * configs/module-update.json
4
+ * 模块自动更新的全局策略。缺文件时写入内置默认值,已存在的文件不覆盖。
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { readJson, writeJson } from "./io.mjs";
9
+ import { ROOT } from "./paths.mjs";
10
+
11
+ /** 与 SDK configSchemaRef("module_update") 同一地址,供编辑器校验。 */
12
+ export const MODULE_UPDATE_SCHEMA =
13
+ "https://cdn.jsdelivr.net/gh/DogeLakeDev/ScriptsForMinecraftServer@main/modules/sdk/@sfmc-sdk/schemas/module_update.schema.json";
14
+
15
+ /**
16
+ * @typedef {{
17
+ * enabled: boolean,
18
+ * checkOnBdsStart: boolean,
19
+ * applyOnBdsStart: boolean,
20
+ * allowMajor: boolean,
21
+ * failMode: "continue" | "abort",
22
+ * distTag: string,
23
+ * }} ModuleUpdateConfig
24
+ */
25
+
26
+ /** 内置默认策略:官方索引模块可在开服前升级,major 默认跳过。 */
27
+ export const DEFAULT_MODULE_UPDATE_CONFIG = {
28
+ enabled: true,
29
+ checkOnBdsStart: true,
30
+ applyOnBdsStart: true,
31
+ allowMajor: false,
32
+ failMode: "continue",
33
+ distTag: "latest",
34
+ };
35
+
36
+ /**
37
+ * @param {string} [root]
38
+ */
39
+ export function moduleUpdateConfigPath(root = ROOT) {
40
+ return path.join(root, "configs", "module-update.json");
41
+ }
42
+
43
+ /**
44
+ * @param {unknown} raw
45
+ * @returns {ModuleUpdateConfig}
46
+ */
47
+ export function normalizeModuleUpdateConfig(raw) {
48
+ const src = raw && typeof raw === "object" ? /** @type {Record<string, unknown>} */ (raw) : {};
49
+ const failMode = src.failMode === "abort" ? "abort" : "continue";
50
+ const distTag = typeof src.distTag === "string" && src.distTag.trim() ? src.distTag.trim() : "latest";
51
+ return {
52
+ enabled: src.enabled !== false,
53
+ checkOnBdsStart: src.checkOnBdsStart !== false,
54
+ applyOnBdsStart: src.applyOnBdsStart !== false,
55
+ allowMajor: src.allowMajor === true,
56
+ failMode,
57
+ distTag,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * 确保策略文件存在。返回最终路径。
63
+ * @param {string} [root]
64
+ */
65
+ export function ensureModuleUpdateConfigFile(root = ROOT) {
66
+ const file = moduleUpdateConfigPath(root);
67
+ if (!fs.existsSync(file)) {
68
+ writeJson(file, {
69
+ $schema: MODULE_UPDATE_SCHEMA,
70
+ ...DEFAULT_MODULE_UPDATE_CONFIG,
71
+ });
72
+ }
73
+ return file;
74
+ }
75
+
76
+ /**
77
+ * @param {string} [root]
78
+ * @returns {ModuleUpdateConfig}
79
+ */
80
+ export function loadModuleUpdateConfig(root = ROOT) {
81
+ ensureModuleUpdateConfigFile(root);
82
+ return normalizeModuleUpdateConfig(readJson(moduleUpdateConfigPath(root), null));
83
+ }