@sfmc-bds/bds-tools 0.2.0-beta.0 → 0.2.0-beta.10

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 (108) hide show
  1. package/LICENSE +663 -0
  2. package/dist/bds-manager.d.ts +16 -6
  3. package/dist/bds-manager.d.ts.map +1 -1
  4. package/dist/bds-manager.js +40 -65
  5. package/dist/bds-manager.js.map +2 -2
  6. package/dist/check-update.d.ts.map +1 -1
  7. package/dist/check-update.js +15 -17
  8. package/dist/check-update.js.map +2 -2
  9. package/dist/cli-pack-manager.js +7 -3
  10. package/dist/cli-pack-manager.js.map +2 -2
  11. package/dist/fsx.d.ts +6 -1
  12. package/dist/fsx.d.ts.map +1 -1
  13. package/dist/fsx.js +9 -1
  14. package/dist/fsx.js.map +2 -2
  15. package/dist/host-platform.d.ts +25 -0
  16. package/dist/host-platform.d.ts.map +1 -0
  17. package/dist/host-platform.js +50 -0
  18. package/dist/host-platform.js.map +7 -0
  19. package/dist/http.d.ts +49 -11
  20. package/dist/http.d.ts.map +1 -1
  21. package/dist/http.js.map +2 -2
  22. package/dist/log.d.ts +2 -3
  23. package/dist/log.d.ts.map +1 -1
  24. package/dist/log.js +0 -1
  25. package/dist/log.js.map +2 -2
  26. package/dist/pack-manager-lifecycle.test.js +252 -0
  27. package/dist/pack-manager-lifecycle.test.js.map +7 -0
  28. package/dist/pack-manager.d.ts +24 -29
  29. package/dist/pack-manager.d.ts.map +1 -1
  30. package/dist/pack-manager.js +11 -4
  31. package/dist/pack-manager.js.map +2 -2
  32. package/dist/pack-update/bindings.d.ts +16 -0
  33. package/dist/pack-update/bindings.d.ts.map +1 -0
  34. package/dist/pack-update/bindings.js +69 -0
  35. package/dist/pack-update/bindings.js.map +7 -0
  36. package/dist/pack-update/config.d.ts +16 -0
  37. package/dist/pack-update/config.d.ts.map +1 -0
  38. package/dist/pack-update/config.js +146 -0
  39. package/dist/pack-update/config.js.map +7 -0
  40. package/dist/pack-update/config.test.js +75 -0
  41. package/dist/pack-update/config.test.js.map +7 -0
  42. package/dist/pack-update/deps.d.ts +28 -0
  43. package/dist/pack-update/deps.d.ts.map +1 -0
  44. package/dist/pack-update/deps.js +20 -0
  45. package/dist/pack-update/deps.js.map +7 -0
  46. package/dist/pack-update/index.d.ts +55 -0
  47. package/dist/pack-update/index.d.ts.map +1 -0
  48. package/dist/pack-update/index.js +72 -0
  49. package/dist/pack-update/index.js.map +7 -0
  50. package/dist/pack-update/providers/curseforge.d.ts +23 -0
  51. package/dist/pack-update/providers/curseforge.d.ts.map +1 -0
  52. package/dist/pack-update/providers/curseforge.js +229 -0
  53. package/dist/pack-update/providers/curseforge.js.map +7 -0
  54. package/dist/pack-update/providers/index.d.ts +16 -0
  55. package/dist/pack-update/providers/index.d.ts.map +1 -0
  56. package/dist/pack-update/providers/index.js +37 -0
  57. package/dist/pack-update/providers/index.js.map +7 -0
  58. package/dist/pack-update/service.d.ts +28 -0
  59. package/dist/pack-update/service.d.ts.map +1 -0
  60. package/dist/pack-update/service.js +570 -0
  61. package/dist/pack-update/service.js.map +7 -0
  62. package/dist/pack-update/types.d.ts +126 -0
  63. package/dist/pack-update/types.d.ts.map +1 -0
  64. package/dist/pack-update/types.js +8 -0
  65. package/dist/pack-update/types.js.map +7 -0
  66. package/dist/pack-update/version-policy.d.ts +81 -0
  67. package/dist/pack-update/version-policy.d.ts.map +1 -0
  68. package/dist/pack-update/version-policy.js +210 -0
  69. package/dist/pack-update/version-policy.js.map +7 -0
  70. package/dist/pack-update/version-policy.test.js +137 -0
  71. package/dist/pack-update/version-policy.test.js.map +7 -0
  72. package/dist/paths.d.ts +1 -1
  73. package/dist/paths.d.ts.map +1 -1
  74. package/dist/paths.js +3 -5
  75. package/dist/paths.js.map +2 -2
  76. package/dist/process-probe.d.ts +34 -0
  77. package/dist/process-probe.d.ts.map +1 -0
  78. package/dist/process-probe.js +152 -0
  79. package/dist/process-probe.js.map +7 -0
  80. package/dist/process-probe.test.js +76 -0
  81. package/dist/process-probe.test.js.map +7 -0
  82. package/dist/qq-events-notify.d.ts +16 -0
  83. package/dist/qq-events-notify.d.ts.map +1 -0
  84. package/dist/qq-events-notify.js +70 -0
  85. package/dist/qq-events-notify.js.map +7 -0
  86. package/dist/qqutil.d.ts +10 -5
  87. package/dist/qqutil.d.ts.map +1 -1
  88. package/dist/qqutil.js +49 -8
  89. package/dist/qqutil.js.map +2 -2
  90. package/dist/update-result.d.ts +8 -2
  91. package/dist/update-result.d.ts.map +1 -1
  92. package/dist/update-result.js +7 -5
  93. package/dist/update-result.js.map +2 -2
  94. package/dist/upstream.d.ts +18 -9
  95. package/dist/upstream.d.ts.map +1 -1
  96. package/dist/upstream.js +8 -7
  97. package/dist/upstream.js.map +2 -2
  98. package/dist/version.js +2 -2
  99. package/dist/version.js.map +2 -2
  100. package/dist/world-packs.d.ts +158 -5
  101. package/dist/world-packs.d.ts.map +1 -1
  102. package/dist/world-packs.js +294 -48
  103. package/dist/world-packs.js.map +2 -2
  104. package/dist/world-packs.test.js +676 -0
  105. package/dist/world-packs.test.js.map +7 -0
  106. package/dist/zipx.test.js +36 -0
  107. package/dist/zipx.test.js.map +7 -0
  108. package/package.json +43 -39
