@wizzlethorpe/vaults 0.13.5 → 0.14.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.
Files changed (53) hide show
  1. package/dist/asset-refs.js +274 -0
  2. package/dist/asset-refs.js.map +1 -0
  3. package/dist/auth.js.map +1 -1
  4. package/dist/build.js +252 -492
  5. package/dist/build.js.map +1 -1
  6. package/dist/commands/build.js +54 -5
  7. package/dist/commands/build.js.map +1 -1
  8. package/dist/commands/preview.js +0 -4
  9. package/dist/commands/preview.js.map +1 -1
  10. package/dist/commands/push.js +4 -9
  11. package/dist/commands/push.js.map +1 -1
  12. package/dist/commands/role.js +55 -14
  13. package/dist/commands/role.js.map +1 -1
  14. package/dist/config.js +25 -5
  15. package/dist/config.js.map +1 -1
  16. package/dist/foundry-importer.bundle.js +1153 -319
  17. package/dist/foundry-importer.js +2 -7
  18. package/dist/foundry-importer.js.map +1 -1
  19. package/dist/foundry-meta.js +284 -0
  20. package/dist/foundry-meta.js.map +1 -0
  21. package/dist/foundry-module-journal.js +112 -0
  22. package/dist/foundry-module-journal.js.map +1 -0
  23. package/dist/foundry-module-render.js +75 -0
  24. package/dist/foundry-module-render.js.map +1 -0
  25. package/dist/foundry-module.js +1075 -0
  26. package/dist/foundry-module.js.map +1 -0
  27. package/dist/frontmatter-defaults.js +68 -0
  28. package/dist/frontmatter-defaults.js.map +1 -0
  29. package/dist/index.js +9 -9
  30. package/dist/index.js.map +1 -1
  31. package/dist/manifest.js +115 -0
  32. package/dist/manifest.js.map +1 -0
  33. package/dist/render/auth-template.js +323 -35
  34. package/dist/render/auth-template.js.map +1 -1
  35. package/dist/render/bases.js +22 -38
  36. package/dist/render/bases.js.map +1 -1
  37. package/dist/render/cover.js +23 -1
  38. package/dist/render/cover.js.map +1 -1
  39. package/dist/render/handlers/builtin/download.js +90 -0
  40. package/dist/render/handlers/builtin/download.js.map +1 -0
  41. package/dist/render/handlers/builtin/foundry-manifest.js +158 -0
  42. package/dist/render/handlers/builtin/foundry-manifest.js.map +1 -0
  43. package/dist/render/handlers/builtin/index.js +3 -1
  44. package/dist/render/handlers/builtin/index.js.map +1 -1
  45. package/dist/render/pipeline.js +4 -1
  46. package/dist/render/pipeline.js.map +1 -1
  47. package/dist/render/slug.js +0 -5
  48. package/dist/render/slug.js.map +1 -1
  49. package/dist/scan.js +8 -0
  50. package/dist/scan.js.map +1 -1
  51. package/dist/settings.js +112 -6
  52. package/dist/settings.js.map +1 -1
  53. package/package.json +2 -1
