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
22 changes: 22 additions & 0 deletions bds-tools/pack-manager-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ describe("pack-manager CLI extensions", () => {
assert.deepEqual(readWorldPackList(worldsDir, "L1", "behavior"), listed);
});

it("readWorldPackListResult 区分缺失与 JSON 损坏(doctor parseFail)", async () => {
const { readWorldPackListResult } = await import("./dist/pack-manager.js");
const worldsDir = path.join(tmp, "worlds-parse");
const levelDir = path.join(worldsDir, "Lbad");
fs.mkdirSync(levelDir, { recursive: true });

const missing = readWorldPackListResult(worldsDir, "Lbad", "behavior");
assert.deepEqual(missing.entries, []);
assert.equal(missing.parseFailedFile, undefined);

const badFile = path.join(levelDir, "world_behavior_packs.json");
fs.writeFileSync(badFile, "{not-json", "utf8");
const bad = readWorldPackListResult(worldsDir, "Lbad", "behavior");
assert.deepEqual(bad.entries, []);
assert.equal(bad.parseFailedFile, badFile);

fs.writeFileSync(badFile, '{"not":"array"}', "utf8");
const notArr = readWorldPackListResult(worldsDir, "Lbad", "behavior");
assert.deepEqual(notArr.entries, []);
assert.equal(notArr.parseFailedFile, badFile);
});

