Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 84 additions & 2 deletions bds-tools/pack-manager-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ describe("pack-manager CLI extensions", () => {
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");
it("read-manifest / has-pack / list-packs CLI 契约", async () => {
const { assembleBehaviorPack, enablePackInWorld, worldPackListHas, readWorldPackList } =
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 });
Expand Down Expand Up @@ -135,5 +136,86 @@ describe("pack-manager CLI extensions", () => {
]);
assert.equal(hp.status, 0);
assert.equal(hp.out.trim(), "1");

const lp = run(["list-packs", "--worlds-dir", worldsDir, "--level", "L1", "--kind", "behavior"]);
assert.equal(lp.status, 0, lp.err);
const listed = JSON.parse(lp.out.trim());
assert.equal(listed.length, 1);
assert.equal(listed[0].pack_id, uuid);
assert.deepEqual(readWorldPackList(worldsDir, "L1", "behavior"), listed);
});

it("无 deploy-catalog 时仍可凭磁盘 RP manifest 卸世界清单(BLOCKER 回归)", async () => {
const {
assembleResourcePack,
deployToBDS,
enablePackInWorld,
disablePackInWorld,
readPackManifestHeader,
worldPackListHas,
} = await import("./dist/pack-manager.js");

const bds = path.join(tmp, "bds-stale");
const level = "Bedrock level";
const worlds = path.join(bds, "worlds", level);
const worldsDir = path.join(bds, "worlds");
const bpSrc = path.join(tmp, "bp-stale-src");
const rpMod = path.join(tmp, "mod-stale", "resource_pack");
fs.mkdirSync(path.join(bpSrc, "scripts"), { recursive: true });
fs.writeFileSync(path.join(bpSrc, "scripts", "main.js"), "//\n");
fs.mkdirSync(rpMod, { recursive: true });
fs.writeFileSync(path.join(rpMod, "x.txt"), "x");

const staleUuid = "44444444-4444-4444-4444-444444444444";
const rpAssembled = path.join(tmp, "rp-stale-assembled");
await assembleResourcePack({
moduleResourceDirs: { stale: rpMod },
outDir: rpAssembled,
projectName: "sfmc-modules-rp",
uuid: staleUuid,
version: [1, 0, 0],
});

await deployToBDS({
bdsRoot: bds,
levelName: level,
behaviorPackSrc: bpSrc,
resourcePackSrc: rpAssembled,
bpName: "sfmc-modules",
rpName: "sfmc-modules-rp",
});
await enablePackInWorld({
worldsDir,
levelName: level,
kind: "resource",
packUuid: staleUuid,
version: [1, 0, 0],
});
assert.equal(worldPackListHas(worldsDir, level, "resource", staleUuid), true);

/* 模拟 catalog 缺失:只留磁盘 RP,lifecycle 应在 clear 前读 manifest */
const rpDst = path.join(worlds, "resource_packs", "sfmc-modules-rp");
const header = readPackManifestHeader(rpDst);
assert.equal(header?.uuid, staleUuid);
assert.ok(header);

await deployToBDS({
bdsRoot: bds,
levelName: level,
behaviorPackSrc: bpSrc,
bpName: "sfmc-modules",
rpName: "sfmc-modules-rp",
clearResourcePack: true,
});
assert.ok(!fs.existsSync(rpDst));

await disablePackInWorld({
worldsDir,
levelName: level,
kind: "resource",
packUuid: header.uuid,
version: [1, 0, 0],
});
assert.equal(worldPackListHas(worldsDir, level, "resource", staleUuid), false);
});
});
10 changes: 10 additions & 0 deletions bds-tools/src/cli-pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* node bds-tools/dist/pack-manager.js read-level --bds-root <dir>
* node bds-tools/dist/pack-manager.js read-manifest --pack-dir <dir>
* node bds-tools/dist/pack-manager.js has-pack --worlds-dir <dir> --level <name> --kind behavior|resource --pack-id <uuid>
* node bds-tools/dist/pack-manager.js list-packs --worlds-dir <dir> --level <name> --kind behavior|resource
*
* 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 —
Expand Down Expand Up @@ -200,6 +201,15 @@ async function main(): Promise<void> {
process.stdout.write(ok ? "1\n" : "0\n");
return;
}
case "list-packs": {
/* 权威世界清单读取 — 调用方勿再直接解析 world_*_packs.json(Demeter/DRY) */
const worldsDir = need(args, "worlds-dir");
const level = need(args, "level");
const kind = need(args, "kind") as "behavior" | "resource";
const list = mod.readWorldPackList(path.resolve(worldsDir), level, kind);
process.stdout.write(`${JSON.stringify(list)}\n`);
return;
}
default:
die(`unknown verb: ${verb}`);
}
Expand Down
54 changes: 36 additions & 18 deletions bds-tools/src/pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,15 +302,8 @@ async function editWorldPackList(
opts.levelName,
opts.kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
);
let entries: WorldPackEntry[] = [];
try {
const raw = await fs.promises.readFile(file, "utf8");
const parsed: unknown = JSON.parse(raw);
if (Array.isArray(parsed)) entries = parsed as WorldPackEntry[];
} catch {
/* missing / corrupt → start with [] */
entries = [];
}
/* 与 readWorldPackList 同源解析,再写回(DRY);异步写路径保留原语义 */
let entries: WorldPackEntry[] = readWorldPackList(opts.worldsDir, opts.levelName, opts.kind);
const idx = entries.findIndex((e) => e.pack_id === opts.packUuid);
if (mode === "enable") {
const next: WorldPackEntry = { pack_id: opts.packUuid, version: opts.version };
Expand Down Expand Up @@ -442,23 +435,48 @@ export function loadModuleResourcePackMap(jsonPath: string): Record<string, stri
return out;
}

/** 世界 enable-list 是否已含指定 pack_id(只读,供 preflight 复用)。 */
export function worldPackListHas(
/**
* 读取世界 enable-list(单一权威,供 has-pack / list-packs / 调用方复用 — DRY)。
* 文件缺失或损坏时返回 []。
*/
export function readWorldPackList(
worldsDir: string,
levelName: string,
kind: "behavior" | "resource",
packUuid: string
): boolean {
kind: "behavior" | "resource"
): Array<{ pack_id: string; version: [number, number, number] }> {
const file = path.join(
worldsDir,
levelName,
kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
);
if (!fs.existsSync(file)) return false;
if (!fs.existsSync(file)) return [];
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);
const arr = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
if (!Array.isArray(arr)) return [];
const out: Array<{ pack_id: string; version: [number, number, number] }> = [];
for (const e of arr) {
if (!e || typeof e !== "object") continue;
const packId = (e as { pack_id?: unknown }).pack_id;
const ver = (e as { version?: unknown }).version;
if (typeof packId !== "string" || !packId) continue;
const version: [number, number, number] =
Array.isArray(ver) && ver.length >= 3
? [Number(ver[0]), Number(ver[1]), Number(ver[2])]
: [1, 0, 0];
out.push({ pack_id: packId, version });
}
return out;
} catch {
return false;
return [];
}
}

/** 世界 enable-list 是否已含指定 pack_id(只读,供 preflight 复用)。 */
export function worldPackListHas(
worldsDir: string,
levelName: string,
kind: "behavior" | "resource",
packUuid: string
): boolean {
return readWorldPackList(worldsDir, levelName, kind).some((e) => e.pack_id === packUuid);
}
Loading
Loading