@@ -16,10 +16,15 @@ async function fetchManifest(vault) {
16
16
  var BATCH_SIZE = 100;
17
17
  var BATCH_CONCURRENCY = 4;
18
18
  var DIRECT_CONCURRENCY = 8;
19
- async function fetchSourceBatch(vault, paths) {
19
+ function batchEndpoint(vault, role) {
20
+ const endpoint = new URL(url(vault, "/_batch"));
21
+ if (role) endpoint.searchParams.set("role", role);
22
+ return endpoint;
23
+ }
24
+ async function fetchSourceBatch(vault, paths, role) {
20
25
  if (paths.length === 0) return /* @__PURE__ */ new Map();
21
26
  if (vault.public) return fetchSourceDirect(vault, paths);
22
- const endpoint = url(vault, "/_batch");
27
+ const endpoint = batchEndpoint(vault, role);
23
28
  const chunks = [];
24
29
  for (let i = 0; i < paths.length; i += BATCH_SIZE) chunks.push(paths.slice(i, i + BATCH_SIZE));
25
30
  const out = /* @__PURE__ */ new Map();
@@ -27,7 +32,7 @@ async function fetchSourceBatch(vault, paths) {
27
32
  const workers = Array.from({ length: Math.min(BATCH_CONCURRENCY, chunks.length) }, async () => {
28
33
  while (next < chunks.length) {
29
34
  const idx = next++;
30
- const res = await fetch(endpoint, {
35
+ const res = await fetch(endpoint.toString(), {
31
36
  method: "POST",
32
37
  headers: { "Content-Type": "text/plain" },
33
38
  body: chunks[idx].join("\n")
@@ -69,6 +74,185 @@ async function fetchSourceDirect(vault, paths) {
69
74
  // ../foundry/scripts/settings.mjs
70
75
  var MODULE_ID = "vaults";
71
76
 
77
+ // ../foundry/scripts/foundry-base.mjs
78
+ var PACK_KEY = {
79
+ Actor: "actors",
80
+ Item: "items",
81
+ Scene: "scenes",
82
+ JournalEntry: "journal",
83
+ RollTable: "tables",
84
+ Macro: "macros",
85
+ Cards: "cards",
86
+ Playlist: "playlists"
87
+ };
88
+ var BLANK_DOC_TYPES = [
89
+ "Actor",
90
+ "Item",
91
+ "Scene",
92
+ "JournalEntry",
93
+ "RollTable",
94
+ "Macro",
95
+ "Cards",
96
+ "Playlist"
97
+ ];
98
+ function canonicalType(raw) {
99
+ if (!raw) return null;
100
+ return BLANK_DOC_TYPES.find((t) => t.toLowerCase() === raw.toLowerCase()) ?? null;
101
+ }
102
+ function parseFoundryBase(spec) {
103
+ if (typeof spec !== "string" || !spec) return null;
104
+ if (spec.startsWith("@moulinette/")) {
105
+ const ref = spec.slice("@moulinette/".length);
106
+ return ref ? { kind: "moulinette", ref } : null;
107
+ }
108
+ if (spec.includes(".")) return { kind: "uuid", uuid: spec };
109
+ const [typeRaw, subtype] = spec.split(":");
110
+ const docName = canonicalType(typeRaw);
111
+ if (!docName) return null;
112
+ return { kind: "blank", docName, subtype: subtype || void 0 };
113
+ }
114
+ function docNameOf(parsed) {
115
+ if (!parsed) return null;
116
+ if (parsed.kind === "blank") return parsed.docName;
117
+ if (parsed.kind === "moulinette") return null;
118
+ const parts = parsed.uuid.split(".");
119
+ if (parts.length < 2) return null;
120
+ const raw = parts[parts.length - 2];
121
+ return canonicalType(raw) ?? raw ?? null;
122
+ }
123
+ function docNameFromBase(base) {
124
+ const specs = Array.isArray(base) ? base : [base];
125
+ for (const spec of specs) {
126
+ const docName = docNameOf(parseFoundryBase(spec));
127
+ if (docName) return docName;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ // ../foundry/scripts/packs.mjs
133
+ function packName(vault, docName) {
134
+ const key = docName === "Adventure" ? "adventure" : PACK_KEY[docName];
135
+ if (!key) return null;
136
+ return `${vault.id}-${key}`;
137
+ }
138
+ function packCollection(vault, docName) {
139
+ const name = packName(vault, docName);
140
+ return name ? `world.${name}` : null;
141
+ }
142
+ function isAdventure(vault) {
143
+ return vault.foundryPackage === "adventure";
144
+ }
145
+ function uuidPrefix(vault, docName) {
146
+ return isAdventure(vault) ? "" : `Compendium.${packCollection(vault, docName)}.`;
147
+ }
148
+ async function pruneStalePacks(vault) {
149
+ const wanted = isAdventure(vault) ? /* @__PURE__ */ new Set(["Adventure"]) : new Set(Object.keys(PACK_KEY));
150
+ for (const docName of [...Object.keys(PACK_KEY), "Adventure"]) {
151
+ if (wanted.has(docName)) continue;
152
+ const pack = getPack(vault, docName);
153
+ if (!pack) continue;
154
+ try {
155
+ await pack.deleteCompendium();
156
+ console.info(
157
+ `Vaults | ${vault.label}: removed ${pack.collection}, left over from the previous foundry_package setting.`
158
+ );
159
+ } catch (err) {
160
+ console.warn(`Vaults | could not remove the stale pack ${pack.collection}:`, err);
161
+ }
162
+ }
163
+ }
164
+ function journalPageUuid(vault, entryId2, pageId2) {
165
+ return uuidPrefix(vault, "JournalEntry") + `JournalEntry.${entryId2}.JournalEntryPage.${pageId2}`;
166
+ }
167
+ function instanceUuid(vault, docName, id) {
168
+ return uuidPrefix(vault, docName) + `${docName}.${id}`;
169
+ }
170
+ function getPack(vault, docName) {
171
+ const collection = packCollection(vault, docName);
172
+ return collection ? game.packs.get(collection) ?? null : null;
173
+ }
174
+ function vaultPacks(vault) {
175
+ return [...Object.keys(PACK_KEY), "Adventure"].map((docName) => getPack(vault, docName)).filter((pack) => pack !== null);
176
+ }
177
+ async function deleteVaultPacks(vaultId) {
178
+ for (const pack of vaultPacks({ id: vaultId })) {
179
+ try {
180
+ await pack.deleteCompendium();
181
+ } catch (err) {
182
+ console.warn(`Vaults | failed to delete pack ${pack.collection}:`, err);
183
+ }
184
+ }
185
+ }
186
+ var inFlight = /* @__PURE__ */ new Map();
187
+ async function ensurePack(vault, docName) {
188
+ const collection = packCollection(vault, docName);
189
+ if (!collection) throw new Error(`No pack is defined for ${docName} documents`);
190
+ const existing = game.packs.get(collection);
191
+ if (existing) {
192
+ assertWritable(existing, vault);
193
+ await enforceOwnership(existing, vault);
194
+ return existing;
195
+ }
196
+ const pending = inFlight.get(collection);
197
+ if (pending) return pending;
198
+ const promise = createPack(vault, docName, collection).finally(() => inFlight.delete(collection));
199
+ inFlight.set(collection, promise);
200
+ return promise;
201
+ }
202
+ function ownershipFor(vault) {
203
+ const base = { GAMEMASTER: "OWNER", ASSISTANT: "OWNER" };
204
+ return vault.public ? { ...base, TRUSTED: "OBSERVER", PLAYER: "OBSERVER" } : { ...base, TRUSTED: "NONE", PLAYER: "NONE" };
205
+ }
206
+ async function enforceOwnership(pack, vault) {
207
+ const want = ownershipFor(vault);
208
+ const have = pack.config.ownership;
209
+ if (have && Object.entries(want).every(([k, v]) => have[k] === v)) return;
210
+ await pack.configure({ ownership: want });
211
+ if (!vault.public) {
212
+ console.info(
213
+ `Vaults | ${pack.collection}: restricted to GM. It was readable by ${have?.PLAYER ?? "PLAYER: OBSERVER (Foundry's default)"}, which exposes every name and image in the pack index.`
214
+ );
215
+ }
216
+ }
217
+ async function createPack(vault, docName, collection) {
218
+ const label = `${vault.label || "Vault"}: ${LABEL[docName] ?? docName}`;
219
+ await foundry.documents.collections.CompendiumCollection.createCompendium({
220
+ type: docName,
221
+ label,
222
+ name: packName(vault, docName),
223
+ packageType: "world"
224
+ });
225
+ const pack = game.packs.get(collection);
226
+ if (!pack) throw new Error(`Compendium pack ${collection} was not created`);
227
+ await pack.setFolder(await ensurePackFolder(vault));
228
+ assertWritable(pack, vault);
229
+ await enforceOwnership(pack, vault);
230
+ return pack;
231
+ }
232
+ function assertWritable(pack, vault) {
233
+ if (!pack.locked) return;
234
+ throw new Error(
235
+ `Compendium pack ${pack.collection} is locked. Unlock it in the sidebar to let ${vault.label || "this vault"} sync into it.`
236
+ );
237
+ }
238
+ async function ensurePackFolder(vault) {
239
+ const name = vault.rootFolder || vault.label || "Vault";
240
+ const existing = game.folders.find((f) => f.type === "Compendium" && f.name === name);
241
+ if (existing) return existing;
242
+ return Folder.create({ name, type: "Compendium" });
243
+ }
244
+ var LABEL = {
245
+ Adventure: "Adventure",
246
+ Actor: "Actors",
247
+ Item: "Items",
248
+ Scene: "Scenes",
249
+ JournalEntry: "Journals",
250
+ RollTable: "Roll Tables",
251
+ Macro: "Macros",
252
+ Cards: "Cards",
253
+ Playlist: "Playlists"
254
+ };
255
+
72
256
  // ../foundry/scripts/util.mjs
73
257
  function escapeAttr(s) {
74
258
  return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
@@ -83,6 +267,11 @@ async function hexDigest(algorithm, text) {
83
267
  const buf = await crypto.subtle.digest(algorithm, new TextEncoder().encode(text));
84
268
  return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
85
269
  }
270
+ function localizeOr(host, key, fallback, args) {
271
+ const text = host.localize(key, args);
272
+ if (text && text !== key) return text;
273
+ return String(fallback).replace(/\{(\w+)\}/g, (whole, name) => args && name in args ? String(args[name]) : whole);
274
+ }
86
275
 
87
276
  // ../foundry/scripts/ids.mjs
88
277
  async function det(kind, key) {
@@ -98,10 +287,65 @@ var pageId = (vaultId, path) => det("page", `${vaultId}:${path}`);
98
287
  var folderId = (vaultId, path) => det("folder", `${vaultId}:${path}`);
99
288
  var instanceId = (vaultId, path) => det("instance", `${vaultId}:${path}`);
100
289
  var subdocId = (vaultId, path, pointer) => det("subdoc", `${vaultId}:${path}:${pointer}`);
290
+ var adventureId = (vaultId) => det("adventure", vaultId);
101
291
 
102
292
  // ../foundry/scripts/parser.mjs
103
293
  var CACHED_EXT_RE = /\.(png|jpe?g|webp|gif|svg|avif|tiff?|bmp|heic|apng|ogg|mp3|m4a|wav|flac|opus|aac|mp4|webm|mov|ogv|pdf|epub|json)$/i;
104
294
 
295
+ // ../foundry/scripts/progress.mjs
296
+ var bar = null;
297
+ var vaultLabel = "";
298
+ var phaseLabel = "";
299
+ var done = 0;
300
+ var total = 0;
301
+ function paint(message) {
302
+ if (!bar) return;
303
+ const counter = total > 0 ? ` ${Math.min(done, total)}/${total}` : "";
304
+ const head = phaseLabel ? `${vaultLabel}: ${phaseLabel}${counter}` : vaultLabel;
305
+ try {
306
+ bar.update({
307
+ pct: total > 0 ? Math.min(done / total, 1) : 0,
308
+ message: message ? `${head} \u2014 ${message}` : head
309
+ });
310
+ } catch {
311
+ bar = null;
312
+ }
313
+ }
314
+ function begin(label) {
315
+ vaultLabel = label;
316
+ phaseLabel = "";
317
+ done = 0;
318
+ total = 0;
319
+ try {
320
+ const handle = ui?.notifications?.info(label, { progress: true, permanent: true });
321
+ bar = typeof handle?.update === "function" ? handle : null;
322
+ } catch {
323
+ bar = null;
324
+ }
325
+ }
326
+ function phase(name, count) {
327
+ phaseLabel = name;
328
+ done = 0;
329
+ total = count;
330
+ paint("");
331
+ }
332
+ function step(message) {
333
+ done++;
334
+ paint(message);
335
+ }
336
+ function note(message) {
337
+ paint(message);
338
+ }
339
+ function end() {
340
+ if (!bar) return;
341
+ try {
342
+ ui?.notifications?.remove?.(bar);
343
+ } catch {
344
+ }
345
+ bar = null;
346
+ phaseLabel = "";
347
+ }
348
+
105
349
  // ../foundry/scripts/media.mjs
106
350
  var CACHE_DIR = "vaults-cache";
107
351
  var BATCH_SIZE2 = 25;
@@ -111,6 +355,7 @@ function isCacheable(path) {
111
355
  if (!CACHED_EXT_RE.test(path)) return false;
112
356
  if (path.startsWith("_") || path.includes("/_")) return false;
113
357
  if (/\.preview\.json$/i.test(path)) return false;
358
+ if (/(^|\/)module\.json$/i.test(path)) return false;
114
359
  return true;
115
360
  }
116
361
  function localFileUrl(vaultId, vaultPath) {
@@ -178,6 +423,7 @@ async function syncImages(host, vault, manifestFiles) {
178
423
  chunkBytes += bytes;
179
424
  }
180
425
  if (chunk.length > 0) chunks.push(chunk);
426
+ phase("Images", toDownload.length);
181
427
  let next = 0;
182
428
  const downloaded = [];
183
429
  const errors = [];
@@ -194,6 +440,7 @@ async function syncImages(host, vault, manifestFiles) {
194
440
  continue;
195
441
  }
196
442
  try {
443
+ step(path.split("/").pop());
197
444
  await uploadToWorld(baseDir, path, blob);
198
445
  downloaded.push(path);
199
446
  } catch (err) {
@@ -420,23 +667,15 @@ function buildPathIndex(manifestFiles) {
420
667
  for (const f of manifestFiles) if (f.hash) hashes.set(f.path, f.hash);
421
668
  return { paths, idOverrides, docTargets, hashes };
422
669
  }
423
- function docNameFromBase(base) {
424
- if (typeof base !== "string" || !base) return null;
425
- if (base.startsWith("Compendium.")) {
426
- const parts = base.split(".");
427
- return parts.length >= 5 ? parts[3] : null;
428
- }
429
- return base.split(":")[0] || null;
430
- }
431
- async function targetUuid(vaultId, path, index) {
670
+ async function targetUuid(vault, path, index) {
432
671
  const docName = index.docTargets?.get(path);
433
672
  if (docName) {
434
- const id = index.idOverrides?.get(path) ?? await instanceId(vaultId, path);
435
- return `${docName}.${id}`;
673
+ const id = index.idOverrides?.get(path) ?? await instanceId(vault.id, path);
674
+ return instanceUuid(vault, docName, id);
436
675
  }
437
- const eId = await entryId(vaultId, path);
438
- const pId = index.idOverrides?.get(path) ?? await pageId(vaultId, path);
439
- return `JournalEntry.${eId}.JournalEntryPage.${pId}`;
676
+ const eId = await entryId(vault.id, path);
677
+ const pId = index.idOverrides?.get(path) ?? await pageId(vault.id, path);
678
+ return journalPageUuid(vault, eId, pId);
440
679
  }
441
680
  function logicalPathFromHref(href) {
442
681
  const decoded = decodeHtmlEntities(href);
@@ -453,7 +692,7 @@ function decodeHtmlEntities(s) {
453
692
  return ta.value;
454
693
  }
455
694
  async function transformHtmlForFoundry(vault, html, index, mediaRefs) {
456
- html = await rewriteWikilinks(vault.id, html, index);
695
+ html = await rewriteWikilinks(vault, html, index);
457
696
  html = rewriteMediaSrcs(vault.id, html, index?.hashes, mediaRefs);
458
697
  html = rewritePassthroughLinks(vault.id, html);
459
698
  html = await applyDomTransforms(html, vault, index);
@@ -465,7 +704,7 @@ async function applyDomTransforms(html, vault, index) {
465
704
  touched = stripWebOnlyWidgets(doc) || touched;
466
705
  touched = flattenBasesTabs(doc) || touched;
467
706
  touched = neutralizeEnrichersInCode(doc) || touched;
468
- touched = await rewriteBasesCardLinks(doc, vault.id, index) || touched;
707
+ touched = await rewriteBasesCardLinks(doc, vault, index) || touched;
469
708
  touched = rewriteDiceButtons(doc) || touched;
470
709
  touched = wrapRestrictedCalloutsAsSecret(doc, vault) || touched;
471
710
  return touched ? doc.body.innerHTML : html;
@@ -519,7 +758,18 @@ function neutralizeEnrichersInCode(doc) {
519
758
  }
520
759
  return touched;
521
760
  }
522
- async function rewriteBasesCardLinks(doc, vaultId, index) {
761
+ function contentLinkAttrs(uuid) {
762
+ return {
763
+ // Empty string, matching what TextEditor.createAnchor emits: the selector
764
+ // tests for presence, not value.
765
+ "data-link": "",
766
+ "data-uuid": uuid,
767
+ // Real content links are draggable, and the drag handler keys off the same
768
+ // attribute; without this a card cannot be dropped onto the canvas.
769
+ draggable: "true"
770
+ };
771
+ }
772
+ async function rewriteBasesCardLinks(doc, vault, index) {
523
773
  const cards = doc.querySelectorAll("a.bases-card[href]");
524
774
  if (cards.length === 0) return false;
525
775
  let touched = false;
@@ -529,23 +779,25 @@ async function rewriteBasesCardLinks(doc, vaultId, index) {
529
779
  const path = logicalPathFromHref(href);
530
780
  if (!index.paths.has(path)) continue;
531
781
  a.classList.add("content-link");
532
- a.setAttribute("data-uuid", await targetUuid(vaultId, path, index));
782
+ for (const [k, v] of Object.entries(contentLinkAttrs(await targetUuid(vault, path, index)))) {
783
+ a.setAttribute(k, v);
784
+ }
533
785
  a.removeAttribute("href");
534
786
  touched = true;
535
787
  }
536
788
  return touched;
537
789
  }
538
790
  function wrapRestrictedCalloutsAsSecret(doc, vault) {
539
- if (!vault?.dmRole || !Array.isArray(vault.knownRoles) || vault.knownRoles.length === 0) {
791
+ if (!vault?.playerRole || !Array.isArray(vault.knownRoles) || vault.knownRoles.length === 0) {
540
792
  return false;
541
793
  }
542
- const dmIdx = vault.knownRoles.indexOf(vault.dmRole);
543
- if (dmIdx < 0) return false;
544
- const restrictedRoles = vault.knownRoles.slice(dmIdx);
794
+ const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
795
+ if (playerIdx < 0) return false;
796
+ const restrictedRoles = vault.knownRoles.slice(playerIdx + 1);
545
797
  if (restrictedRoles.length === 0) return false;
546
798
  let touched = false;
547
799
  for (const role of restrictedRoles) {
548
- for (const el of doc.querySelectorAll(".callout.callout-" + cssEscape(role))) {
800
+ for (const el of doc.querySelectorAll(calloutSelectorFor(role))) {
549
801
  const section = doc.createElement("section");
550
802
  section.className = "secret";
551
803
  el.parentNode.insertBefore(section, el);
@@ -555,10 +807,13 @@ function wrapRestrictedCalloutsAsSecret(doc, vault) {
555
807
  }
556
808
  return touched;
557
809
  }
810
+ function calloutSelectorFor(role) {
811
+ return ".callout.callout-" + cssEscape(String(role).toLowerCase());
812
+ }
558
813
  function cssEscape(s) {
559
814
  return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
560
815
  }
561
- async function rewriteWikilinks(vaultId, html, index) {
816
+ async function rewriteWikilinks(vault, html, index) {
562
817
  const matches = [];
563
818
  let m;
564
819
  ANCHOR_RE.lastIndex = 0;
@@ -579,7 +834,7 @@ async function rewriteWikilinks(vaultId, html, index) {
579
834
  }
580
835
  const uuidMatches = matches.filter((r) => r.kind === "uuid");
581
836
  const resolved = await Promise.all(
582
- uuidMatches.map((r) => targetUuid(vaultId, r.path, index))
837
+ uuidMatches.map((r) => targetUuid(vault, r.path, index))
583
838
  );
584
839
  uuidMatches.forEach((r, i) => {
585
840
  r.uuid = resolved[i];
@@ -624,46 +879,17 @@ function stripTags(s) {
624
879
  // ../foundry/scripts/importer.mjs
625
880
  var INDEX_BASENAME = "index";
626
881
  var NON_INDEX_SORT_BASE = 1e5;
627
- var KNOWN_INSTANCE_DOC_TYPES = [
628
- "Actor",
629
- "Item",
630
- "Scene",
631
- "JournalEntry",
632
- "RollTable",
633
- "Macro",
634
- "Cards",
635
- "Playlist"
636
- ];
637
- function inferInstanceDocName(base) {
638
- if (typeof base !== "string" || !base) return null;
639
- const raw = base.includes(".") ? base.split(".").at(-2) : base.split(":")[0];
640
- return KNOWN_INSTANCE_DOC_TYPES.find((t) => t.toLowerCase() === raw?.toLowerCase()) ?? null;
641
- }
642
- async function ensureFolderChain(vault, segments) {
643
- const rootName = vault.rootFolder || vault.label || "Vault";
644
- const rootKey = `${vault.id}/__root__/${rootName}`;
645
- const rootFId = await folderId(vault.id, rootKey);
646
- await upsertFolder(rootFId, rootName, null);
647
- let parentId = rootFId;
648
- let acc = rootKey;
882
+ async function ensureFolderChain(target, vault, segments) {
883
+ let parentId = null;
884
+ let acc = `${vault.id}/__root__/${vault.rootFolder || vault.label || "Vault"}`;
649
885
  for (const seg of segments) {
650
886
  acc += "/" + seg;
651
887
  const fId = await folderId(vault.id, acc);
652
- await upsertFolder(fId, seg, parentId);
888
+ await target.putFolder("JournalEntry", { _id: fId, name: seg, folder: parentId });
653
889
  parentId = fId;
654
890
  }
655
891
  return parentId;
656
892
  }
657
- async function upsertFolder(id, name, parentId) {
658
- const existing = game.folders.get(id);
659
- if (existing) {
660
- if (existing.name !== name || existing.folder?.id !== parentId) {
661
- await existing.update({ name, folder: parentId });
662
- }
663
- return existing;
664
- }
665
- return Folder.create({ _id: id, name, type: "JournalEntry", folder: parentId }, { keepId: true });
666
- }
667
893
  function buildFolderInfo(mdPaths) {
668
894
  const map = /* @__PURE__ */ new Map();
669
895
  const ensure = (folderPath) => {
@@ -689,7 +915,7 @@ function buildFolderInfo(mdPaths) {
689
915
  function isIndexFile(filename) {
690
916
  return filename.replace(/\.md$/i, "") === INDEX_BASENAME;
691
917
  }
692
- async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs) {
918
+ async function upsertFile(target, vault, path, body, index, meta, folderInfo, mediaRefs) {
693
919
  let html = await transformHtmlForFoundry(vault, body, index, mediaRefs);
694
920
  html = await appendInstanceDocLink(html, vault, path, meta);
695
921
  const segs = path.split("/");
@@ -698,7 +924,7 @@ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs)
698
924
  const fInfo = folderInfo?.get(folderPath);
699
925
  const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
700
926
  const hostSegs = leaf ? segs.slice(0, -1) : segs;
701
- const folderFId = await ensureFolderChain(vault, hostSegs);
927
+ const folderFId = await ensureFolderChain(target, vault, hostSegs);
702
928
  const entryName = folderPath === "" ? vault.rootFolder || vault.label || "Vault" : fInfo?.displayName || segs[segs.length - 1] || "";
703
929
  const pageName = meta?.title || filename.replace(/\.md$/i, "");
704
930
  const eId = await entryId(vault.id, path);
@@ -726,311 +952,555 @@ async function upsertFile(vault, path, body, index, meta, folderInfo, mediaRefs)
726
952
  // page in it). See reconcileOwnership below.
727
953
  ...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
728
954
  };
729
- const existing = game.journal.get(eId);
955
+ const existing = await target.get("JournalEntry", eId);
730
956
  if (existing) {
731
- const entryPatch = {};
732
- if (existing.name !== entryName) entryPatch.name = entryName;
733
- if (existing.folder?.id !== folderFId) entryPatch.folder = folderFId;
734
- if (Object.keys(entryPatch).length > 0) await existing.update(entryPatch);
735
- const existingPage = existing.pages.get(pId);
736
- if (existingPage) await existingPage.update(pageData);
737
- else await existing.createEmbeddedDocuments("JournalEntryPage", [pageData], { keepId: true });
957
+ const pages = (existing.pages ?? []).filter((pg) => pg._id !== pId);
958
+ pages.push(pageData);
959
+ await target.put("JournalEntry", {
960
+ ...existing,
961
+ name: entryName,
962
+ folder: folderFId,
963
+ pages,
964
+ flags
965
+ });
738
966
  return "modified";
739
967
  }
740
- await JournalEntry.create({
968
+ return target.put("JournalEntry", {
741
969
  _id: eId,
742
970
  name: entryName,
743
971
  folder: folderFId,
744
972
  pages: [pageData],
745
973
  flags,
746
- // Bootstrap the entry visible at the page's tier so player-visible
747
- // pages aren't hidden in the gap between create and the post-sync
748
- // reconcileOwnership pass. Mixed-tier folders converge to max(pages)
749
- // via reconcile.
974
+ // Bootstrap the entry visible at the page's tier so player-visible pages
975
+ // aren't hidden in the gap before reconcileEntries runs. Mixed-tier
976
+ // folders converge to max(pages) there.
750
977
  ...pageOwnership !== null ? { ownership: { default: pageOwnership } } : {}
751
- }, { keepId: true });
752
- return "added";
978
+ });
753
979
  }
754
980
  function pageOwnershipLevelFor(vault, pageRole) {
755
- if (!vault.dmRole || !vault.knownRoles?.length) return null;
756
- const dmIdx = vault.knownRoles.indexOf(vault.dmRole);
757
- if (dmIdx < 0) return null;
981
+ if (!vault.playerRole || !vault.knownRoles?.length) return null;
982
+ const playerIdx = vault.knownRoles.indexOf(vault.playerRole);
983
+ if (playerIdx < 0) return null;
758
984
  const pageIdx = pageRole ? vault.knownRoles.indexOf(pageRole) : -1;
759
985
  const effectiveIdx = pageIdx < 0 ? 0 : pageIdx;
760
- return effectiveIdx < dmIdx ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER : CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
986
+ return effectiveIdx <= playerIdx ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER : CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
761
987
  }
762
- async function reconcileOwnership(vault, bodyMetaIndex) {
763
- if (!vault.dmRole || !vault.knownRoles?.length) return;
764
- const ours = game.journal.contents.filter(
765
- (j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
766
- );
988
+ async function reconcileEntries(target, vault, folderInfo, bodyMetaIndex) {
989
+ const ours = (await target.contents("JournalEntry")).filter((j) => j.flags?.[MODULE_ID]?.vaultId === vault.id);
990
+ const gated = !!vault.playerRole && !!vault.knownRoles?.length;
767
991
  for (const entry of ours) {
768
- let entryMax = null;
769
- for (const page of entry.pages.contents) {
770
- const pPath = page.getFlag(MODULE_ID, "path");
771
- if (!pPath) continue;
772
- const bodyPath = pPath.replace(/\.md$/i, ".body.html");
773
- const meta = bodyMetaIndex.get(bodyPath);
774
- if (!meta) continue;
775
- const level = pageOwnershipLevelFor(vault, meta.role);
776
- if (level === null) continue;
777
- if (page.ownership?.default !== level) {
778
- try {
779
- await page.update({ ownership: { default: level } });
780
- } catch (err) {
781
- console.warn(`Vaults | reconcile ownership ${pPath} \u2192 ${level} failed:`, err);
782
- }
783
- }
784
- if (entryMax === null || level > entryMax) entryMax = level;
785
- }
786
- if (entryMax !== null && entry.ownership?.default !== entryMax) {
992
+ const patched = reconcileOwnershipData(gated ? vault : null, entry, bodyMetaIndex);
993
+ const folder = await expectedFolder(target, vault, entry, folderInfo);
994
+ if (folder !== void 0 && folder !== (patched.folder ?? null)) patched.folder = folder;
995
+ if (JSON.stringify(patched) !== JSON.stringify(entry)) {
787
996
  try {
788
- await entry.update({ ownership: { default: entryMax } });
997
+ await target.put("JournalEntry", patched);
789
998
  } catch (err) {
790
- console.warn(`Vaults | reconcile entry ownership for ${entry.name} \u2192 ${entryMax} failed:`, err);
999
+ console.warn(`Vaults | reconcile failed for ${entry.name}:`, err);
791
1000
  }
792
1001
  }
793
1002
  }
794
1003
  }
795
- async function reconcileEntryPlacement(vault, folderInfo) {
796
- const ours = game.journal.contents.filter(
797
- (j) => j.getFlag(MODULE_ID, "vaultId") === vault.id
798
- );
799
- for (const entry of ours) {
800
- const firstPage = entry.pages.contents[0];
801
- const path = firstPage?.getFlag(MODULE_ID, "path");
802
- if (!path) continue;
803
- const segs = path.split("/");
804
- segs.pop();
805
- const folderPath = segs.join("/");
806
- const fInfo = folderInfo?.get(folderPath);
807
- const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
808
- const hostSegs = leaf ? segs.slice(0, -1) : segs;
809
- const expectedFolderId = await ensureFolderChain(vault, hostSegs);
810
- if (entry.folder?.id !== expectedFolderId) {
811
- try {
812
- await entry.update({ folder: expectedFolderId });
813
- } catch (err) {
814
- console.warn(`Vaults | re-place ${path} \u2192 ${expectedFolderId} failed:`, err);
815
- }
816
- }
817
- }
1004
+ function reconcileOwnershipData(vault, entry, bodyMetaIndex) {
1005
+ const patched = structuredClone(entry);
1006
+ if (!vault) return patched;
1007
+ let entryMax = null;
1008
+ for (const page of patched.pages ?? []) {
1009
+ const pPath = page.flags?.[MODULE_ID]?.path;
1010
+ if (!pPath) continue;
1011
+ const meta = bodyMetaIndex.get(pPath.replace(/\.md$/i, ".body.html"));
1012
+ if (!meta) continue;
1013
+ const level = pageOwnershipLevelFor(vault, meta.role);
1014
+ if (level === null) continue;
1015
+ page.ownership = { ...page.ownership, default: level };
1016
+ if (entryMax === null || level > entryMax) entryMax = level;
1017
+ }
1018
+ if (entryMax !== null) patched.ownership = { ...patched.ownership, default: entryMax };
1019
+ return patched;
1020
+ }
1021
+ async function expectedFolder(target, vault, entry, folderInfo) {
1022
+ const path = entry.pages?.[0]?.flags?.[MODULE_ID]?.path;
1023
+ if (!path) return void 0;
1024
+ const segs = path.split("/");
1025
+ segs.pop();
1026
+ const folderPath = segs.join("/");
1027
+ const fInfo = folderInfo?.get(folderPath);
1028
+ const leaf = !!fInfo && folderPath !== "" && !fInfo.hasSubfolders;
1029
+ return ensureFolderChain(target, vault, leaf ? segs.slice(0, -1) : segs);
818
1030
  }
819
- async function deleteFile(vault, path) {
1031
+ async function deleteFile(target, vault, path) {
820
1032
  const eId = await entryId(vault.id, path);
821
1033
  const pId = await pageId(vault.id, path);
822
- const entry = game.journal.get(eId);
1034
+ const entry = await target.get("JournalEntry", eId);
823
1035
  if (!entry) return;
824
- const page = entry.pages.get(pId);
825
- if (page) await page.delete();
826
- if (entry.pages.size === 0) await entry.delete();
827
- }
828
- async function deleteVaultJournals(vaultId) {
829
- const journals = game.journal.contents.filter((j) => j.getFlag(MODULE_ID, "vaultId") === vaultId);
830
- for (const j of journals) await j.delete();
831
- const folders = game.folders.contents.filter((f) => f.type === "JournalEntry");
832
- for (const f of folders.reverse()) {
833
- if (f.contents.length === 0 && f.children.length === 0) {
834
- const root = await folderId(vaultId, `${vaultId}/__root__/${f.name}`);
835
- if (f.id === root) await f.delete();
836
- }
837
- }
1036
+ const pages = (entry.pages ?? []).filter((pg) => pg._id !== pId);
1037
+ if (pages.length === 0) await target.remove("JournalEntry", eId);
1038
+ else await target.put("JournalEntry", { ...entry, pages });
838
1039
  }
839
1040
  async function appendInstanceDocLink(html, vault, path, meta) {
840
1041
  const base = meta?.foundry?.base;
841
- const docName = inferInstanceDocName(base);
1042
+ const docName = docNameFromBase(base);
842
1043
  if (!docName) return html;
843
1044
  const idOverride = meta?.foundry?.id;
844
1045
  const docId = typeof idOverride === "string" && idOverride ? idOverride : await instanceId(vault.id, path);
845
1046
  const label = meta?.title || path.split("/").pop().replace(/\.md$/i, "");
1047
+ const uuid = instanceUuid(vault, docName, docId);
846
1048
  return html + `
847
- <p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${docName}.${docId}]{${escapeBraces(label)}}</p>`;
1049
+ <p class="vaults-instance-link"><em>Foundry document:</em> @UUID[${uuid}]{${escapeBraces(label)}}</p>`;
1050
+ }
1051
+
1052
+ // ../foundry/scripts/moulinette.mjs
1053
+ var MOULINETTE_PREFIX = "@moulinette/";
1054
+ var CACHED_COLLECTION = "mou-cloud-cached";
1055
+ function parseMoulinetteRef(s) {
1056
+ if (typeof s !== "string" || !s.startsWith(MOULINETTE_PREFIX)) return null;
1057
+ const [pack, ...fileParts] = s.slice(MOULINETTE_PREFIX.length).split("/");
1058
+ const file = fileParts.join("/");
1059
+ if (!pack || !file) return null;
1060
+ return { pack, file };
1061
+ }
1062
+ async function loadIndex(log) {
1063
+ const mod = game.modules?.get("moulinette");
1064
+ if (!mod) {
1065
+ log("references need the Moulinette module, which is not installed");
1066
+ return null;
1067
+ }
1068
+ if (!mod.active) {
1069
+ log("references need the Moulinette module, which is installed but not enabled in this world");
1070
+ return null;
1071
+ }
1072
+ const collection = mod.collections?.find((c) => c.getId?.() === CACHED_COLLECTION);
1073
+ if (!collection?.initialize || !collection.selectAsset || !collection.downloadAsset) {
1074
+ log("Moulinette is installed but its asset index is not where we expect; skipping");
1075
+ return null;
1076
+ }
1077
+ try {
1078
+ await collection.initialize();
1079
+ } catch (err) {
1080
+ log(`could not load the Moulinette index: ${err?.message ?? err}`);
1081
+ return null;
1082
+ }
1083
+ const assets = mod.cache?.allAssets;
1084
+ if (!Array.isArray(assets)) {
1085
+ log("Moulinette's asset index is not a list; skipping");
1086
+ return null;
1087
+ }
1088
+ return { mod, collection, assets };
1089
+ }
1090
+ function findAsset(ref, index, log) {
1091
+ const match = index.assets.find(
1092
+ (a) => String(a?.pack_id) === ref.pack && a?.url === ref.file
1093
+ );
1094
+ if (!match) {
1095
+ log(`no asset ${ref.file} in pack ${ref.pack} \u2014 not subscribed, or it moved`);
1096
+ return null;
1097
+ }
1098
+ return match;
1099
+ }
1100
+ async function resolveMoulinetteDocument(spec, warn) {
1101
+ const log = (msg) => warn(msg);
1102
+ const ref = parseMoulinetteRef(MOULINETTE_PREFIX + spec);
1103
+ if (!ref) {
1104
+ log(`malformed reference '${spec}' \u2014 expected <pack_ref>/<filepath>`);
1105
+ return null;
1106
+ }
1107
+ const index = await loadIndex(log);
1108
+ if (!index) return null;
1109
+ const asset = findAsset(ref, index, log);
1110
+ if (!asset) return null;
1111
+ try {
1112
+ const descriptor = await index.mod.cloudclient.apiGET(`/asset/${asset.id}`, {
1113
+ session: index.mod.getSessionId()
1114
+ });
1115
+ const dl = await index.collection.downloadAsset(descriptor);
1116
+ if (!dl?.message) {
1117
+ log(`${ref.pack}/${ref.file} is not a document; foundry.base needs one`);
1118
+ return null;
1119
+ }
1120
+ return JSON.parse(dl.message);
1121
+ } catch (err) {
1122
+ log(`could not read ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
1123
+ return null;
1124
+ }
1125
+ }
1126
+ async function resolveOne(ref, index, log) {
1127
+ const match = findAsset(ref, index, log);
1128
+ if (!match) return null;
1129
+ try {
1130
+ note(`Moulinette: ${ref.file.split("/").pop()}`);
1131
+ const path = await index.collection.selectAsset(match);
1132
+ if (!path) {
1133
+ log(`${ref.pack}/${ref.file} is not a media asset; only files can be referenced`);
1134
+ return null;
1135
+ }
1136
+ return path;
1137
+ } catch (err) {
1138
+ log(`download failed for ${ref.pack}/${ref.file}: ${err?.message ?? err}`);
1139
+ return null;
1140
+ }
1141
+ }
1142
+ async function resolveMoulinetteRefs(value, warn) {
1143
+ const cache = /* @__PURE__ */ new Map();
1144
+ const stats = { resolved: 0, unresolved: 0 };
1145
+ const seenWarnings = /* @__PURE__ */ new Set();
1146
+ const log = (msg) => {
1147
+ if (seenWarnings.has(msg)) return;
1148
+ seenWarnings.add(msg);
1149
+ warn(msg);
1150
+ };
1151
+ let index;
1152
+ const lookup = async (s) => {
1153
+ if (cache.has(s)) return cache.get(s);
1154
+ const ref = parseMoulinetteRef(s);
1155
+ let path = null;
1156
+ if (!ref) {
1157
+ log(`malformed reference '${s}' \u2014 expected @moulinette/<pack_ref>/<filepath>`);
1158
+ } else {
1159
+ if (index === void 0) index = await loadIndex(log);
1160
+ if (index) path = await resolveOne(ref, index, log);
1161
+ }
1162
+ cache.set(s, path);
1163
+ if (path) stats.resolved++;
1164
+ else stats.unresolved++;
1165
+ return path;
1166
+ };
1167
+ const walk = async (node) => {
1168
+ if (Array.isArray(node)) {
1169
+ const kept = [];
1170
+ for (const item of node) {
1171
+ if (typeof item === "string" && item.startsWith(MOULINETTE_PREFIX)) {
1172
+ const path = await lookup(item);
1173
+ if (path) kept.push(path);
1174
+ continue;
1175
+ }
1176
+ if (await walk(item)) kept.push(item);
1177
+ }
1178
+ node.length = 0;
1179
+ node.push(...kept);
1180
+ return true;
1181
+ }
1182
+ if (node && typeof node === "object") {
1183
+ let viable = true;
1184
+ for (const key of Object.keys(node)) {
1185
+ const v = node[key];
1186
+ if (typeof v === "string" && v.startsWith(MOULINETTE_PREFIX)) {
1187
+ const path = await lookup(v);
1188
+ if (path) node[key] = path;
1189
+ else {
1190
+ delete node[key];
1191
+ viable = false;
1192
+ }
1193
+ continue;
1194
+ }
1195
+ if (!await walk(v)) delete node[key];
1196
+ }
1197
+ return viable;
1198
+ }
1199
+ return true;
1200
+ };
1201
+ await walk(value);
1202
+ return stats;
848
1203
  }
849
1204
 
850
1205
  // ../foundry/scripts/instance.mjs
1206
+ var CORE_DESCRIPTION_FIELDS = {
1207
+ RollTable: "description",
1208
+ Playlist: "description"
1209
+ };
1210
+ function descriptionPathFor(docName, systemId) {
1211
+ return CORE_DESCRIPTION_FIELDS[docName] ?? DESCRIPTION_FIELDS[systemId]?.[docName];
1212
+ }
851
1213
  var DESCRIPTION_FIELDS = {
852
1214
  dnd5e: {
853
1215
  Actor: "system.details.biography.value",
854
1216
  Item: "system.description.value"
855
1217
  }
856
1218
  };
857
- var CLONE_SUPPORTED_DOCS = /* @__PURE__ */ new Set(["Actor", "Item"]);
858
- var BLANK_DOC_TYPES = /* @__PURE__ */ new Set([
859
- "Actor",
860
- "Item",
861
- "Scene",
862
- "JournalEntry",
863
- "RollTable",
864
- "Macro",
865
- "Cards",
866
- "Playlist"
867
- ]);
868
- var COLLECTION_FOR = {
869
- Actor: () => game.actors,
870
- Item: () => game.items,
871
- Scene: () => game.scenes,
872
- JournalEntry: () => game.journal,
873
- RollTable: () => game.tables,
874
- Macro: () => game.macros,
875
- Cards: () => game.cards,
876
- Playlist: () => game.playlists
877
- };
878
- async function applyInstance(vault, vaultPath, meta) {
1219
+ function sceneBackgroundSrc(data) {
1220
+ const levels = Array.isArray(data?.levels) ? data.levels : [];
1221
+ const initial = levels.find((l) => l._id === data.initialLevel) ?? levels[0];
1222
+ return initial?.background?.src || data?.background?.src || null;
1223
+ }
1224
+ async function attachSceneThumb(data, docName, vaultPath) {
1225
+ if (docName !== "Scene" || data.thumb) return;
1226
+ const src = sceneBackgroundSrc(data);
1227
+ if (!src) return;
1228
+ try {
1229
+ const { thumb } = await foundry.helpers.media.ImageHelper.createThumbnail(
1230
+ src,
1231
+ { width: 300, height: 100 }
1232
+ );
1233
+ if (thumb) data.thumb = thumb;
1234
+ } catch (err) {
1235
+ console.warn(`Vaults | could not make a scene thumbnail for ${vaultPath}:`, err);
1236
+ }
1237
+ }
1238
+ function wantsJournalNote(docName, fm) {
1239
+ return docName === "Scene" && fm?.journal !== false;
1240
+ }
1241
+ async function applyInstance(target, vault, vaultPath, meta, { forceFull = false } = {}) {
879
1242
  const fm = meta?.foundry;
880
- if (!fm || typeof fm !== "object") return;
881
- const parsed = parseFoundryBase(fm.base);
882
- if (!parsed) return;
883
- let docName;
884
- let baseData;
885
- if (parsed.kind === "uuid") {
886
- const template = await safeFromUuid(parsed.uuid);
887
- if (!template) {
888
- console.warn(`Vaults | foundry.base: ${vaultPath} \u2192 ${parsed.uuid} did not resolve; skipping.`);
889
- return;
890
- }
891
- docName = template.documentName;
892
- if (!CLONE_SUPPORTED_DOCS.has(docName)) {
893
- console.warn(
894
- `Vaults | foundry.base: ${vaultPath} \u2192 ${parsed.uuid} is a ${docName}; clone-from-UUID only supports ${[...CLONE_SUPPORTED_DOCS].join(", ")}.`
895
- );
896
- return;
897
- }
898
- try {
899
- baseData = template.toObject();
900
- } catch (err) {
901
- console.warn(`Vaults | foundry.base: could not read template ${parsed.uuid}:`, err);
902
- return;
903
- }
904
- delete baseData._id;
905
- } else {
906
- docName = parsed.docName;
907
- baseData = parsed.subtype ? { type: parsed.subtype } : {};
908
- }
909
- const collection = COLLECTION_FOR[docName]?.();
910
- if (!collection) {
911
- console.warn(`Vaults | foundry.base: no world collection for ${docName}; skipping ${vaultPath}.`);
912
- return;
1243
+ if (!fm || typeof fm !== "object") return null;
1244
+ if (fm.base === void 0 || fm.base === null) return null;
1245
+ const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
1246
+ const candidates = specs.map(parseFoundryBase);
1247
+ if (candidates.length === 0 || candidates.some((c) => !c)) {
1248
+ console.warn(
1249
+ `Vaults | foundry.base: ${vaultPath} \u2192 unrecognised base ${JSON.stringify(fm.base)}; expected a UUID ("Compendium.<pkg>.<pack>.Actor.<id>") or a type ("Actor:npc"), or a list of those. Skipping.`
1250
+ );
1251
+ return { ok: false, reason: "unparseable" };
1252
+ }
1253
+ const docNames = new Set(candidates.map(docNameOf).filter(Boolean));
1254
+ const docName = [...docNames][0] ?? null;
1255
+ if (!docName) {
1256
+ console.warn(`Vaults | foundry.base: ${vaultPath} \u2192 could not read a document type from ${JSON.stringify(fm.base)}. Skipping.`);
1257
+ return { ok: false, reason: "unparseable" };
1258
+ }
1259
+ if (docNames.size > 1) {
1260
+ console.warn(
1261
+ `Vaults | foundry.base: ${vaultPath} \u2192 every entry must name the same document type, got ${[...docNames].join(", ")}. Skipping.`
1262
+ );
1263
+ return { ok: false, reason: "mixed-types" };
1264
+ }
1265
+ if (!CONFIG[docName]) {
1266
+ console.warn(`Vaults | foundry.base: this world has no ${docName} documents; skipping ${vaultPath}.`);
1267
+ return { ok: false, reason: "no-collection" };
913
1268
  }
914
- const docClass = CONFIG[docName].documentClass;
915
1269
  const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, vaultPath);
916
1270
  const dataJson = fm.data_json && typeof fm.data_json === "object" && !Array.isArray(fm.data_json) ? rewriteVaultPaths(structuredClone(fm.data_json), vault.id) : null;
917
- if (dataJson) await ensureEmbeddedIds(dataJson, vault.id, vaultPath);
1271
+ const moulinetteWarn = (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`);
1272
+ if (dataJson) await resolveMoulinetteRefs(dataJson, moulinetteWarn);
1273
+ if (dataJson) {
1274
+ await resolveItemUuids(dataJson, vaultPath);
1275
+ await ensureEmbeddedIds(dataJson, vault.id, vaultPath);
1276
+ }
918
1277
  const derived = {};
919
- const overlay = await buildOverlay(vault, vaultPath, meta, docName, derived);
1278
+ const overlay = await buildOverlay(target, vault, vaultPath, meta, docName, derived);
920
1279
  const tokenFloor = derived.tokenTexture && !dataJson?.prototypeToken?.texture?.src && !fm?.data?.prototypeToken?.texture?.src ? { prototypeToken: { texture: { src: derived.tokenTexture } } } : null;
921
- const existing = collection.get(id);
1280
+ const existing = await target.get(docName, id);
922
1281
  if (existing) {
923
1282
  const base = tokenFloor ? deepMerge(structuredClone(tokenFloor), dataJson ?? {}) : dataJson;
924
1283
  const updatePatch = base ? deepMerge(structuredClone(base), overlay) : overlay;
1284
+ if (wantsJournalNote(docName, fm)) {
1285
+ await attachJournalNote(updatePatch, {
1286
+ width: updatePatch.width ?? existing.width,
1287
+ height: updatePatch.height ?? existing.height,
1288
+ padding: updatePatch.padding ?? existing.padding,
1289
+ grid: updatePatch.grid ?? { size: existing.grid?.size }
1290
+ }, vault, vaultPath, meta);
1291
+ }
1292
+ if (!forceFull) delete updatePatch.folder;
925
1293
  try {
926
- await existing.update(updatePatch);
1294
+ const merged = deepMerge(structuredClone(existing), updatePatch);
1295
+ if (sceneBackgroundSrc(merged) !== sceneBackgroundSrc(existing) || !existing.thumb) {
1296
+ await attachSceneThumb(merged, docName, vaultPath);
1297
+ }
1298
+ await target.put(docName, merged);
927
1299
  } catch (err) {
928
1300
  console.warn(`Vaults | foundry.base update failed for ${vaultPath}:`, err);
1301
+ return { ok: false, reason: "update-failed" };
929
1302
  }
930
- return;
1303
+ return { ok: true, action: "updated" };
931
1304
  }
1305
+ const resolved = await resolveBase(candidates, vaultPath);
1306
+ if (!resolved) return { ok: false, reason: "unresolved" };
1307
+ const baseData = resolved.data;
1308
+ const baseItems = Array.isArray(baseData.items) ? baseData.items : null;
932
1309
  if (tokenFloor) deepMerge(baseData, tokenFloor);
933
1310
  if (dataJson) deepMerge(baseData, dataJson);
934
1311
  baseData._id = id;
935
1312
  deepMerge(baseData, overlay);
1313
+ if (wantsJournalNote(docName, fm)) {
1314
+ await attachJournalNote(baseData, baseData, vault, vaultPath, meta);
1315
+ }
1316
+ if (baseItems && baseData.items !== baseItems) {
1317
+ baseData.items = mergeItemsById(baseItems, baseData.items);
1318
+ }
936
1319
  try {
937
- await docClass.create(baseData, { keepId: true, keepEmbeddedIds: true });
1320
+ await attachSceneThumb(baseData, docName, vaultPath);
1321
+ await target.put(docName, baseData);
938
1322
  } catch (err) {
939
1323
  console.warn(`Vaults | foundry.base create failed for ${vaultPath}:`, err);
940
- return;
1324
+ return { ok: false, reason: "create-failed" };
941
1325
  }
942
- if (docName === "Scene") {
943
- const created = collection.get(id);
944
- if (created && !created.thumb) {
945
- try {
946
- const { thumb } = await created.createThumbnail();
947
- if (thumb) await created.update({ thumb });
948
- } catch (err) {
949
- console.warn(`Vaults | scene thumbnail generation failed for ${vaultPath}:`, err);
1326
+ const created = await target.get(docName, id);
1327
+ if (!created) {
1328
+ console.warn(
1329
+ `Vaults | foundry.base: ${vaultPath} \u2192 ${docName} ${id} was rejected on create; no document exists. Foundry logged the validation error separately (look for "DataModelValidationError" above).`
1330
+ );
1331
+ return { ok: false, reason: "create-rejected" };
1332
+ }
1333
+ if (docName === "Scene" && (!created.thumb || resolved.from)) {
1334
+ try {
1335
+ const { thumb } = await created.createThumbnail();
1336
+ if (thumb) await created.update({ thumb });
1337
+ else console.warn(`Vaults | scene thumbnail came back empty for ${vaultPath}`);
1338
+ } catch (err) {
1339
+ console.warn(`Vaults | scene thumbnail generation failed for ${vaultPath}:`, err);
1340
+ }
1341
+ }
1342
+ return { ok: true, action: "created", skew: resolved.skew ?? null };
1343
+ }
1344
+ function generationSkew(data, ref) {
1345
+ const exported = Number.parseInt(String(data?._stats?.coreVersion ?? ""), 10);
1346
+ const world = Number(game.release?.generation);
1347
+ if (!Number.isFinite(exported) || !Number.isFinite(world) || exported === world) return null;
1348
+ return { ref, exported: data._stats.coreVersion, world: game.release.generation };
1349
+ }
1350
+ async function resolveBase(candidates, vaultPath) {
1351
+ const tried = [];
1352
+ for (const parsed of candidates) {
1353
+ if (parsed.kind === "blank") {
1354
+ if (tried.length > 0) {
1355
+ console.info(
1356
+ `Vaults | foundry.base: ${vaultPath} \u2192 fell back to blank ${parsed.docName}${parsed.subtype ? `:${parsed.subtype}` : ""} after ${tried.length} earlier candidate(s) did not resolve.`
1357
+ );
1358
+ }
1359
+ return { data: parsed.subtype ? { type: parsed.subtype } : {}, from: null };
1360
+ }
1361
+ if (parsed.kind === "moulinette") {
1362
+ const data2 = await resolveMoulinetteDocument(
1363
+ parsed.ref,
1364
+ (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`)
1365
+ );
1366
+ if (!data2) {
1367
+ tried.push(`@moulinette/${parsed.ref} \u2014 did not resolve`);
1368
+ continue;
950
1369
  }
1370
+ const skew = generationSkew(data2, parsed.ref);
1371
+ delete data2._id;
1372
+ if (tried.length > 0) {
1373
+ console.info(
1374
+ `Vaults | foundry.base: ${vaultPath} \u2192 using @moulinette/${parsed.ref}; earlier candidate(s) skipped:
1375
+ ` + tried.join("\n ")
1376
+ );
1377
+ }
1378
+ return { data: data2, from: `@moulinette/${parsed.ref}`, skew };
1379
+ }
1380
+ const template = await safeFromUuid(parsed.uuid);
1381
+ if (!template) {
1382
+ tried.push(`${parsed.uuid} \u2014 did not resolve`);
1383
+ continue;
951
1384
  }
1385
+ if (!BLANK_DOC_TYPES.includes(template.documentName)) {
1386
+ tried.push(`${parsed.uuid} \u2014 is a ${template.documentName}; vaults can instantiate ${BLANK_DOC_TYPES.join(", ")}`);
1387
+ continue;
1388
+ }
1389
+ let data;
1390
+ try {
1391
+ data = template.toObject();
1392
+ } catch (err) {
1393
+ tried.push(`${parsed.uuid} \u2014 unreadable: ${err.message}`);
1394
+ continue;
1395
+ }
1396
+ delete data._id;
1397
+ const sourceUuid = template.uuid ?? parsed.uuid;
1398
+ if (sourceUuid.startsWith("Compendium.")) {
1399
+ data._stats = { ...data._stats, compendiumSource: sourceUuid };
1400
+ }
1401
+ const via = sourceUuid === parsed.uuid ? "" : ` (redirected to ${sourceUuid})`;
1402
+ if (tried.length > 0) {
1403
+ console.info(
1404
+ `Vaults | foundry.base: ${vaultPath} \u2192 using ${parsed.uuid}${via}; earlier candidate(s) skipped:
1405
+ ` + tried.join("\n ")
1406
+ );
1407
+ }
1408
+ return { data, from: sourceUuid };
952
1409
  }
1410
+ console.warn(
1411
+ `Vaults | foundry.base: ${vaultPath} \u2192 no candidate resolved:
1412
+ ` + tried.join("\n ") + `
1413
+ Add a blank-document entry (e.g. "Actor:npc") as the last item so this can't fail.`
1414
+ );
1415
+ return null;
1416
+ }
1417
+ async function missingBasePackages(metas) {
1418
+ const pageSpecs = [];
1419
+ for (const meta of metas) {
1420
+ const base = meta?.foundry?.base;
1421
+ if (base === void 0 || base === null) continue;
1422
+ const specs = (Array.isArray(base) ? base : [base]).filter((s) => typeof s === "string");
1423
+ if (specs.length === 0) continue;
1424
+ if (specs.some((s) => !s.includes("."))) continue;
1425
+ if (specs.some((s) => !s.startsWith("Compendium."))) continue;
1426
+ if (specs.some((s) => s.split(".")[1] === "world")) continue;
1427
+ pageSpecs.push(specs);
1428
+ }
1429
+ if (pageSpecs.length === 0) return /* @__PURE__ */ new Map();
1430
+ const PROBES_PER_PACK = 3;
1431
+ const packOf = (spec) => spec.split(".").slice(1, 3).join(".");
1432
+ const probes = /* @__PURE__ */ new Map();
1433
+ for (const specs of pageSpecs) {
1434
+ for (const spec of specs) {
1435
+ const pack = packOf(spec);
1436
+ const chosen = probes.get(pack) ?? [];
1437
+ if (chosen.length < PROBES_PER_PACK && !chosen.includes(spec)) chosen.push(spec);
1438
+ probes.set(pack, chosen);
1439
+ }
1440
+ }
1441
+ const reachable = /* @__PURE__ */ new Map();
1442
+ for (const [pack, specs] of probes) {
1443
+ let ok = false;
1444
+ for (const spec of specs) if (await safeFromUuid(spec)) {
1445
+ ok = true;
1446
+ break;
1447
+ }
1448
+ reachable.set(pack, ok);
1449
+ }
1450
+ const missing = /* @__PURE__ */ new Map();
1451
+ for (const specs of pageSpecs) {
1452
+ if (specs.some((s) => reachable.get(packOf(s)))) continue;
1453
+ for (const pkg of new Set(specs.map((s) => s.split(".")[1]).filter(Boolean))) {
1454
+ missing.set(pkg, (missing.get(pkg) ?? 0) + 1);
1455
+ }
1456
+ }
1457
+ return missing;
953
1458
  }
954
- function parseFoundryBase(spec) {
955
- if (typeof spec !== "string" || !spec) return null;
956
- if (spec.includes(".")) return { kind: "uuid", uuid: spec };
957
- const [typeRaw, subtype] = spec.split(":");
958
- const docName = [...BLANK_DOC_TYPES].find((t) => t.toLowerCase() === typeRaw.toLowerCase());
959
- if (!docName) return null;
960
- return { kind: "blank", docName, subtype: subtype || void 0 };
961
- }
962
- async function deleteInstance(vault, vaultPath) {
1459
+ async function deleteInstance(target, vault, vaultPath) {
963
1460
  const id = await instanceId(vault.id, vaultPath);
964
- for (const getCollection of Object.values(COLLECTION_FOR)) {
965
- const collection = getCollection();
966
- const doc = collection?.get(id);
1461
+ for (const docName of BLANK_DOC_TYPES) {
1462
+ const doc = await target.get(docName, id);
967
1463
  if (!doc) continue;
968
- if (doc.getFlag(MODULE_ID, "vaultId") !== vault.id) continue;
1464
+ if (doc.flags?.[MODULE_ID]?.vaultId !== vault.id) continue;
969
1465
  try {
970
- await doc.delete();
1466
+ await target.remove(docName, id);
971
1467
  } catch (err) {
972
- console.warn(`Vaults | failed to delete ${doc.documentName} for ${vaultPath}:`, err);
1468
+ console.warn(`Vaults | failed to delete ${docName} for ${vaultPath}:`, err);
973
1469
  }
974
1470
  }
975
1471
  }
976
- async function deleteVaultInstances(vaultId) {
977
- for (const [docName, getCollection] of Object.entries(COLLECTION_FOR)) {
978
- const collection = getCollection();
979
- if (!collection) continue;
980
- const ours = collection.contents.filter((d) => d.getFlag(MODULE_ID, "vaultId") === vaultId);
981
- for (const doc of ours) {
982
- try {
983
- await doc.delete();
984
- } catch (err) {
985
- console.warn(`Vaults | failed to delete ${docName} ${doc.id}:`, err);
986
- }
987
- }
988
- }
989
- for (const docName of BLANK_DOC_TYPES) {
990
- const fId = await instanceFolderId(vaultId, docName);
991
- const folder = game.folders.get(fId);
992
- if (!folder || folder.type !== docName) continue;
993
- if (folder.contents.length > 0 || folder.children.length > 0) continue;
1472
+ async function ensureInstanceFolder(target, vault, docName, subPath = "") {
1473
+ let parentId = null;
1474
+ let key = `${vault.id}/__instance__/${docName}`;
1475
+ for (const segment of splitFolderPath(subPath)) {
1476
+ key += `/${segment}`;
1477
+ const fId = await folderId(vault.id, key);
994
1478
  try {
995
- await folder.delete();
1479
+ await target.putFolder(docName, { _id: fId, name: segment, folder: parentId });
996
1480
  } catch (err) {
997
- console.warn(`Vaults | failed to delete ${docName} folder:`, err);
1481
+ console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
1482
+ return parentId;
998
1483
  }
1484
+ parentId = fId;
999
1485
  }
1486
+ return parentId;
1000
1487
  }
1001
- async function instanceFolderId(vaultId, docName) {
1002
- return folderId(vaultId, `${vaultId}/__instance__/${docName}`);
1488
+ function splitFolderPath(subPath) {
1489
+ return typeof subPath === "string" ? subPath.split("/").map((s) => s.trim()).filter(Boolean) : [];
1003
1490
  }
1004
- async function ensureInstanceFolder(vault, docName) {
1005
- const fId = await instanceFolderId(vault.id, docName);
1006
- const existing = game.folders.get(fId);
1007
- const name = vault.rootFolder || vault.label || "Vault";
1008
- if (existing) {
1009
- if (existing.name !== name) {
1010
- try {
1011
- await existing.update({ name });
1012
- } catch (err) {
1013
- console.warn(`Vaults | could not rename ${docName} folder for ${vault.label}:`, err);
1014
- }
1015
- }
1016
- return fId;
1017
- }
1018
- try {
1019
- await Folder.create({ _id: fId, name, type: docName, folder: null }, { keepId: true });
1020
- return fId;
1021
- } catch (err) {
1022
- console.warn(`Vaults | could not create ${docName} folder for ${vault.label}:`, err);
1023
- return null;
1024
- }
1491
+ function instanceSubPath(vaultPath, meta) {
1492
+ const override = meta?.foundry?.folder;
1493
+ if (typeof override === "string" && override.trim()) return override;
1494
+ return folderOfPath(vaultPath);
1025
1495
  }
1026
- async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
1496
+ async function buildOverlay(target, vault, vaultPath, meta, docName, derived = {}) {
1027
1497
  const overlay = {
1028
1498
  // Prefer the page's frontmatter `title:` over the filename — the wiki
1029
1499
  // already treats title as the page's display name, and a doc named
1030
1500
  // "Potion of Healing (Mossfoot Brew)" reads better in the Foundry
1031
1501
  // sidebar than "Healing Potion".
1032
1502
  name: meta.title || baseName(vaultPath),
1033
- folder: await ensureInstanceFolder(vault, docName),
1503
+ folder: await ensureInstanceFolder(target, vault, docName, instanceSubPath(vaultPath, meta)),
1034
1504
  flags: { [MODULE_ID]: { vaultId: vault.id, path: vaultPath } }
1035
1505
  };
1036
1506
  if (meta.image) {
@@ -1041,42 +1511,45 @@ async function buildOverlay(vault, vaultPath, meta, docName, derived = {}) {
1041
1511
  }
1042
1512
  }
1043
1513
  const fm = meta?.foundry;
1044
- const descPath = DESCRIPTION_FIELDS[game.system.id]?.[docName];
1514
+ const descPath = descriptionPathFor(docName, game.system.id);
1045
1515
  const embedAuto = fm?.embed !== false && fm?.journal !== false;
1046
1516
  if (descPath && embedAuto) {
1047
1517
  const eId = await entryId(vault.id, vaultPath);
1048
1518
  const pId = await pageId(vault.id, vaultPath);
1049
- setPath(overlay, descPath, `<p>@Embed[JournalEntry.${eId}.JournalEntryPage.${pId} inline]</p>`);
1519
+ setPath(overlay, descPath, `<p>@Embed[${journalPageUuid(vault, eId, pId)} inline]</p>`);
1050
1520
  }
1051
1521
  if (fm?.data && typeof fm.data === "object") {
1052
1522
  const cloned = rewriteVaultPaths(structuredClone(fm.data), vault.id);
1523
+ await resolveMoulinetteRefs(cloned, (msg) => console.warn(`Vaults | moulinette: ${vaultPath}: ${msg}`));
1524
+ await resolveItemUuids(cloned, vaultPath);
1053
1525
  await ensureEmbeddedIds(cloned, vault.id, vaultPath);
1054
1526
  deepMerge(overlay, cloned);
1055
1527
  }
1056
- if (docName === "Scene") {
1057
- const note = await buildJournalNote(vault, vaultPath, meta);
1058
- if (note) overlay.notes = [...overlay.notes ?? [], note];
1059
- }
1060
1528
  return overlay;
1061
1529
  }
1062
- async function buildJournalNote(vault, vaultPath, meta) {
1063
- const fm = meta?.foundry ?? {};
1064
- const cfg = { ...fm.data_json ?? {}, ...fm.data ?? {} };
1065
- const width = Number(cfg.width) || 4e3;
1066
- const height = Number(cfg.height) || 3e3;
1067
- const padding = Number(cfg.padding ?? 0.25);
1068
- const gridSize = Number(cfg.grid?.size) || 100;
1069
- const iconSize = gridSize;
1530
+ function notePosition(geom) {
1531
+ const width = Number(geom.width) || 4e3;
1532
+ const height = Number(geom.height) || 3e3;
1533
+ const padding = Number(geom.padding ?? 0.25);
1534
+ const gridSize = Number(geom.grid?.size) || 100;
1535
+ return {
1536
+ x: gridSize * (Math.ceil(width / gridSize * padding) - 0.5),
1537
+ y: gridSize * (Math.ceil(height / gridSize * padding) + 0.5)
1538
+ };
1539
+ }
1540
+ async function attachJournalNote(sceneData, geom, vault, vaultPath, meta) {
1541
+ const gridSize = Number(geom.grid?.size) || 100;
1542
+ const { x, y } = notePosition(geom);
1070
1543
  const eId = await entryId(vault.id, vaultPath);
1071
1544
  const idOverride = meta?.foundry?.id;
1072
1545
  const pId = typeof idOverride === "string" && idOverride ? idOverride : await pageId(vault.id, vaultPath);
1073
- return {
1546
+ const note2 = {
1074
1547
  _id: await subdocId(vault.id, vaultPath, "/notes/__journalLink__"),
1075
1548
  entryId: eId,
1076
1549
  pageId: pId,
1077
- x: gridSize * (Math.ceil(width / gridSize * padding) - 0.5),
1078
- y: gridSize * (Math.ceil(height / gridSize * padding) + 0.5),
1079
- iconSize,
1550
+ x,
1551
+ y,
1552
+ iconSize: gridSize,
1080
1553
  texture: {
1081
1554
  src: "icons/svg/book.svg",
1082
1555
  anchorX: 0.5,
@@ -1086,8 +1559,48 @@ async function buildJournalNote(vault, vaultPath, meta) {
1086
1559
  },
1087
1560
  text: ""
1088
1561
  };
1562
+ sceneData.notes = [...sceneData.notes ?? [], note2];
1089
1563
  }
1090
1564
  var VALID_SUBDOC_ID = /^[A-Za-z0-9]{16}$/;
1565
+ function mergeItemsById(baseItems, pageItems) {
1566
+ if (!Array.isArray(pageItems)) return baseItems;
1567
+ if (!baseItems.length) return pageItems;
1568
+ const merged = baseItems.map((item) => structuredClone(item));
1569
+ const positionOf = new Map(merged.map((item, i) => [item?._id, i]));
1570
+ for (const item of pageItems) {
1571
+ const at = item && item._id !== void 0 ? positionOf.get(item._id) : void 0;
1572
+ if (at === void 0) merged.push(item);
1573
+ else deepMerge(merged[at], item);
1574
+ }
1575
+ return merged;
1576
+ }
1577
+ async function resolveItemUuids(data, vaultPath) {
1578
+ if (!Array.isArray(data?.items)) return;
1579
+ const resolved = [];
1580
+ for (const entry of data.items) {
1581
+ if (!entry || typeof entry !== "object" || typeof entry.uuid !== "string") {
1582
+ resolved.push(entry);
1583
+ continue;
1584
+ }
1585
+ const { uuid, ...overrides } = entry;
1586
+ const source = await safeFromUuid(uuid);
1587
+ if (!source) {
1588
+ console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} did not resolve; skipping that item.`);
1589
+ continue;
1590
+ }
1591
+ if (source.documentName !== "Item") {
1592
+ console.warn(`Vaults | foundry item uuid: ${vaultPath} \u2192 ${uuid} is a ${source.documentName}, not an Item; skipping that item.`);
1593
+ continue;
1594
+ }
1595
+ const itemData = source.toObject();
1596
+ delete itemData._id;
1597
+ if (uuid.startsWith("Compendium.")) {
1598
+ itemData._stats = { ...itemData._stats, compendiumSource: uuid };
1599
+ }
1600
+ resolved.push(deepMerge(itemData, overrides));
1601
+ }
1602
+ data.items = resolved;
1603
+ }
1091
1604
  async function ensureEmbeddedIds(value, vaultId, pagePath, ptr = "") {
1092
1605
  if (Array.isArray(value)) {
1093
1606
  for (let i = 0; i < value.length; i++) {
@@ -1161,6 +1674,37 @@ function deepMerge(target, source) {
1161
1674
  }
1162
1675
  return target;
1163
1676
  }
1677
+ async function findMissingDocuments(target, vault, entries) {
1678
+ const out = [];
1679
+ const journalPages = /* @__PURE__ */ new Set();
1680
+ for (const entry of await target.contents("JournalEntry")) {
1681
+ for (const page of entry.pages ?? []) journalPages.add(`${entry._id}.${page._id}`);
1682
+ }
1683
+ const idsByType = /* @__PURE__ */ new Map();
1684
+ const packIds = async (docName) => {
1685
+ if (!idsByType.has(docName)) idsByType.set(docName, await target.ids(docName));
1686
+ return idsByType.get(docName);
1687
+ };
1688
+ for (const { logicalPath, meta } of entries) {
1689
+ const fm = meta?.foundry;
1690
+ const missing = [];
1691
+ if (fm?.journal !== false) {
1692
+ const eId = await entryId(vault.id, logicalPath);
1693
+ const pId = typeof fm?.id === "string" && fm.id ? fm.id : await pageId(vault.id, logicalPath);
1694
+ if (!journalPages.has(`${eId}.${pId}`)) missing.push("journal");
1695
+ }
1696
+ if (fm?.base !== void 0 && fm?.base !== null) {
1697
+ const specs = Array.isArray(fm.base) ? fm.base : [fm.base];
1698
+ const docName = docNameOf(parseFoundryBase(specs[0]));
1699
+ if (docName) {
1700
+ const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, logicalPath);
1701
+ if (!(await packIds(docName)).has(id)) missing.push("document");
1702
+ }
1703
+ }
1704
+ if (missing.length > 0) out.push({ path: logicalPath, missing: missing.join(" + ") });
1705
+ }
1706
+ return out;
1707
+ }
1164
1708
 
1165
1709
  // ../foundry/scripts/auth.mjs
1166
1710
  function tokenInfo(token) {
@@ -1174,8 +1718,187 @@ function tokenInfo(token) {
1174
1718
  };
1175
1719
  }
1176
1720
 
1721
+ // ../foundry/scripts/target.mjs
1722
+ async function openTarget(vault) {
1723
+ return isAdventure(vault) ? openAdventure(vault) : openCompendium(vault);
1724
+ }
1725
+ async function openCompendium(vault) {
1726
+ return {
1727
+ vault,
1728
+ adventure: false,
1729
+ async get(docName, id) {
1730
+ const pack = await ensurePack(vault, docName);
1731
+ const doc = await pack.getDocument(id);
1732
+ return doc ? doc.toObject() : null;
1733
+ },
1734
+ async put(docName, data) {
1735
+ const pack = await ensurePack(vault, docName);
1736
+ const existing = await pack.getDocument(data._id);
1737
+ if (existing) {
1738
+ await existing.update(data);
1739
+ return "modified";
1740
+ }
1741
+ const cls = CONFIG[docName].documentClass;
1742
+ await cls.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
1743
+ if (!await pack.getDocument(data._id)) {
1744
+ throw new Error(`${docName} ${data._id} was rejected on create`);
1745
+ }
1746
+ return "added";
1747
+ },
1748
+ async remove(docName, id) {
1749
+ const pack = getPack(vault, docName);
1750
+ const doc = pack && await pack.getDocument(id);
1751
+ if (doc) await doc.delete();
1752
+ },
1753
+ async putFolder(docName, folder) {
1754
+ const pack = await ensurePack(vault, docName);
1755
+ const existing = pack.folders.get(folder._id);
1756
+ if (existing) {
1757
+ if (existing.name !== folder.name || (existing.folder?.id ?? null) !== (folder.folder ?? null)) {
1758
+ await existing.update({ name: folder.name, folder: folder.folder ?? null });
1759
+ }
1760
+ return;
1761
+ }
1762
+ await Folder.create(
1763
+ { ...folder, type: docName },
1764
+ { pack: pack.collection, keepId: true }
1765
+ );
1766
+ if (!pack.folders.get(folder._id)) {
1767
+ throw new Error(`Folder ${folder._id} ("${folder.name}") was rejected on create`);
1768
+ }
1769
+ },
1770
+ /** Every document of a type this vault owns. One request, not one per id. */
1771
+ async contents(docName) {
1772
+ const pack = getPack(vault, docName);
1773
+ return pack ? (await pack.getDocuments()).map((d) => d.toObject()) : [];
1774
+ },
1775
+ async ids(docName) {
1776
+ const pack = getPack(vault, docName);
1777
+ return new Set(pack ? (await pack.getIndex()).map((e) => e._id) : []);
1778
+ },
1779
+ // Each write already landed.
1780
+ async commit() {
1781
+ }
1782
+ };
1783
+ }
1784
+ var FIELD = {
1785
+ Actor: "actors",
1786
+ Item: "items",
1787
+ Scene: "scenes",
1788
+ JournalEntry: "journal",
1789
+ RollTable: "tables",
1790
+ Macro: "macros",
1791
+ Cards: "cards",
1792
+ Playlist: "playlists"
1793
+ };
1794
+ async function openAdventure(vault) {
1795
+ const pack = await ensurePack(vault, "Adventure");
1796
+ const id = await adventureId(vault.id);
1797
+ const existing = await pack.getDocument(id);
1798
+ const source = existing ? existing.toObject() : null;
1799
+ const docs = /* @__PURE__ */ new Map();
1800
+ for (const [docName, field] of Object.entries(FIELD)) {
1801
+ docs.set(docName, new Map((source?.[field] ?? []).map((d) => [d._id, d])));
1802
+ }
1803
+ const folders = new Map((source?.folders ?? []).map((f) => [f._id, f]));
1804
+ let dirty = false;
1805
+ return {
1806
+ vault,
1807
+ adventure: true,
1808
+ async get(docName, docId) {
1809
+ return docs.get(docName)?.get(docId) ?? null;
1810
+ },
1811
+ async put(docName, data) {
1812
+ const byId = docs.get(docName);
1813
+ if (!byId) throw new Error(`An Adventure cannot hold a ${docName}`);
1814
+ const had = byId.has(data._id);
1815
+ byId.set(data._id, data);
1816
+ dirty = true;
1817
+ return had ? "modified" : "added";
1818
+ },
1819
+ async remove(docName, docId) {
1820
+ if (docs.get(docName)?.delete(docId)) dirty = true;
1821
+ },
1822
+ async putFolder(docName, folder) {
1823
+ const want = { ...folder, type: docName, folder: folder.folder ?? null };
1824
+ const have = folders.get(folder._id);
1825
+ if (have && have.name === want.name && have.folder === want.folder) return;
1826
+ folders.set(folder._id, want);
1827
+ dirty = true;
1828
+ },
1829
+ async contents(docName) {
1830
+ return [...docs.get(docName)?.values() ?? []];
1831
+ },
1832
+ async ids(docName) {
1833
+ return new Set(docs.get(docName)?.keys() ?? []);
1834
+ },
1835
+ async commit() {
1836
+ if (!dirty && existing) return;
1837
+ const data = {
1838
+ _id: id,
1839
+ name: vault.label || "Vault",
1840
+ // The sheet shows these before import, and they are the only
1841
+ // description a GM gets of what they are about to bring in.
1842
+ caption: `Synced from ${vault.url}`,
1843
+ description: `<p>Every page, document and folder from the <strong>${vault.label || "vault"}</strong> vault. Importing creates them in this world; importing again updates what is already here rather than duplicating it.</p>`,
1844
+ folders: [...folders.values()],
1845
+ flags: { [MODULE_ID]: { vaultId: vault.id } }
1846
+ };
1847
+ for (const [docName, field] of Object.entries(FIELD)) {
1848
+ data[field] = [...docs.get(docName).values()];
1849
+ }
1850
+ if (existing) await existing.update(data);
1851
+ else {
1852
+ await Adventure.create(data, { pack: pack.collection, keepId: true, keepEmbeddedIds: true });
1853
+ if (!await pack.getDocument(id)) {
1854
+ throw new Error(`Adventure ${id} was rejected on create`);
1855
+ }
1856
+ }
1857
+ }
1858
+ };
1859
+ }
1860
+
1177
1861
  // ../foundry/scripts/sync.mjs
1178
- async function sync(host, vault, { forceFull = false } = {}) {
1862
+ async function orderByBaseDeps(vault, paths, bodyMetaIndex) {
1863
+ const ownerOf = /* @__PURE__ */ new Map();
1864
+ for (const p of paths) {
1865
+ const fm = bodyMetaIndex.get(p)?.foundry;
1866
+ if (!fm?.base) continue;
1867
+ const id = typeof fm.id === "string" && fm.id ? fm.id : await instanceId(vault.id, p.replace(/\.body\.html$/i, ".md"));
1868
+ ownerOf.set(id, p);
1869
+ }
1870
+ if (!ownerOf.size) return paths;
1871
+ const dependsOn = /* @__PURE__ */ new Map();
1872
+ for (const p of paths) {
1873
+ const base = bodyMetaIndex.get(p)?.foundry?.base;
1874
+ const match = typeof base === "string" && base.match(/^(?:Actor|Item)\.([A-Za-z0-9]{16})$/);
1875
+ const from = match && ownerOf.get(match[1]);
1876
+ if (from && from !== p) dependsOn.set(p, from);
1877
+ }
1878
+ if (!dependsOn.size) return paths;
1879
+ const ordered = [];
1880
+ const placed = /* @__PURE__ */ new Set();
1881
+ const visit = (p, chain) => {
1882
+ if (placed.has(p) || chain.has(p)) return;
1883
+ chain.add(p);
1884
+ const from = dependsOn.get(p);
1885
+ if (from) visit(from, chain);
1886
+ if (!placed.has(p)) {
1887
+ placed.add(p);
1888
+ ordered.push(p);
1889
+ }
1890
+ };
1891
+ for (const p of paths) visit(p, /* @__PURE__ */ new Set());
1892
+ return ordered;
1893
+ }
1894
+ async function sync(host, vault, opts = {}) {
1895
+ try {
1896
+ return await runSync(host, vault, opts);
1897
+ } finally {
1898
+ end();
1899
+ }
1900
+ }
1901
+ async function runSync(host, vault, { forceFull = false } = {}) {
1179
1902
  if (!vault?.url) {
1180
1903
  host.notify("error", host.localize("VAULTS.Sync.NoUrl"));
1181
1904
  return { ok: false, refreshHandlerAssets: false };
@@ -1191,6 +1914,7 @@ async function sync(host, vault, { forceFull = false } = {}) {
1191
1914
  }
1192
1915
  const start = Date.now();
1193
1916
  host.notify("info", host.localize("VAULTS.Sync.StartingNamed", { name: vault.label }));
1917
+ begin(vault.label);
1194
1918
  let manifest;
1195
1919
  try {
1196
1920
  manifest = await fetchManifest(vault);
@@ -1209,6 +1933,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
1209
1933
  const knownRoles = Array.isArray(manifest.auth?.roles) ? manifest.auth.roles : [];
1210
1934
  const patch = {};
1211
1935
  if (vault.public !== isPublic) patch.public = isPublic;
1936
+ const foundryPackage = manifest.foundry_package || "compendium";
1937
+ if (vault.foundryPackage !== foundryPackage) patch.foundryPackage = foundryPackage;
1212
1938
  if (!arraysEqual(vault.knownRoles, knownRoles)) patch.knownRoles = knownRoles;
1213
1939
  const remoteAssets = manifest.assets?.foundry || {};
1214
1940
  const newAssetPaths = {
@@ -1218,7 +1944,8 @@ async function sync(host, vault, { forceFull = false } = {}) {
1218
1944
  if (JSON.stringify(vault.handlerAssetPaths || {}) !== JSON.stringify(newAssetPaths)) {
1219
1945
  patch.handlerAssetPaths = newAssetPaths;
1220
1946
  }
1221
- if (vault.dmRole && !knownRoles.includes(vault.dmRole)) patch.dmRole = "";
1947
+ const playerRole = typeof manifest.foundry_player_role === "string" ? manifest.foundry_player_role : "";
1948
+ if (vault.playerRole !== playerRole) patch.playerRole = playerRole;
1222
1949
  if (Object.keys(patch).length > 0) {
1223
1950
  await host.updateVaultEntry(vault.id, patch);
1224
1951
  Object.assign(vault, patch);
@@ -1228,7 +1955,15 @@ async function sync(host, vault, { forceFull = false } = {}) {
1228
1955
  );
1229
1956
  const remote = new Map(files.map((f) => [f.path, f.hash]));
1230
1957
  const lastSync = host.getVaultState(vault.id);
1231
- const local = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastManifest || {}));
1958
+ const remoteIdScheme = manifest.id_scheme || "v1";
1959
+ const schemeChanged = !!lastSync.lastIdScheme && lastSync.lastIdScheme !== remoteIdScheme;
1960
+ if (schemeChanged) {
1961
+ console.warn(
1962
+ `Vaults | ${vault.label}: document id scheme changed (${lastSync.lastIdScheme} \u2192 ${remoteIdScheme}); forcing a full re-sync so existing documents are re-derived rather than duplicated.`
1963
+ );
1964
+ }
1965
+ const fullPass = forceFull || schemeChanged;
1966
+ const local = fullPass ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastManifest || {}));
1232
1967
  const bodyPaths = files.filter((f) => f.path.endsWith(".body.html")).map((f) => f.path);
1233
1968
  const pathIndex = buildPathIndex(files);
1234
1969
  const allMdPaths = bodyPaths.map((p) => p.replace(/\.body\.html$/i, ".md"));
@@ -1237,10 +1972,19 @@ async function sync(host, vault, { forceFull = false } = {}) {
1237
1972
  for (const f of files) {
1238
1973
  if (f.meta && f.path.endsWith(".body.html")) bodyMetaIndex.set(f.path, f.meta);
1239
1974
  }
1975
+ const missingPackages = await missingBasePackages(bodyMetaIndex.values());
1976
+ if (missingPackages.size > 0) {
1977
+ const summary = [...missingPackages].map(([pkg, n]) => `${pkg} (${n})`).join(", ");
1978
+ host.notify("warn", host.localize("VAULTS.Sync.MissingPackages", { packages: summary }));
1979
+ console.warn(
1980
+ `Vaults | ${vault.label}: foundry.base points into package(s) this world can't read: ${summary}. Those pages will sync as journals but instantiate no document.`
1981
+ );
1982
+ }
1240
1983
  const prevImages = forceFull ? /* @__PURE__ */ new Map() : new Map(Object.entries(lastSync.lastImageManifest || {}));
1241
1984
  const lastMediaRefs = lastSync.lastMediaRefs || {};
1242
1985
  const mediaStale = (bodyPath) => (lastMediaRefs[bodyPath] || []).some((m) => remote.get(m) !== prevImages.get(m));
1243
- const toUpsert = bodyPaths.filter((p) => remote.get(p) !== local.get(p) || mediaStale(p));
1986
+ const changed = bodyPaths.filter((p) => remote.get(p) !== local.get(p) || mediaStale(p));
1987
+ const toUpsert = await orderByBaseDeps(vault, changed, bodyMetaIndex);
1244
1988
  const toDelete = [...local.keys()].filter((p) => p.endsWith(".body.html") && !remote.has(p));
1245
1989
  if (forceFull) await host.setVaultState(vault.id, { lastImageManifest: {} });
1246
1990
  let imageStats = { downloaded: 0, removed: 0, errors: 0 };
@@ -1249,8 +1993,25 @@ async function sync(host, vault, { forceFull = false } = {}) {
1249
1993
  } catch (err) {
1250
1994
  console.warn(`Vaults | image sync failed for ${vault.label}:`, err);
1251
1995
  }
1996
+ const untouched = new Set(bodyPaths);
1997
+ for (const p of toUpsert) untouched.delete(p);
1998
+ for (const p of toDelete) untouched.delete(p);
1999
+ const target = await openTarget(vault);
2000
+ const missingDocs = await findMissingDocuments(target, vault, [...untouched].map((bodyPath) => ({
2001
+ logicalPath: bodyPath.replace(/\.body\.html$/i, ".md"),
2002
+ meta: bodyMetaIndex.get(bodyPath)
2003
+ })));
2004
+ const reportMissingDocs = () => {
2005
+ if (missingDocs.length === 0) return;
2006
+ host.notify("warn", host.localize("VAULTS.Sync.MissingDocuments", { count: missingDocs.length }));
2007
+ console.warn(
2008
+ `Vaults | ${vault.label}: ${missingDocs.length} page(s) have no document in this world. An incremental sync will not notice them again \u2014 use Force Sync to restore:`,
2009
+ missingDocs
2010
+ );
2011
+ };
1252
2012
  if (toUpsert.length === 0 && toDelete.length === 0 && imageStats.downloaded === 0 && imageStats.removed === 0) {
1253
2013
  host.notify("info", host.localize("VAULTS.Sync.NothingToDo"));
2014
+ reportMissingDocs();
1254
2015
  return {
1255
2016
  ok: true,
1256
2017
  refreshHandlerAssets: false,
@@ -1258,12 +2019,18 @@ async function sync(host, vault, { forceFull = false } = {}) {
1258
2019
  modified: 0,
1259
2020
  removed: 0,
1260
2021
  imageStats,
1261
- instances: 0
2022
+ instances: 0,
2023
+ skipped: [],
2024
+ failed: [],
2025
+ // The drift list is computed above this return, so report it here too —
2026
+ // an up-to-date sync with missing documents is exactly the case a caller
2027
+ // most wants to see.
2028
+ missingDocuments: missingDocs
1262
2029
  };
1263
2030
  }
1264
2031
  host.notify(
1265
2032
  "info",
1266
- forceFull ? host.localize("VAULTS.Sync.Initial", { count: toUpsert.length }) : host.localize("VAULTS.Sync.Incremental", {
2033
+ fullPass ? host.localize("VAULTS.Sync.Initial", { count: toUpsert.length }) : host.localize("VAULTS.Sync.Incremental", {
1267
2034
  add: toUpsert.length,
1268
2035
  mod: 0,
1269
2036
  del: toDelete.length
@@ -1271,69 +2038,134 @@ async function sync(host, vault, { forceFull = false } = {}) {
1271
2038
  );
1272
2039
  let bodies;
1273
2040
  try {
1274
- bodies = await fetchSourceBatch(vault, toUpsert);
2041
+ const byRole = /* @__PURE__ */ new Map();
2042
+ for (const bodyPath of toUpsert) {
2043
+ const pageRole = bodyMetaIndex.get(bodyPath)?.role;
2044
+ const key = pageRole || "";
2045
+ if (!byRole.has(key)) byRole.set(key, []);
2046
+ byRole.get(key).push(bodyPath);
2047
+ }
2048
+ bodies = /* @__PURE__ */ new Map();
2049
+ for (const [pageRole, group] of byRole) {
2050
+ const fetched = await fetchSourceBatch(vault, group, pageRole || void 0);
2051
+ for (const [k, v] of fetched) bodies.set(k, v);
2052
+ }
1275
2053
  } catch (err) {
1276
2054
  console.error(`Vaults | batch fetch failed for ${vault.label}:`, err);
1277
2055
  host.notify("error", host.localize("VAULTS.Sync.Error", { message: err.message }));
1278
2056
  return { ok: false, refreshHandlerAssets: false };
1279
2057
  }
1280
2058
  let added = 0, modified = 0, instances = 0;
2059
+ const skipped = [];
2060
+ const versionSkew = [];
2061
+ const failedPages = /* @__PURE__ */ new Map();
1281
2062
  const mediaRefs = {};
1282
2063
  for (const p of bodyPaths) if (lastMediaRefs[p]) mediaRefs[p] = lastMediaRefs[p];
2064
+ phase("Pages", toUpsert.length);
1283
2065
  for (const bodyPath of toUpsert) {
2066
+ step(bodyPath.replace(/\.body\.html$/i, "").split("/").pop());
1284
2067
  const html = bodies.get(bodyPath);
1285
2068
  if (html == null) {
1286
2069
  console.warn(`Vaults | server returned no content for ${bodyPath}`);
2070
+ failedPages.set(bodyPath, "no content returned");
1287
2071
  continue;
1288
2072
  }
1289
2073
  const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
1290
2074
  const pageMeta = bodyMetaIndex.get(bodyPath);
1291
2075
  try {
1292
2076
  if (pageMeta?.foundry?.journal === false) {
1293
- await deleteFile(vault, logicalPath);
2077
+ await deleteFile(target, vault, logicalPath);
1294
2078
  } else {
1295
2079
  const refs = /* @__PURE__ */ new Set();
1296
- const result = await upsertFile(vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
2080
+ const result = await upsertFile(target, vault, logicalPath, html, pathIndex, pageMeta, folderInfo, refs);
1297
2081
  mediaRefs[bodyPath] = [...refs];
1298
2082
  if (result === "added") added++;
1299
2083
  else modified++;
1300
2084
  }
1301
2085
  if (pageMeta?.foundry?.base) {
1302
2086
  try {
1303
- await applyInstance(vault, logicalPath, pageMeta);
1304
- instances++;
2087
+ const outcome = await applyInstance(target, vault, logicalPath, pageMeta, { forceFull: fullPass });
2088
+ if (outcome?.ok) {
2089
+ instances++;
2090
+ if (outcome.skew) versionSkew.push({ path: logicalPath, ...outcome.skew });
2091
+ } else if (outcome) skipped.push({ path: logicalPath, reason: outcome.reason });
1305
2092
  } catch (err) {
1306
2093
  console.warn(`Vaults | foundry instantiation failed for ${logicalPath}:`, err);
2094
+ skipped.push({ path: logicalPath, reason: "threw" });
1307
2095
  }
1308
2096
  }
1309
2097
  } catch (err) {
1310
2098
  console.warn(`Vaults | upsert failed for ${logicalPath}:`, err);
2099
+ failedPages.set(bodyPath, err?.message || "upsert threw");
1311
2100
  }
1312
2101
  }
1313
2102
  let removed = 0;
2103
+ const failedDeletes = /* @__PURE__ */ new Set();
2104
+ if (toDelete.length > 0) phase("Removing", toDelete.length);
1314
2105
  for (const bodyPath of toDelete) {
1315
2106
  const logicalPath = bodyPath.replace(/\.body\.html$/i, ".md");
2107
+ step(logicalPath.split("/").pop());
1316
2108
  try {
1317
- await deleteFile(vault, logicalPath);
2109
+ await deleteFile(target, vault, logicalPath);
1318
2110
  removed++;
1319
2111
  } catch (err) {
1320
2112
  console.warn(`Vaults | delete failed for ${logicalPath}:`, err);
2113
+ failedDeletes.add(bodyPath);
1321
2114
  }
1322
2115
  try {
1323
- await deleteInstance(vault, logicalPath);
2116
+ await deleteInstance(target, vault, logicalPath);
1324
2117
  } catch (err) {
1325
2118
  console.warn(`Vaults | delete instance failed for ${logicalPath}:`, err);
1326
2119
  }
1327
2120
  }
1328
- await host.setVaultState(vault.id, { lastManifest: Object.fromEntries(remote), lastMediaRefs: mediaRefs });
1329
- await reconcileEntryPlacement(vault, folderInfo);
1330
- await reconcileOwnership(vault, bodyMetaIndex);
2121
+ const persisted = new Map(remote);
2122
+ for (const bodyPath of failedPages.keys()) {
2123
+ const previous = local.get(bodyPath);
2124
+ if (previous === void 0) persisted.delete(bodyPath);
2125
+ else persisted.set(bodyPath, previous);
2126
+ }
2127
+ for (const bodyPath of failedDeletes) {
2128
+ const previous = local.get(bodyPath);
2129
+ if (previous !== void 0) persisted.set(bodyPath, previous);
2130
+ }
2131
+ await host.setVaultState(vault.id, {
2132
+ lastManifest: Object.fromEntries(persisted),
2133
+ lastMediaRefs: mediaRefs,
2134
+ lastIdScheme: remoteIdScheme
2135
+ });
2136
+ await reconcileEntries(target, vault, folderInfo, bodyMetaIndex);
2137
+ await target.commit();
2138
+ await pruneStalePacks(vault);
1331
2139
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
1332
2140
  host.notify("info", host.localize("VAULTS.Sync.Done", { added, modified, removed, seconds }));
2141
+ reportMissingDocs();
1333
2142
  if (imageStats.downloaded > 0 || imageStats.removed > 0) {
1334
2143
  console.info(`Vaults | ${vault.label} images: ${imageStats.downloaded} downloaded, ${imageStats.removed} removed` + (imageStats.errors ? `, ${imageStats.errors} failed` : ""));
1335
2144
  }
1336
2145
  if (instances > 0) console.info(`Vaults | ${vault.label} instantiated ${instances} document(s) from page foundry.base.`);
2146
+ if (versionSkew.length > 0) {
2147
+ host.notify("warn", localizeOr(
2148
+ host,
2149
+ "VAULTS.Sync.VersionSkew",
2150
+ "{count} document(s) came from a different Foundry generation and may not render correctly. See the console for which pack, and which version it targets.",
2151
+ { count: versionSkew.length }
2152
+ ));
2153
+ console.warn(
2154
+ `Vaults | ${vault.label}: ${versionSkew.length} document(s) were exported for a different Foundry generation than this world (${game.release?.generation}). They import, but parts of a stale document may not place correctly \u2014 a Foundry 13 Scene keeps its walls and lights but draws its tiles at the canvas origin. Ask the creator for a re-export, or point the base at a pack built for this generation:`,
2155
+ versionSkew
2156
+ );
2157
+ }
2158
+ if (failedPages.size > 0) {
2159
+ host.notify("warn", host.localize("VAULTS.Sync.PagesFailed", { count: failedPages.size }));
2160
+ console.warn(
2161
+ `Vaults | ${vault.label} failed to sync ${failedPages.size} page(s); they stay in the diff and retry next sync:`,
2162
+ [...failedPages].map(([path, reason]) => ({ path, reason }))
2163
+ );
2164
+ }
2165
+ if (skipped.length > 0) {
2166
+ host.notify("warn", host.localize("VAULTS.Sync.InstancesSkipped", { count: skipped.length }));
2167
+ console.warn(`Vaults | ${vault.label} no document created for ${skipped.length} page(s):`, skipped);
2168
+ }
1337
2169
  return {
1338
2170
  ok: true,
1339
2171
  refreshHandlerAssets: true,
@@ -1341,7 +2173,10 @@ async function sync(host, vault, { forceFull = false } = {}) {
1341
2173
  modified,
1342
2174
  removed,
1343
2175
  imageStats,
1344
- instances
2176
+ instances,
2177
+ skipped,
2178
+ missingDocuments: missingDocs,
2179
+ failed: [...failedPages].map(([path, reason]) => ({ path, reason }))
1345
2180
  };
1346
2181
  }
1347
2182
  function arraysEqual(a, b) {
@@ -1353,16 +2188,15 @@ function arraysEqual(a, b) {
1353
2188
 
1354
2189
  // ../foundry/scripts/importer-entry.mjs
1355
2190
  var REQUIRED_HOST_VERSION = 1;
1356
- async function runSync(host, vault, options = {}) {
2191
+ async function runSync2(host, vault, options = {}) {
1357
2192
  return sync(host, vault, options);
1358
2193
  }
1359
2194
  async function runRemove(_host, vault) {
1360
- await deleteVaultJournals(vault.id);
2195
+ await deleteVaultPacks(vault.id);
1361
2196
  await deleteVaultCache(vault.id);
1362
- await deleteVaultInstances(vault.id);
1363
2197
  }
1364
2198
  export {
1365
2199
  REQUIRED_HOST_VERSION,
1366
2200
  runRemove,
1367
- runSync
2201
+ runSync2 as runSync
1368
2202
  };