it("无 deploy-catalog 时仍可凭磁盘 RP manifest 卸世界清单(BLOCKER 回归)", async () => {
const {
assembleResourcePack,
Expand Down
29 changes: 22 additions & 7 deletions bds-tools/src/pack-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,22 +437,28 @@ export function loadModuleResourcePackMap(jsonPath: string): Record<string, stri

/**
* 读取世界 enable-list(单一权威,供 has-pack / list-packs / 调用方复用 — DRY)。
* 文件缺失或损坏时返回 []
* 文件缺失 → entries=[];JSON 损坏 → entries=[] 且 parseFailedFile 有值(供 doctor 区分)
*/
export function readWorldPackList(
export type WorldPackListReadResult = {
entries: Array<{ pack_id: string; version: [number, number, number] }>;
/** 文件存在但解析失败时为该 JSON 绝对路径 */
parseFailedFile?: string;
};

export function readWorldPackListResult(
worldsDir: string,
levelName: string,
kind: "behavior" | "resource"
): Array<{ pack_id: string; version: [number, number, number] }> {
): WorldPackListReadResult {
const file = path.join(
worldsDir,
levelName,
kind === "behavior" ? "world_behavior_packs.json" : "world_resource_packs.json"
);
if (!fs.existsSync(file)) return [];
if (!fs.existsSync(file)) return { entries: [] };
try {
const arr = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
if (!Array.isArray(arr)) return [];
if (!Array.isArray(arr)) return { entries: [], parseFailedFile: file };
const out: Array<{ pack_id: string; version: [number, number, number] }> = [];
for (const e of arr) {
if (!e || typeof e !== "object") continue;
Expand All @@ -465,12 +471,21 @@ export function readWorldPackList(
: [1, 0, 0];
out.push({ pack_id: packId, version });
}
return out;
return { entries: out };
} catch {
return [];
return { entries: [], parseFailedFile: file };
}
}

/** 兼容读侧:只要 entries(损坏时与缺失同为 [])。详细结果见 readWorldPackListResult。 */
export function readWorldPackList(
worldsDir: string,
levelName: string,
kind: "behavior" | "resource"
): Array<{ pack_id: string; version: [number, number, number] }> {
return readWorldPackListResult(worldsDir, levelName, kind).entries;
}

/** 世界 enable-list 是否已含指定 pack_id(只读,供 preflight 复用)。 */
export function worldPackListHas(
worldsDir: string,
Expand Down
13 changes: 12 additions & 1 deletion bds-tools/src/world-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
enablePackInWorld,
readPackManifestHeader,
readWorldPackList,
readWorldPackListResult,
type WorldPackListReadResult,
} from "./pack-manager.js";
import { extractZipFileToDir } from "./zipx.js";

Expand Down Expand Up @@ -336,7 +338,16 @@ export function listWorldEnableEntries(
levelName: string,
kind: WorldPackKind
): Array<{ pack_id: string; version: [number, number, number] }> {
return readWorldPackList(path.join(bdsRoot, "worlds"), levelName, kind);
return listWorldEnableListResult(bdsRoot, levelName, kind).entries;
}

/** 含 parseFail 信号的 enable-list 快照(doctor 用;不暴露 JSON 路径构造细节) */
export function listWorldEnableListResult(
bdsRoot: string,
levelName: string,
kind: WorldPackKind
): WorldPackListReadResult {
return readWorldPackListResult(path.join(bdsRoot, "worlds"), levelName, kind);
}

export function findInstalledPackById(
Expand Down
2 changes: 1 addition & 1 deletion bds-tools/src/zipx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* zipx.ts — JSZip 安全解压(防 zip-slip / 绝对路径 / Windows `\`)
* 单一权威,供 world-packs / check-update 复用(DRY)。
* 单一权威,供 world-packs / check-update / wizard / fetch-module 复用(DRY)。
*/
import fs from "node:fs";
import path from "node:path";
Expand Down
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"@typescript-eslint/utils": "^8.65.0",
"esbuild": "^0.28.1",
"eslint": "^10.7.0",
"jszip": "^3.10.1",
"lodash": "^4.18.1",
"nodemon": "^3.1.14",
"npm-run-all2": "^9.0.2",
Expand Down
8 changes: 2 additions & 6 deletions sfmc/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,6 @@ export async function cmdStopAll(): Promise<string> {
return c.dim(t("svc.allStopped"));
}

/** 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志出现空白行(委托 bds-tools/taskbar) */
function stripOsc(s: string): string {
return stripTaskbarOsc(s);
}

/**
* BDS 更新:子进程始终 --no-start,由 sfmc 监督器接管启停与日志。
* (updater 内 detached 自启会导致 REPL 丢 PID / 无 stdout)
Expand All @@ -190,7 +185,8 @@ export async function cmdUpdate(args: string[] = []): Promise<string> {
});
let out = "";
const pushChunk = (raw: string, level: "info" | "error"): void => {
const s = stripOsc(raw);
// 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志空白行(权威:bds-tools/taskbar)
const s = stripTaskbarOsc(raw);
out += s;
for (const line of s
.split(/\r?\n/)
Expand Down
11 changes: 7 additions & 4 deletions sfmc/src/world-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
installPackDirectory,
isPackArchive,
listInstalledWorldPacks,
listWorldEnableEntries,
listWorldEnableListResult,
readPackManifestInfo,
worldPackParentDir,
type InstalledWorldPack,
Expand Down Expand Up @@ -454,10 +454,13 @@ async function cmdDoctor(): Promise<string> {
const issues: string[] = [];

for (const kind of ["behavior", "resource"] as const) {
// 经 listWorldEnableEntries → pack-manager.readWorldPackList(DRY/Demeter,不硬编码 JSON 文件名)
const entries = listWorldEnableEntries(bdsRoot, levelName, kind);
// 经 listWorldEnableListResult → pack-manager(DRY/Demeter;保留 parseFail 信号 — LSP)
const snap = listWorldEnableListResult(bdsRoot, levelName, kind);
if (snap.parseFailedFile) {
issues.push(t("packs.doctor.parseFail", { file: path.basename(snap.parseFailedFile) }));
}
const byUuid = new Map(packs.filter((p) => p.kind === kind).map((p) => [p.uuid, p]));
for (const e of entries) {
for (const e of snap.entries) {
const p = byUuid.get(e.pack_id);
if (!p) {
issues.push(t("packs.doctor.missingDir", { kind, uuid: e.pack_id }));
Expand Down
21 changes: 4 additions & 17 deletions tools/fetch-module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { pipeline } from "node:stream/promises";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { extractZipFileToDir } from "@sfmc-bds/bds-tools/zipx";
import { upsertCatalogEntry, removeCatalogEntry } from "./lib/catalog.mjs";
import { setModuleLockEnabled, removeModuleLock } from "./lib/lock.mjs";
import { PACKAGES_DIR, ROOT } from "./lib/paths.mjs";
Expand Down Expand Up @@ -411,24 +412,10 @@ async function listGithub(source) {
}
}

/** 解压模块包 — 委托 bds-tools/zipx(DRY;防 zip-slip / `\` / 绝对路径) */
async function unzip(zipPath, dstDir) {
const JSZip = (await import("jszip")).default;
const data = await fsp.readFile(zipPath);
const zip = await JSZip.loadAsync(data);
for (const e of Object.values(zip.files)) {
// Windows zip 常带 `\`;必须归一成 `/`,否则 Linux 会写出字面量 `sapi\manifest.json`
const rel = String(e.name).replace(/\\/g, "/").replace(/^\/+/, "");
if (!rel || rel.includes("..")) continue;
const parts = rel.replace(/\/$/, "").split("/").filter(Boolean);
if (parts.length === 0) continue;
const out = path.join(dstDir, ...parts);
if (e.dir || rel.endsWith("/")) {
await fsp.mkdir(out, { recursive: true });
continue;
}
await fsp.mkdir(path.dirname(out), { recursive: true });
await fsp.writeFile(out, await e.async("nodebuffer"));
}
await fsp.mkdir(dstDir, { recursive: true });
await extractZipFileToDir(zipPath, dstDir);
}

async function copyDir(src, dst) {
Expand Down
3 changes: 3 additions & 0 deletions tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"scripts": {
"build": "node -e \"process.exit(0)\""
},
"dependencies": {
"@sfmc-bds/bds-tools": "^0.1.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
Expand Down
Loading