@sfmc-bds/cli 0.2.4 → 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.
- package/dist/command-surface.d.ts.map +1 -1
- package/dist/command-surface.js +5 -1
- package/dist/command-surface.js.map +2 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +20 -79
- package/dist/commands.js.map +2 -2
- package/dist/help-text.d.ts.map +1 -1
- package/dist/help-text.js +2 -0
- package/dist/help-text.js.map +2 -2
- package/dist/i18n/locales/en.d.ts +33 -0
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +33 -0
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.js +33 -0
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/module-commands.d.ts.map +1 -1
- package/dist/module-commands.js +8 -0
- package/dist/module-commands.js.map +2 -2
- package/dist/module-update/index.d.ts +18 -0
- package/dist/module-update/index.d.ts.map +1 -0
- package/dist/module-update/index.js +223 -0
- package/dist/module-update/index.js.map +7 -0
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +11 -0
- package/dist/repl.js.map +2 -2
- package/dist/services.d.ts +29 -1
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +87 -3
- package/dist/services.js.map +2 -2
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +2 -0
- package/dist/wizard.js.map +2 -2
- package/package.json +1 -1
- package/scripts/module-install/fetch-module.mjs +164 -76
- package/scripts/module-install/lib/module-update-config.mjs +83 -0
- package/scripts/module-install/lib/module-update.mjs +507 -0
- package/scripts/module-install/lib/pins.mjs +436 -0
- package/scripts/module-install/lib/registry-cache.mjs +83 -0
- package/scripts/module-install/lib/semver.mjs +124 -0
- 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 {
|
|
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
|
-
*
|
|
58
|
-
* @typedef {Record<string, RegistryEntry>} RegistryIndex
|
|
59
|
-
* @typedef {{ fetchedAt: number, index: RegistryIndex }} RegistryCache
|
|
54
|
+
* 仅在直接执行本文件时跑 main。被 mod update 动态 import 时不要再解析命令行。
|
|
60
55
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
function isFetchModuleEntry() {
|
|
57
|
+
const entry = process.argv[1];
|
|
58
|
+
if (!entry) return false;
|
|
63
59
|
try {
|
|
64
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
|
989
|
-
|
|
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
|
-
|
|
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
|
+
}
|