diff --git a/bds-tools/pack-manager-lifecycle.test.mjs b/bds-tools/pack-manager-lifecycle.test.mjs
new file mode 100644
index 00000000..89d9a7c7
--- /dev/null
+++ b/bds-tools/pack-manager-lifecycle.test.mjs
@@ -0,0 +1,139 @@
+/**
+ * pack-manager SOLID 回归:modules-json / clear-rp / has-pack / read-manifest
+ */
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { describe, it, before, after } from "node:test";
+import { fileURLToPath } from "node:url";
+import { spawnSync } from "node:child_process";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const CLI = path.join(__dirname, "dist", "cli-pack-manager.js");
+
+function run(args) {
+ const r = spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8" });
+ return { status: r.status, out: r.stdout ?? "", err: r.stderr ?? "" };
+}
+
+describe("pack-manager CLI extensions", () => {
+ /** @type {string} */
+ let tmp;
+ before(() => {
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-pm-"));
+ });
+ after(() => {
+ fs.rmSync(tmp, { recursive: true, force: true });
+ });
+
+ it("assemble-rp --modules-json 只用显式 map(不扫整树)", async () => {
+ const { loadModuleResourcePackMap, assembleResourcePack } = await import("./dist/pack-manager.js");
+ const modA = path.join(tmp, "mod-a", "resource_pack");
+ const modB = path.join(tmp, "mod-b", "resource_pack");
+ fs.mkdirSync(modA, { recursive: true });
+ fs.mkdirSync(modB, { recursive: true });
+ fs.writeFileSync(path.join(modA, "a.txt"), "a");
+ fs.writeFileSync(path.join(modB, "b.txt"), "b");
+ const mapFile = path.join(tmp, "map.json");
+ /* 仅启用 mod-a */
+ fs.writeFileSync(mapFile, JSON.stringify({ "mod-a": modA }));
+ const map = loadModuleResourcePackMap(mapFile);
+ assert.deepEqual(Object.keys(map), ["mod-a"]);
+
+ const out = path.join(tmp, "rp-out");
+ await assembleResourcePack({
+ moduleResourceDirs: map,
+ outDir: out,
+ projectName: "test-rp",
+ uuid: "11111111-1111-1111-1111-111111111111",
+ version: [1, 0, 0],
+ });
+ assert.ok(fs.existsSync(path.join(out, "mod-a", "a.txt")));
+ assert.ok(!fs.existsSync(path.join(out, "mod-b")));
+
+ const cli = run([
+ "assemble-rp",
+ "--modules-json",
+ mapFile,
+ "--out",
+ path.join(tmp, "rp-cli"),
+ "--name",
+ "test-rp-cli",
+ "--uuid",
+ "22222222-2222-2222-2222-222222222222",
+ ]);
+ assert.equal(cli.status, 0, cli.err || cli.out);
+ });
+
+ it("deploy --clear-rp 删除世界内残留 RP 目录", async () => {
+ const { deployToBDS } = await import("./dist/pack-manager.js");
+ const bds = path.join(tmp, "bds");
+ const level = "Bedrock level";
+ const worlds = path.join(bds, "worlds", level);
+ const bpSrc = path.join(tmp, "bp-src");
+ const rpDst = path.join(worlds, "resource_packs", "sfmc-modules-rp");
+ fs.mkdirSync(bpSrc, { recursive: true });
+ fs.writeFileSync(path.join(bpSrc, "dummy.txt"), "bp");
+ fs.mkdirSync(rpDst, { recursive: true });
+ fs.writeFileSync(path.join(rpDst, "stale.txt"), "old");
+
+ await deployToBDS({
+ bdsRoot: bds,
+ levelName: level,
+ behaviorPackSrc: bpSrc,
+ bpName: "sfmc-modules",
+ rpName: "sfmc-modules-rp",
+ clearResourcePack: true,
+ });
+ assert.ok(!fs.existsSync(rpDst));
+ assert.ok(fs.existsSync(path.join(worlds, "behavior_packs", "sfmc-modules", "dummy.txt")));
+ });
+
+ it("read-manifest / has-pack CLI 契约", async () => {
+ const { assembleBehaviorPack, enablePackInWorld, worldPackListHas } = await import("./dist/pack-manager.js");
+ const bpOut = path.join(tmp, "bp-assembled");
+ const src = path.join(tmp, "bp-empty-src");
+ fs.mkdirSync(path.join(src, "scripts"), { recursive: true });
+ fs.writeFileSync(path.join(src, "scripts", "main.js"), "//\n");
+ const uuid = "33333333-3333-3333-3333-333333333333";
+ await assembleBehaviorPack({
+ srcDir: src,
+ outDir: bpOut,
+ projectName: "sfmc-modules",
+ uuid,
+ version: [1, 2, 3],
+ });
+
+ const rm = run(["read-manifest", "--pack-dir", bpOut]);
+ assert.equal(rm.status, 0, rm.err);
+ const header = JSON.parse(rm.out.trim());
+ assert.equal(header.uuid, uuid);
+ assert.deepEqual(header.version, [1, 2, 3]);
+
+ const worldsDir = path.join(tmp, "worlds-root");
+ await enablePackInWorld({
+ worldsDir,
+ levelName: "L1",
+ kind: "behavior",
+ packUuid: uuid,
+ version: [1, 2, 3],
+ });
+ assert.equal(worldPackListHas(worldsDir, "L1", "behavior", uuid), true);
+ assert.equal(worldPackListHas(worldsDir, "L1", "behavior", "00000000-0000-0000-0000-000000000000"), false);
+
+ const hp = run([
+ "has-pack",
+ "--worlds-dir",
+ worldsDir,
+ "--level",
+ "L1",
+ "--kind",
+ "behavior",
+ "--pack-id",
+ uuid,
+ ]);
+ assert.equal(hp.status, 0);
+ assert.equal(hp.out.trim(), "1");
+ });
+});
diff --git a/bds-tools/package.json b/bds-tools/package.json
index ec51949d..4a536358 100644
--- a/bds-tools/package.json
+++ b/bds-tools/package.json
@@ -41,7 +41,7 @@
"dev": "tsx src/dist/bds-manager.ts",
"typecheck": "tsc --noEmit",
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
- "test": "node --test is-main.test.mjs",
+ "test": "node --test is-main.test.mjs pack-manager-lifecycle.test.mjs",
"update": "node dist/check-update.js",
"update:check": "node dist/check-update.js --check-only",
"update:force": "node dist/check-update.js --force",
diff --git a/bds-tools/src/cli-pack-manager.ts b/bds-tools/src/cli-pack-manager.ts
index b9ca06f3..8d0f23ed 100644
--- a/bds-tools/src/cli-pack-manager.ts
+++ b/bds-tools/src/cli-pack-manager.ts
@@ -4,12 +4,14 @@
* so spawnService can call it as a sub-process.
*
* node bds-tools/dist/pack-manager.js assemble-bp --src
--out --name [--uuid ] [--module-uuid ] [--version 1,0,0] [--description "..."] [--icon ]
- * node bds-tools/dist/pack-manager.js assemble-rp --modules-dir --out --name [--uuid ] [--module-uuid ] [--version 1,0,0] [--description "..."]
- * node bds-tools/dist/pack-manager.js deploy --bds-root --level --bp-src [--rp-src ] --bp-name [--rp-name ]
+ * node bds-tools/dist/pack-manager.js assemble-rp (--modules-dir |--modules-json ) --out --name [--uuid ] [--module-uuid ] [--version 1,0,0] [--description "..."]
+ * node bds-tools/dist/pack-manager.js deploy --bds-root --level --bp-src [--rp-src ] --bp-name [--rp-name ] [--clear-rp]
* node bds-tools/dist/pack-manager.js enable-pack --worlds-dir --level --kind behavior|resource --pack-id --version 1,0,0
* node bds-tools/dist/pack-manager.js disable-pack --worlds-dir --level --kind behavior|resource --pack-id
* node bds-tools/dist/pack-manager.js ensure-permission --bds-root --pack-id
* node bds-tools/dist/pack-manager.js read-level --bds-root
+ * node bds-tools/dist/pack-manager.js read-manifest --pack-dir
+ * node bds-tools/dist/pack-manager.js has-pack --worlds-dir --level --kind behavior|resource --pack-id
*
* The pure-function API lives in pack-manager.ts. This CLI exists so the
* SEA-launched child process doesn't have to deal with module resolution —
@@ -33,7 +35,13 @@ function parseArgs(argv: string[]): { [k: string]: string | undefined } {
if (a && a.startsWith("--")) {
const key = a.slice(2);
const next = argv[++i];
- out[key] = next;
+ /* 布尔旗标(无值或下一参也是 --xxx) */
+ if (next === undefined || next.startsWith("--")) {
+ out[key] = "1";
+ if (next !== undefined) i--;
+ } else {
+ out[key] = next;
+ }
}
}
return out;
@@ -84,10 +92,16 @@ async function main(): Promise {
return;
}
case "assemble-rp": {
- const modulesDir = need(args, "modules-dir");
const out = need(args, "out");
const name = need(args, "name");
- const map = mod.scanModuleResourcePacks(path.resolve(modulesDir));
+ /* OCP:显式 map 优先;否则回退扫描 --modules-dir */
+ let map: Record;
+ if (args["modules-json"]) {
+ map = mod.loadModuleResourcePackMap(path.resolve(args["modules-json"]));
+ } else {
+ const modulesDir = need(args, "modules-dir");
+ map = mod.scanModuleResourcePacks(path.resolve(modulesDir));
+ }
await mod.assembleResourcePack({
moduleResourceDirs: map,
outDir: path.resolve(out),
@@ -107,6 +121,7 @@ async function main(): Promise {
const bpName = need(args, "bp-name");
const rpSrc = args["rp-src"];
const rpName = args["rp-name"];
+ const clearRp = args["clear-rp"] === "1" || args["clear-rp"] === "true";
await mod.deployToBDS({
bdsRoot: path.resolve(bdsRoot),
levelName: level,
@@ -114,6 +129,7 @@ async function main(): Promise {
...(rpSrc ? { resourcePackSrc: path.resolve(rpSrc) } : {}),
bpName,
...(rpName ? { rpName } : {}),
+ ...(clearRp ? { clearResourcePack: true } : {}),
});
process.stdout.write(`[pack-manager] deployed to ${path.join(bdsRoot, "worlds", level)}\n`);
return;
@@ -165,6 +181,25 @@ async function main(): Promise {
process.stdout.write(`${name}\n`);
return;
}
+ case "read-manifest": {
+ const packDir = need(args, "pack-dir");
+ const header = mod.readPackManifestHeader(path.resolve(packDir));
+ if (!header) {
+ process.stdout.write("null\n");
+ return;
+ }
+ process.stdout.write(`${JSON.stringify(header)}\n`);
+ return;
+ }
+ case "has-pack": {
+ const worldsDir = need(args, "worlds-dir");
+ const level = need(args, "level");
+ const kind = need(args, "kind") as "behavior" | "resource";
+ const packId = need(args, "pack-id");
+ const ok = mod.worldPackListHas(path.resolve(worldsDir), level, kind, packId);
+ process.stdout.write(ok ? "1\n" : "0\n");
+ return;
+ }
default:
die(`unknown verb: ${verb}`);
}
@@ -173,4 +208,4 @@ async function main(): Promise {
main().catch((err: unknown) => {
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
die(msg);
-});
\ No newline at end of file
+});
diff --git a/bds-tools/src/pack-manager.ts b/bds-tools/src/pack-manager.ts
index 150b7661..f4ee0d10 100644
--- a/bds-tools/src/pack-manager.ts
+++ b/bds-tools/src/pack-manager.ts
@@ -80,6 +80,11 @@ export interface DeployOpts {
bpName: string;
/** RP 目标目录名 (worlds//resource_packs/);省略时复用 bpName + '-rp' */
rpName?: string;
+ /**
+ * 无 resourcePackSrc 时是否删除世界内已有 RP 目录。
+ * 用于模块不再提供 RP 时清理聚合包残留(默认 false,避免误删未声明的 RP)。
+ */
+ clearResourcePack?: boolean;
}
export interface DeployResult {
@@ -211,6 +216,9 @@ export async function deployToBDS(opts: DeployOpts): Promise {
await fs.promises.rm(rpDst, { recursive: true, force: true });
await copyDirAsync(opts.resourcePackSrc, rpDst);
rpDir = rpDst;
+ } else if (opts.clearResourcePack) {
+ /* 调用方显式声明不再部署 RP — 清掉聚合 RP 目录残留 */
+ await fs.promises.rm(rpDst, { recursive: true, force: true });
}
return { bpDir: bpDst, rpDir };
}
@@ -264,7 +272,6 @@ export async function writePermissionsJson(bpDir: string): Promise {
const tmp = path.join(bpDir, `.permissions.${process.pid}.tmp`);
await fs.promises.writeFile(tmp, JSON.stringify(payload, null, 2) + "\n", "utf8");
await fs.promises.rename(tmp, path.join(bpDir, "permissions.json"));
- void copyDirAsync;
}
/**
@@ -412,4 +419,46 @@ export function scanModuleResourcePacks(modulesDir: string): Record {
+ const raw = JSON.parse(fs.readFileSync(jsonPath, "utf8")) as unknown;
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
+ throw new Error(`modules-json must be an object: ${jsonPath}`);
+ }
+ const out: Record = {};
+ for (const [id, dir] of Object.entries(raw as Record)) {
+ if (typeof id !== "string" || !id || typeof dir !== "string" || !dir) continue;
+ const abs = path.resolve(dir);
+ if (!fs.existsSync(abs)) {
+ throw new Error(`modules-json entry missing: ${id} → ${abs}`);
+ }
+ out[id] = abs;
+ }
+ return out;
+}
+
+/** 世界 enable-list 是否已含指定 pack_id(只读,供 preflight 复用)。 */
+export function worldPackListHas(
+ worldsDir: string,
+ levelName: string,
+ kind: "behavior" | "resource",
+ packUuid: string
+): boolean {
+ const file = path.join(
+ worldsDir,
+ levelName,
+ kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
+ );
+ if (!fs.existsSync(file)) return false;
+ try {
+ const arr = JSON.parse(fs.readFileSync(file, "utf8")) as Array<{ pack_id?: string }>;
+ return Array.isArray(arr) && arr.some((e) => e.pack_id === packUuid);
+ } catch {
+ return false;
+ }
}
\ No newline at end of file
diff --git a/sfmc/src/pack-lifecycle.ts b/sfmc/src/pack-lifecycle.ts
index 6dfa552c..161e1308 100644
--- a/sfmc/src/pack-lifecycle.ts
+++ b/sfmc/src/pack-lifecycle.ts
@@ -96,26 +96,24 @@ function catalogPath(): string {
return modulePath(path.join(ROOT, "modules"), "catalog.json");
}
-/** 读 BDS 路径与 level-name */
+function spawnPackManager(args: string[]): { ok: boolean; out: string; err: string } {
+ const proc = spawnServiceSync("pack-manager", args, { encoding: "utf8" });
+ return {
+ ok: proc.status === 0,
+ out: (proc.stdout ?? "").toString(),
+ err: (proc.stderr ?? "").toString(),
+ };
+}
+
+/** 读 BDS 路径与 level-name(level 权威来源:pack-manager read-level,DRY/DIP) */
export function resolveBdsContext(): { bdsRoot: string; levelName: string } {
const cfg = (readJson(configPath(ROOT, "bds_updater.json")) ?? {}) as BdsUpdaterConfig;
const bdsRoot = cfg.bds_path;
if (!bdsRoot) {
throw new Error("bds_path not configured. Run `sfmc init` first.");
}
- let levelName = "Bedrock level";
- try {
- const text = readFileSync(path.join(bdsRoot, "server.properties"), "utf8");
- for (const line of text.split(/\r?\n/)) {
- const m = /^\s*level-name\s*=\s*(.+?)\s*$/.exec(line);
- if (m?.[1]) {
- levelName = m[1];
- break;
- }
- }
- } catch {
- /* use default */
- }
+ const r = spawnPackManager(["read-level", "--bds-root", bdsRoot]);
+ const levelName = r.ok && r.out.trim() ? r.out.trim() : "Bedrock level";
return { bdsRoot, levelName };
}
@@ -131,22 +129,17 @@ export function deployedCatalogPath(bdsRoot: string, levelName: string): string
return path.join(deployedBpDir(bdsRoot, levelName), DEPLOY_CATALOG_NAME);
}
+/** 读 BP/RP manifest header — 委托 pack-manager(与 readPackManifestHeader 单一权威) */
function readManifestHeader(
packDir: string
): { uuid: string; version: [number, number, number]; moduleUuid?: string } | null {
- const file = path.join(packDir, "manifest.json");
- if (!existsSync(file)) return null;
+ if (!existsSync(path.join(packDir, "manifest.json"))) return null;
+ const r = spawnPackManager(["read-manifest", "--pack-dir", packDir]);
+ if (!r.ok) return null;
+ const text = r.out.trim();
+ if (!text || text === "null") return null;
try {
- const raw = JSON.parse(readFileSync(file, "utf8")) as {
- header?: { uuid?: string; version?: number[] };
- modules?: Array<{ uuid?: string }>;
- };
- const uuid = raw.header?.uuid;
- const ver = raw.header?.version;
- if (typeof uuid !== "string" || !Array.isArray(ver) || ver.length < 3) return null;
- const version: [number, number, number] = [Number(ver[0]), Number(ver[1]), Number(ver[2])];
- const moduleUuid = raw.modules?.[0]?.uuid;
- return { uuid, version, ...(typeof moduleUuid === "string" ? { moduleUuid } : {}) };
+ return JSON.parse(text) as { uuid: string; version: [number, number, number]; moduleUuid?: string };
} catch {
return null;
}
@@ -184,8 +177,8 @@ export async function scanLocalModules(): Promise<
for (const m of cat.modules) {
const id = typeof m.id === "string" ? m.id : "";
if (!id) continue;
- /* enabledByDefault 缺省按 false(与 feature 模块一致) */
- defaults.set(id, typeof m.enabledByDefault === "boolean" ? m.enabledByDefault : false);
+ /* 与 db-server 契约一致:enabledByDefault !== false(缺省 true) — LSP */
+ defaults.set(id, m.enabledByDefault !== false);
}
}
@@ -346,29 +339,20 @@ export function catalogsEqual(a: DeployCatalog, b: DeployCatalog): boolean {
return true;
}
+/** 世界 enable-list 查询 — 委托 pack-manager has-pack(DRY) */
function worldPackListHas(bdsRoot: string, levelName: string, kind: "behavior" | "resource", uuid: string): boolean {
- const file = path.join(
- bdsRoot,
- "worlds",
+ const r = spawnPackManager([
+ "has-pack",
+ "--worlds-dir",
+ path.join(bdsRoot, "worlds"),
+ "--level",
levelName,
- kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
- );
- if (!existsSync(file)) return false;
- try {
- const arr = JSON.parse(readFileSync(file, "utf8")) as Array<{ pack_id?: string }>;
- return Array.isArray(arr) && arr.some((e) => e.pack_id === uuid);
- } catch {
- return false;
- }
-}
-
-function spawnPackManager(args: string[]): { ok: boolean; out: string; err: string } {
- const proc = spawnServiceSync("pack-manager", args, { encoding: "utf8" });
- return {
- ok: proc.status === 0,
- out: (proc.stdout ?? "").toString(),
- err: (proc.stderr ?? "").toString(),
- };
+ "--kind",
+ kind,
+ "--pack-id",
+ uuid,
+ ]);
+ return r.ok && r.out.trim() === "1";
}
/** esbuild 聚合启用模块 → assemble BP + RP(若有) */
@@ -377,8 +361,16 @@ export async function buildPacks(desired?: DeployCatalog): Promise m.enabled && m.entryPath).map((m) => m.entryPath!);
+ const rpDirs: Record = {};
+ for (const m of mods) {
+ if (!m.enabled || !m.hasResourcePack) continue;
+ rpDirs[m.folderId] = path.join(packagesDir(), m.folderId, "resource_pack");
+ }
+
const { build } = await import("esbuild");
- const entries = await listEnabledSapiEntries();
const outFile = path.join(bpSrc(), "scripts", "main.js");
await fs.mkdir(path.dirname(outFile), { recursive: true });
if (entries.length === 0) {
@@ -419,28 +411,14 @@ export async function buildPacks(desired?: DeployCatalog): Promise 0 && catalog.rpUuid) {
- /* assemble-rp CLI scans modules-dir; filter by writing only enabled via temp is heavy —
- * pass packages dir and rely on enabled filter by assembling via inline map:
- * CLI only supports --modules-dir full scan. Filter: call assemble with all scanned,
- * but we need only enabled. Use --modules-dir packages and accept scan of all RPs
- * OR write a temp modules mirror. Simpler: spawn with packages dir but pre-filter
- * by temporarily not using CLI filter — extend CLI later.
- * Workaround: assemble-rp scans all; we only want enabled. Copy enabled RPs to temp. */
- const tmpRpModules = path.join(buildRoot(), "_rp-modules-tmp");
- await fs.rm(tmpRpModules, { recursive: true, force: true });
- await fs.mkdir(tmpRpModules, { recursive: true });
- for (const [id, rpDir] of Object.entries(rpDirs)) {
- const dst = path.join(tmpRpModules, id, "resource_pack");
- await fs.mkdir(path.dirname(dst), { recursive: true });
- /* 用 pack-manager copy:直接递归复制 */
- await copyDirRecursive(rpDir, dst);
- }
+ /* OCP:显式 --modules-json 交给 pack-manager,不再临时镜像整树 */
+ const mapFile = path.join(buildRoot(), "_rp-modules-map.json");
+ writeJson(mapFile, rpDirs);
const rpArgs = [
"assemble-rp",
- "--modules-dir",
- tmpRpModules,
+ "--modules-json",
+ mapFile,
"--out",
rpOut(),
"--name",
@@ -454,7 +432,7 @@ export async function buildPacks(desired?: DeployCatalog): Promise {
- await fs.mkdir(dst, { recursive: true });
- for (const e of await fs.readdir(src, { withFileTypes: true })) {
- const s = path.join(src, e.name);
- const d = path.join(dst, e.name);
- if (e.isDirectory()) await copyDirRecursive(s, d);
- else await fs.copyFile(s, d);
- }
-}
-
/** 部署到世界 + enable 清单 + Script API permission */
export async function deployPacks(catalog: DeployCatalog): Promise {
const { bdsRoot, levelName } = resolveBdsContext();
@@ -488,6 +456,7 @@ export async function deployPacks(catalog: DeployCatalog): Promise {
throw new Error(`BP not built at ${bpOut()}. Run build first.`);
}
+ const previous = readDeployedCatalog(bdsRoot, levelName);
const deployArgs = [
"deploy",
"--bds-root",
@@ -498,9 +467,14 @@ export async function deployPacks(catalog: DeployCatalog): Promise {
bpOut(),
"--bp-name",
BP_NAME,
+ "--rp-name",
+ RP_NAME,
];
if (catalog.rpUuid && existsSync(rpOut())) {
- deployArgs.push("--rp-src", rpOut(), "--rp-name", RP_NAME);
+ deployArgs.push("--rp-src", rpOut());
+ } else {
+ /* 不再需要 RP:清世界内聚合 RP 目录 */
+ deployArgs.push("--clear-rp");
}
const dep = spawnPackManager(deployArgs);
if (!dep.ok) throw new Error(`deploy failed: ${dep.err || dep.out}`);
@@ -544,6 +518,46 @@ export async function deployPacks(catalog: DeployCatalog): Promise {
packLog(`enabled resource pack ${catalog.rpUuid} in world list`, "success");
}
+ /* 卸掉过期 RP:UUID 轮换或模块不再提供 RP(BLOCKER 修复) */
+ if (previous?.rpUuid && previous.rpUuid !== (catalog.rpUuid ?? null)) {
+ const dis = spawnPackManager([
+ "disable-pack",
+ "--worlds-dir",
+ worldsDir,
+ "--level",
+ levelName,
+ "--kind",
+ "resource",
+ "--pack-id",
+ previous.rpUuid,
+ ]);
+ if (!dis.ok) {
+ packLog(`disable stale RP ${previous.rpUuid} failed: ${dis.err || dis.out}`, "warn");
+ } else {
+ packLog(`disabled stale resource pack ${previous.rpUuid}`, "success");
+ }
+ }
+
+ /* 若 BP uuid 轮换,卸掉旧 BP 清单项 */
+ if (previous?.bpUuid && previous.bpUuid !== catalog.bpUuid) {
+ const disBp = spawnPackManager([
+ "disable-pack",
+ "--worlds-dir",
+ worldsDir,
+ "--level",
+ levelName,
+ "--kind",
+ "behavior",
+ "--pack-id",
+ previous.bpUuid,
+ ]);
+ if (!disBp.ok) {
+ packLog(`disable stale BP ${previous.bpUuid} failed: ${disBp.err || disBp.out}`, "warn");
+ } else {
+ packLog(`disabled stale behavior pack ${previous.bpUuid}`, "success");
+ }
+ }
+
const perm = spawnPackManager([
"ensure-permission",
"--bds-root",