@@ -0,0 +1,676 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { after, before, describe, it } from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawnSync } from "node:child_process";
8
+ const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist", "cli-pack-manager.js");
9
+ function run(args) {
10
+ const r = spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8" });
11
+ return { status: r.status, out: r.stdout ?? "", err: r.stderr ?? "" };
12
+ }
13
+ function writeManifest(dir, { name, uuid, version, type }) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ fs.writeFileSync(
16
+ path.join(dir, "manifest.json"),
17
+ JSON.stringify(
18
+ {
19
+ format_version: 2,
20
+ header: { name, uuid, version, description: "test" },
21
+ modules: [{ type, uuid: cryptoRandom(), version }]
22
+ },
23
+ null,
24
+ 2
25
+ )
26
+ );
27
+ }
28
+ function cryptoRandom() {
29
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
30
+ const r = Math.random() * 16 | 0;
31
+ const v = c === "x" ? r : r & 3 | 8;
32
+ return v.toString(16);
33
+ });
34
+ }
35
+ describe("world-packs primitives", () => {
36
+ let tmp;
37
+ before(() => {
38
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-wp-"));
39
+ });
40
+ after(() => {
41
+ fs.rmSync(tmp, { recursive: true, force: true });
42
+ });
43
+ it("formatWorldPackFolderName \u53BB\u683C\u5F0F\u7801/\u540E\u7F00\u5E76\u52A0\u524D\u7F00", async () => {
44
+ const { formatWorldPackFolderName, isGenericPackFolderStem } = await import("./world-packs.js");
45
+ assert.equal(formatWorldPackFolderName("\xA7aCool\xA7l Textures.mcpack", "resource"), "[RP] Cool Textures");
46
+ assert.equal(formatWorldPackFolderName("My BP.zip", "behavior"), "[BP] My BP");
47
+ assert.equal(formatWorldPackFolderName("[RP] Already", "resource"), "[RP] Already");
48
+ assert.equal(isGenericPackFolderStem("B"), true);
49
+ assert.equal(isGenericPackFolderStem("[RP] R"), true);
50
+ assert.equal(isGenericPackFolderStem("Slash Blade"), false);
51
+ });
52
+ it("discoverPackRoots maxDepth=2", async () => {
53
+ const { discoverPackRoots } = await import("./world-packs.js");
54
+ const root = path.join(tmp, "discover");
55
+ const nested = path.join(root, "outer", "inner-pack");
56
+ writeManifest(nested, {
57
+ name: "Inner",
58
+ uuid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
59
+ version: [1, 0, 0],
60
+ type: "resources"
61
+ });
62
+ const found = discoverPackRoots(root, { maxDepth: 2 });
63
+ assert.ok(found.some((p) => path.resolve(p) === path.resolve(nested)));
64
+ });
65
+ it("resolvePackRoots\uFF1A\u6241\u5E73\u76EE\u5F55 / \u5B50\u76EE\u5F55 BP+RP", async () => {
66
+ const { resolvePackRoots, readPackManifestInfo } = await import("./world-packs.js");
67
+ const flat = path.join(tmp, "flat-pack");
68
+ writeManifest(flat, {
69
+ name: "FlatBP",
70
+ uuid: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
71
+ version: [1, 0, 0],
72
+ type: "data"
73
+ });
74
+ const flatRes = await resolvePackRoots(flat);
75
+ try {
76
+ assert.equal(flatRes.roots.length, 1);
77
+ assert.equal(readPackManifestInfo(flatRes.roots[0])?.name, "FlatBP");
78
+ } finally {
79
+ flatRes.dispose();
80
+ }
81
+ const nest = path.join(tmp, "nest-dir");
82
+ writeManifest(path.join(nest, "BP"), {
83
+ name: "NestBP",
84
+ uuid: "cccccccc-cccc-cccc-cccc-cccccccccccc",
85
+ version: [1, 0, 0],
86
+ type: "data"
87
+ });
88
+ writeManifest(path.join(nest, "RP"), {
89
+ name: "NestRP",
90
+ uuid: "dddddddd-dddd-dddd-dddd-dddddddddddd",
91
+ version: [1, 0, 0],
92
+ type: "resources"
93
+ });
94
+ const nestRes = await resolvePackRoots(nest);
95
+ try {
96
+ assert.equal(nestRes.roots.length, 2);
97
+ const kinds = nestRes.roots.map((r) => readPackManifestInfo(r)?.kind).sort();
98
+ assert.deepEqual(kinds, ["behavior", "resource"]);
99
+ } finally {
100
+ nestRes.dispose();
101
+ }
102
+ });
103
+ it("resolvePackRoots\uFF1Amcaddon \u5185\u5D4C mcpack + zip \u5957\u5A03", async () => {
104
+ const JSZip = (await import("jszip")).default;
105
+ const { resolvePackRoots, readPackManifestInfo } = await import("./world-packs.js");
106
+ async function zipDirAsArchive(entries, outFile) {
107
+ const zip2 = new JSZip();
108
+ for (const [name, content] of entries) {
109
+ zip2.file(name, content);
110
+ }
111
+ const buf = await zip2.generateAsync({ type: "nodebuffer" });
112
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
113
+ fs.writeFileSync(outFile, buf);
114
+ }
115
+ function manifestJson({ name, uuid, version, type }) {
116
+ return JSON.stringify({
117
+ format_version: 2,
118
+ header: { name, uuid, version, description: "test" },
119
+ modules: [{ type, uuid: cryptoRandom(), version }]
120
+ });
121
+ }
122
+ const rpMcpack = path.join(tmp, "nested-build", "inner-rp.mcpack");
123
+ await zipDirAsArchive(
124
+ [
125
+ [
126
+ "manifest.json",
127
+ manifestJson({
128
+ name: "InnerRP",
129
+ uuid: "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
130
+ version: [1, 0, 0],
131
+ type: "resources"
132
+ })
133
+ ]
134
+ ],
135
+ rpMcpack
136
+ );
137
+ const bpManifest = manifestJson({
138
+ name: "OuterBP",
139
+ uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
140
+ version: [1, 0, 0],
141
+ type: "data"
142
+ });
143
+ const addon = path.join(tmp, "nested-build", "bundle.mcaddon");
144
+ const zip = new JSZip();
145
+ zip.file("BP/manifest.json", bpManifest);
146
+ zip.file("inner-rp.mcpack", fs.readFileSync(rpMcpack));
147
+ fs.writeFileSync(addon, await zip.generateAsync({ type: "nodebuffer" }));
148
+ const addonRes = await resolvePackRoots(addon);
149
+ try {
150
+ assert.equal(addonRes.roots.length, 2, `roots=${JSON.stringify(addonRes.roots)}`);
151
+ const names = addonRes.roots.map((r) => readPackManifestInfo(r)?.name).sort();
152
+ assert.deepEqual(names, ["InnerRP", "OuterBP"]);
153
+ } finally {
154
+ addonRes.dispose();
155
+ }
156
+ const leafMcpack = path.join(tmp, "nested-build", "leaf.mcpack");
157
+ await zipDirAsArchive(
158
+ [
159
+ [
160
+ "manifest.json",
161
+ manifestJson({
162
+ name: "LeafPack",
163
+ uuid: "12121212-1212-1212-1212-121212121212",
164
+ version: [2, 0, 0],
165
+ type: "data"
166
+ })
167
+ ]
168
+ ],
169
+ leafMcpack
170
+ );
171
+ const outerZip = path.join(tmp, "nested-build", "outer.zip");
172
+ const z2 = new JSZip();
173
+ z2.file("leaf.mcpack", fs.readFileSync(leafMcpack));
174
+ fs.writeFileSync(outerZip, await z2.generateAsync({ type: "nodebuffer" }));
175
+ const zipRes = await resolvePackRoots(outerZip);
176
+ try {
177
+ assert.equal(zipRes.roots.length, 1);
178
+ assert.equal(readPackManifestInfo(zipRes.roots[0])?.name, "LeafPack");
179
+ } finally {
180
+ zipRes.dispose();
181
+ }
182
+ });
183
+ it("bumpPackPatchVersion \u540C\u6B65 header \u4E0E modules", async () => {
184
+ const { bumpPackPatchVersion, readPackManifestInfo } = await import("./world-packs.js");
185
+ const dir = path.join(tmp, "bump-rp");
186
+ writeManifest(dir, {
187
+ name: "BumpMe",
188
+ uuid: "11111111-2222-3333-4444-555555555555",
189
+ version: [1, 2, 3],
190
+ type: "resources"
191
+ });
192
+ const next = bumpPackPatchVersion(dir);
193
+ assert.deepEqual(next, [1, 2, 4]);
194
+ const info = readPackManifestInfo(dir);
195
+ assert.deepEqual(info?.version, [1, 2, 4]);
196
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, "manifest.json"), "utf8"));
197
+ assert.deepEqual(raw.modules[0].version, [1, 2, 4]);
198
+ });
199
+ it("ensureVersionGreaterThan / nextEnabledVersion\uFF1Abump(max(\u65B0\u5305, \u65E7\u7248))", async () => {
200
+ const { ensureVersionGreaterThan, nextEnabledVersion, readPackManifestInfo } = await import("./world-packs.js");
201
+ assert.deepEqual(nextEnabledVersion([1, 0, 0], [1, 21, 100], "patch"), [1, 21, 101]);
202
+ const dir = path.join(tmp, "bump-floor");
203
+ writeManifest(dir, {
204
+ name: "SlashRp",
205
+ uuid: "8b450660-c968-3d5e-696e-b7f14c03388a",
206
+ version: [1, 0, 0],
207
+ type: "resources"
208
+ });
209
+ const next = ensureVersionGreaterThan(dir, [1, 21, 100], "patch");
210
+ assert.deepEqual(next, [1, 21, 101]);
211
+ assert.deepEqual(readPackManifestInfo(dir)?.version, [1, 21, 101]);
212
+ });
213
+ it("list-installed CLI + listInstalledWorldPacks", async () => {
214
+ const { listInstalledWorldPacks } = await import("./world-packs.js");
215
+ const bds = path.join(tmp, "bds");
216
+ const level = "Bedrock level";
217
+ const rpDir = path.join(bds, "worlds", level, "resource_packs", "[RP] Tex");
218
+ writeManifest(rpDir, {
219
+ name: "Tex",
220
+ uuid: "99999999-8888-7777-6666-555555555555",
221
+ version: [0, 1, 0],
222
+ type: "resources"
223
+ });
224
+ fs.mkdirSync(path.join(bds, "worlds", level), { recursive: true });
225
+ fs.writeFileSync(
226
+ path.join(bds, "worlds", level, "world_resource_packs.json"),
227
+ JSON.stringify([{ pack_id: "99999999-8888-7777-6666-555555555555", version: [0, 1, 0] }])
228
+ );
229
+ const list = listInstalledWorldPacks(bds, level);
230
+ assert.equal(list.length, 1);
231
+ assert.equal(list[0].enabled, true);
232
+ assert.equal(list[0].kind, "resource");
233
+ const cli = run(["list-installed", "--bds-root", bds, "--level", level]);
234
+ assert.equal(cli.status, 0, cli.err || cli.out);
235
+ const parsed = JSON.parse(cli.out.trim());
236
+ assert.equal(parsed.length, 1);
237
+ });
238
+ it("bump-version CLI", async () => {
239
+ const dir = path.join(tmp, "cli-bump");
240
+ writeManifest(dir, {
241
+ name: "CliBump",
242
+ uuid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
243
+ version: [2, 0, 0],
244
+ type: "resources"
245
+ });
246
+ const cli = run(["bump-version", "--pack-dir", dir]);
247
+ assert.equal(cli.status, 0, cli.err || cli.out);
248
+ const { version } = JSON.parse(cli.out.trim());
249
+ assert.deepEqual(version, [2, 0, 1]);
250
+ });
251
+ it("scanDestOccupancy / readPackDirOccupancy\uFF1A\u5B8C\u6574\u4E0E\u6B8B\u7F3A manifest", async () => {
252
+ const { scanDestOccupancy, readPackDirOccupancy, formatWorldPackFolderName } = await import("./world-packs.js");
253
+ const parent = path.join(tmp, "occupancy-scan");
254
+ const fullName = formatWorldPackFolderName("Full", "resource");
255
+ const brokenName = formatWorldPackFolderName("Broken", "behavior");
256
+ const fullDir = path.join(parent, fullName);
257
+ const brokenDir = path.join(parent, brokenName);
258
+ writeManifest(fullDir, {
259
+ name: "Full Pack",
260
+ uuid: "11111111-1111-1111-1111-111111111111",
261
+ version: [2, 3, 4],
262
+ type: "resources"
263
+ });
264
+ fs.mkdirSync(brokenDir, { recursive: true });
265
+ fs.writeFileSync(
266
+ path.join(brokenDir, "manifest.json"),
267
+ JSON.stringify({
268
+ format_version: 2,
269
+ header: {
270
+ name: "Broken Pack",
271
+ uuid: "22222222-2222-2222-2222-222222222222",
272
+ version: [9, 0, 1]
273
+ },
274
+ modules: []
275
+ })
276
+ );
277
+ const fullFacts = readPackDirOccupancy(fullDir);
278
+ assert.equal(fullFacts?.uuid, "11111111-1111-1111-1111-111111111111");
279
+ assert.deepEqual(fullFacts?.version, [2, 3, 4]);
280
+ assert.equal(fullFacts?.kind, "resource");
281
+ const brokenFacts = readPackDirOccupancy(brokenDir);
282
+ assert.equal(brokenFacts?.uuid, "22222222-2222-2222-2222-222222222222");
283
+ assert.deepEqual(brokenFacts?.version, [9, 0, 1]);
284
+ assert.equal(brokenFacts?.kind, void 0);
285
+ const occ = scanDestOccupancy(parent);
286
+ assert.equal(occ.length, 2);
287
+ const byFolder = Object.fromEntries(occ.map((o) => [o.folderName, o]));
288
+ assert.equal(byFolder[fullName]?.uuid, "11111111-1111-1111-1111-111111111111");
289
+ assert.equal(byFolder[fullName]?.kind, "resource");
290
+ assert.equal(byFolder[brokenName]?.uuid, "22222222-2222-2222-2222-222222222222");
291
+ assert.equal(byFolder[brokenName]?.kind, void 0);
292
+ });
293
+ it("decidePackInstallPlan \u8868\u9A71\u52A8", async () => {
294
+ const { decidePackInstallPlan, formatWorldPackFolderName } = await import("./world-packs.js");
295
+ const uuidA = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
296
+ const uuidB = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
297
+ const mk = (over) => ({
298
+ name: "Foo",
299
+ uuid: uuidB,
300
+ version: [1, 0, 0],
301
+ kind: "behavior",
302
+ ...over
303
+ });
304
+ const cases = [
305
+ {
306
+ title: "\u7A7A\u5360\u7528 + hint B \u2192 fresh \u7528 manifest \u540D",
307
+ incoming: mk({ name: "Foo", uuid: uuidB }),
308
+ hint: "B",
309
+ occupancy: [],
310
+ force: false,
311
+ expectKind: "fresh",
312
+ expectFolder: "[BP] Foo"
313
+ },
314
+ {
315
+ title: "\u5DF2\u6709 [BP] B uuidA + \u65B0 uuidB hint B \u2192 fresh \u4E14\u4E0D\u5360\u7528 B",
316
+ incoming: mk({ name: "pack.name", uuid: uuidB, version: [5, 0, 4] }),
317
+ hint: "B",
318
+ occupancy: [
319
+ {
320
+ folderName: "[BP] B",
321
+ dir: "/x/[BP] B",
322
+ uuid: uuidA,
323
+ version: [1, 0, 0],
324
+ name: "Slash"
325
+ }
326
+ ],
327
+ force: false,
328
+ expectKind: "fresh",
329
+ expectFolderNot: "[BP] B"
330
+ },
331
+ {
332
+ title: "\u540C uuid \u7248\u672C\u66F4\u9AD8 !force \u2192 overwriteInPlace",
333
+ incoming: mk({ name: "Slash v2", uuid: uuidA, version: [1, 2, 0] }),
334
+ hint: "Other",
335
+ occupancy: [
336
+ {
337
+ folderName: "[BP] B",
338
+ dir: "/x/[BP] B",
339
+ uuid: uuidA,
340
+ version: [1, 1, 0],
341
+ name: "Slash"
342
+ }
343
+ ],
344
+ force: false,
345
+ expectKind: "overwriteInPlace",
346
+ expectFolder: "[BP] B"
347
+ },
348
+ {
349
+ title: "\u540C uuid \u7248\u672C\u76F8\u7B49 !force \u2192 needConfirm",
350
+ incoming: mk({ name: "Slash", uuid: uuidA, version: [1, 1, 0] }),
351
+ hint: "Other",
352
+ occupancy: [
353
+ {
354
+ folderName: "[BP] B",
355
+ dir: "/x/[BP] B",
356
+ uuid: uuidA,
357
+ version: [1, 1, 0],
358
+ name: "Slash"
359
+ }
360
+ ],
361
+ force: false,
362
+ expectKind: "needConfirm",
363
+ expectFolder: "[BP] B"
364
+ },
365
+ {
366
+ title: "\u540C uuid \u7248\u672C\u66F4\u4F4E !force \u2192 needConfirm",
367
+ incoming: mk({ name: "Slash", uuid: uuidA, version: [1, 0, 0] }),
368
+ hint: "Other",
369
+ occupancy: [
370
+ {
371
+ folderName: "[BP] B",
372
+ dir: "/x/[BP] B",
373
+ uuid: uuidA,
374
+ version: [1, 1, 0],
375
+ name: "Slash"
376
+ }
377
+ ],
378
+ force: false,
379
+ expectKind: "needConfirm",
380
+ expectFolder: "[BP] B"
381
+ },
382
+ {
383
+ title: "\u540C uuid \u964D\u7EA7 + force \u2192 overwriteInPlace",
384
+ incoming: mk({ name: "Slash old", uuid: uuidA, version: [1, 0, 0] }),
385
+ hint: "Other",
386
+ occupancy: [
387
+ {
388
+ folderName: "[BP] B",
389
+ dir: "/x/[BP] B",
390
+ uuid: uuidA,
391
+ version: [1, 1, 0],
392
+ name: "Slash"
393
+ }
394
+ ],
395
+ force: true,
396
+ expectKind: "overwriteInPlace",
397
+ expectFolder: "[BP] B"
398
+ },
399
+ {
400
+ title: "\u4E24\u4EBA\u540D\u649E\u8F66 \u2192 fresh \u5E26 uuid \u540E\u7F00",
401
+ incoming: mk({ name: "Cool", uuid: uuidB, version: [1, 0, 0] }),
402
+ hint: "Cool",
403
+ occupancy: [
404
+ {
405
+ folderName: formatWorldPackFolderName("Cool", "behavior"),
406
+ dir: "/x/a",
407
+ uuid: uuidA,
408
+ version: [1, 0, 0]
409
+ }
410
+ ],
411
+ force: false,
412
+ expectKind: "fresh",
413
+ expectFolder: formatWorldPackFolderName(`Cool ${uuidB.slice(0, 8)}`, "behavior")
414
+ }
415
+ ];
416
+ for (const c of cases) {
417
+ const plan = decidePackInstallPlan({
418
+ incoming: c.incoming,
419
+ hint: c.hint,
420
+ occupancy: c.occupancy,
421
+ force: c.force
422
+ });
423
+ assert.equal(plan.kind, c.expectKind, c.title);
424
+ if (c.expectFolder) assert.equal(plan.folderName, c.expectFolder, c.title);
425
+ if (c.expectFolderNot) assert.notEqual(plan.folderName, c.expectFolderNot, c.title);
426
+ if (plan.kind === "overwriteInPlace" || plan.kind === "needConfirm") {
427
+ assert.equal(plan.folderName, c.occupancy[0].folderName, `${c.title}: \u5FC5\u987B\u539F\u5730`);
428
+ }
429
+ }
430
+ });
431
+ it("installPackDirectory FS\uFF1A\u5F02 uuid \u6362\u540D\uFF1B\u7248\u672C\u5347\u7EA7\u9759\u9ED8\u8986\u76D6\uFF1B\u540C\u7248\u9700 force", async () => {
432
+ const { installPackDirectory, formatWorldPackFolderName, readPackManifestInfo } = await import("./world-packs.js");
433
+ const dest = path.join(tmp, "conflict-parent");
434
+ const existingB = path.join(dest, formatWorldPackFolderName("B", "behavior"));
435
+ writeManifest(existingB, {
436
+ name: "Slash Blade",
437
+ uuid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
438
+ version: [1, 0, 0],
439
+ type: "data"
440
+ });
441
+ const src = path.join(tmp, "incoming-bp");
442
+ writeManifest(src, {
443
+ name: "pack.name",
444
+ uuid: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
445
+ version: [5, 0, 4],
446
+ type: "data"
447
+ });
448
+ const r = await installPackDirectory({
449
+ srcDir: src,
450
+ destParent: dest,
451
+ folderName: "B",
452
+ force: false
453
+ });
454
+ assert.equal(r.ok, true, r.reason);
455
+ assert.notEqual(path.basename(r.destDir), path.basename(existingB));
456
+ assert.equal(readPackManifestInfo(existingB)?.uuid, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
457
+ const srcUp = path.join(tmp, "incoming-upgrade");
458
+ writeManifest(srcUp, {
459
+ name: "Slash Blade v2",
460
+ uuid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
461
+ version: [2, 0, 0],
462
+ type: "data"
463
+ });
464
+ const up = await installPackDirectory({
465
+ srcDir: srcUp,
466
+ destParent: dest,
467
+ folderName: "Totally Different Hint",
468
+ force: false
469
+ });
470
+ assert.equal(up.ok, true, up.reason);
471
+ assert.equal(up.conflict, void 0);
472
+ assert.equal(up.destDir && path.basename(up.destDir), path.basename(existingB));
473
+ assert.equal(readPackManifestInfo(existingB)?.version.join("."), "2.0.0");
474
+ assert.equal(fs.readdirSync(dest).filter((n) => n.startsWith("[BP]")).length, 2);
475
+ const srcSame = path.join(tmp, "incoming-same-ver");
476
+ writeManifest(srcSame, {
477
+ name: "Slash Blade v2b",
478
+ uuid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
479
+ version: [2, 0, 0],
480
+ type: "data"
481
+ });
482
+ const c = await installPackDirectory({
483
+ srcDir: srcSame,
484
+ destParent: dest,
485
+ force: false
486
+ });
487
+ assert.equal(c.ok, false);
488
+ assert.equal(c.reason, "conflict");
489
+ const forced = await installPackDirectory({
490
+ srcDir: srcSame,
491
+ destParent: dest,
492
+ force: true
493
+ });
494
+ assert.equal(forced.ok, true, forced.reason);
495
+ assert.equal(forced.destDir && path.basename(forced.destDir), path.basename(existingB));
496
+ assert.equal(readPackManifestInfo(existingB)?.name, "Slash Blade v2b");
497
+ });
498
+ it("installPackDirectory\uFF1A\u6B8B\u7F3A manifest \u5360\u7528\u76EE\u5F55\u65F6\u6362\u540D\u800C\u975E\u8986\u76D6", async () => {
499
+ const { installPackDirectory, formatWorldPackFolderName } = await import("./world-packs.js");
500
+ const dest = path.join(tmp, "broken-parent");
501
+ const folderName = formatWorldPackFolderName("Broken", "resource");
502
+ const existingDir = path.join(dest, folderName);
503
+ fs.mkdirSync(existingDir, { recursive: true });
504
+ fs.writeFileSync(
505
+ path.join(existingDir, "manifest.json"),
506
+ JSON.stringify({
507
+ format_version: 2,
508
+ header: {
509
+ name: "Broken",
510
+ uuid: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
511
+ version: [1, 0, 0]
512
+ },
513
+ modules: []
514
+ })
515
+ );
516
+ const src = path.join(tmp, "incoming-rp");
517
+ writeManifest(src, {
518
+ name: "Incoming",
519
+ uuid: "cccccccc-cccc-cccc-cccc-cccccccccccc",
520
+ version: [1, 0, 0],
521
+ type: "resources"
522
+ });
523
+ const r = await installPackDirectory({
524
+ srcDir: src,
525
+ destParent: dest,
526
+ folderName: "Broken",
527
+ force: false
528
+ });
529
+ assert.equal(r.ok, true, r.reason);
530
+ assert.notEqual(r.destDir && path.basename(r.destDir), folderName);
531
+ const still = JSON.parse(fs.readFileSync(path.join(existingDir, "manifest.json"), "utf8"));
532
+ assert.equal(still.header.uuid, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
533
+ });
534
+ it("installPackDirectory\uFF1A\u6B8B\u7F3A manifest \u540C uuid \u2192 \u6309\u7248\u672C\u51B3\u7B56\uFF08\u975E\u65C1\u8DEF\u65B0\u76EE\u5F55\uFF09", async () => {
535
+ const { installPackDirectory, formatWorldPackFolderName, readPackManifestInfo } = await import("./world-packs.js");
536
+ const dest = path.join(tmp, "broken-same-uuid");
537
+ const folderName = formatWorldPackFolderName("Legacy", "behavior");
538
+ const existingDir = path.join(dest, folderName);
539
+ fs.mkdirSync(existingDir, { recursive: true });
540
+ const uuid = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee";
541
+ fs.writeFileSync(
542
+ path.join(existingDir, "manifest.json"),
543
+ JSON.stringify({
544
+ format_version: 2,
545
+ header: { name: "Legacy", uuid, version: [1, 0, 0] },
546
+ modules: []
547
+ })
548
+ );
549
+ const srcSame = path.join(tmp, "incoming-same-broken");
550
+ writeManifest(srcSame, {
551
+ name: "Fixed",
552
+ uuid,
553
+ version: [1, 0, 0],
554
+ type: "data"
555
+ });
556
+ const c = await installPackDirectory({
557
+ srcDir: srcSame,
558
+ destParent: dest,
559
+ folderName: "OtherHint",
560
+ force: false
561
+ });
562
+ assert.equal(c.ok, false);
563
+ assert.equal(c.reason, "conflict");
564
+ assert.equal(c.conflict?.existing.dir, existingDir);
565
+ const srcUp = path.join(tmp, "incoming-up-broken");
566
+ writeManifest(srcUp, {
567
+ name: "Fixed v2",
568
+ uuid,
569
+ version: [1, 1, 0],
570
+ type: "data"
571
+ });
572
+ const up = await installPackDirectory({
573
+ srcDir: srcUp,
574
+ destParent: dest,
575
+ folderName: "OtherHint",
576
+ force: false
577
+ });
578
+ assert.equal(up.ok, true, up.reason);
579
+ assert.equal(up.destDir, existingDir);
580
+ assert.equal(readPackManifestInfo(existingDir)?.version.join("."), "1.1.0");
581
+ });
582
+ it("uninstallInstalledPack\uFF1A\u56DE\u6536\u7AD9 / purge / \u76EE\u5F55\u7F3A\u5931", async () => {
583
+ const { uninstallInstalledPack, listInstalledWorldPacks } = await import("./world-packs.js");
584
+ const bds = path.join(tmp, "uninstall-bds");
585
+ const level = "Bedrock level";
586
+ const folder = "[BP] Gone";
587
+ const uuid = "dddddddd-dddd-dddd-dddd-dddddddddddd";
588
+ const packDir = path.join(bds, "worlds", level, "behavior_packs", folder);
589
+ writeManifest(packDir, {
590
+ name: "Gone",
591
+ uuid,
592
+ version: [1, 0, 0],
593
+ type: "data"
594
+ });
595
+ fs.mkdirSync(path.join(bds, "worlds", level), { recursive: true });
596
+ fs.writeFileSync(
597
+ path.join(bds, "worlds", level, "world_behavior_packs.json"),
598
+ JSON.stringify([{ pack_id: uuid, version: [1, 0, 0] }])
599
+ );
600
+ const packs = listInstalledWorldPacks(bds, level);
601
+ assert.equal(packs.length, 1);
602
+ const trash = path.join(tmp, "trash-bin");
603
+ const trashed = await uninstallInstalledPack({
604
+ bdsRoot: bds,
605
+ levelName: level,
606
+ pack: packs[0],
607
+ trashDir: trash
608
+ });
609
+ assert.equal(trashed.action, "trashed");
610
+ assert.ok(trashed.dest && fs.existsSync(trashed.dest));
611
+ assert.equal(fs.existsSync(packDir), false);
612
+ const enable = JSON.parse(
613
+ fs.readFileSync(path.join(bds, "worlds", level, "world_behavior_packs.json"), "utf8")
614
+ );
615
+ assert.equal(enable.length, 0);
616
+ const phantom = {
617
+ ...packs[0],
618
+ dir: path.join(bds, "worlds", level, "behavior_packs", "no-such"),
619
+ enabled: false
620
+ };
621
+ const missing = await uninstallInstalledPack({
622
+ bdsRoot: bds,
623
+ levelName: level,
624
+ pack: phantom,
625
+ trashDir: trash
626
+ });
627
+ assert.equal(missing.action, "missing");
628
+ const folder2 = "[BP] PurgeMe";
629
+ const uuid2 = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee";
630
+ const packDir2 = path.join(bds, "worlds", level, "behavior_packs", folder2);
631
+ writeManifest(packDir2, {
632
+ name: "PurgeMe",
633
+ uuid: uuid2,
634
+ version: [1, 0, 0],
635
+ type: "data"
636
+ });
637
+ fs.writeFileSync(
638
+ path.join(bds, "worlds", level, "world_behavior_packs.json"),
639
+ JSON.stringify([{ pack_id: uuid2, version: [1, 0, 0] }])
640
+ );
641
+ const packs2 = listInstalledWorldPacks(bds, level);
642
+ const purged = await uninstallInstalledPack({
643
+ bdsRoot: bds,
644
+ levelName: level,
645
+ pack: packs2.find((p) => p.uuid === uuid2),
646
+ trashDir: null
647
+ });
648
+ assert.equal(purged.action, "deleted");
649
+ assert.equal(fs.existsSync(packDir2), false);
650
+ });
651
+ it("readPackManifestInfo \u9047 UTF-8 BOM \u4ECD\u53EF\u8BFB\u51FA uuid/name\uFF08\u5265\u79BB\u517C\u5BB9\uFF09", async () => {
652
+ const { readPackManifestInfo } = await import("./world-packs.js");
653
+ const dir = path.join(tmp, "bom-rp");
654
+ fs.mkdirSync(dir, { recursive: true });
655
+ const body = JSON.stringify(
656
+ {
657
+ format_version: 2,
658
+ header: {
659
+ name: "\xA7l\xA7a\u795E\u91D1",
660
+ uuid: "2b6de4b1-1f74-4c6e-937b-77f5e9c1f199",
661
+ version: [1, 0, 4]
662
+ },
663
+ modules: [{ type: "resources", uuid: "1c710355-15e2-4293-8574-788eec05d35f", version: [1, 0, 4] }]
664
+ },
665
+ null,
666
+ 2
667
+ );
668
+ fs.writeFileSync(path.join(dir, "manifest.json"), `\uFEFF${body}`, "utf8");
669
+ const info = readPackManifestInfo(dir);
670
+ assert.ok(info);
671
+ assert.equal(info.uuid, "2b6de4b1-1f74-4c6e-937b-77f5e9c1f199");
672
+ assert.equal(info.name, "\xA7l\xA7a\u795E\u91D1");
673
+ assert.equal(info.kind, "resource");
674
+ });
675
+ });
676
+ //# sourceMappingURL=world-packs.test.js.map