canopycms 0.12.0 → 0.12.1

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 (154) hide show
  1. package/CHANGELOG.md +164 -48
  2. package/dist/api/admin.d.ts +4 -4
  3. package/dist/api/branch-review.js +45 -13
  4. package/dist/api/branch-status.d.ts +12 -0
  5. package/dist/api/branch-status.js +13 -0
  6. package/dist/api/branch-update-from-base.d.ts +23 -0
  7. package/dist/api/branch-update-from-base.js +176 -0
  8. package/dist/api/branch.d.ts +5 -0
  9. package/dist/api/branch.js +1 -0
  10. package/dist/api/client.d.ts +14 -1
  11. package/dist/api/client.js +22 -0
  12. package/dist/api/comments.d.ts +2 -2
  13. package/dist/api/content.d.ts +38 -1
  14. package/dist/api/content.js +41 -5
  15. package/dist/api/merge-conflicts.d.ts +226 -0
  16. package/dist/api/merge-conflicts.js +674 -0
  17. package/dist/api/routes.js +2 -0
  18. package/dist/api/types.d.ts +7 -3
  19. package/dist/base-update.d.ts +26 -0
  20. package/dist/base-update.js +31 -0
  21. package/dist/branch-metadata-file.d.ts +51 -0
  22. package/dist/branch-metadata-file.js +11 -0
  23. package/dist/branch-schema-cache.d.ts +12 -0
  24. package/dist/branch-schema-cache.js +27 -2
  25. package/dist/cli/backfill-item-keys.d.ts +77 -0
  26. package/dist/cli/backfill-item-keys.js +320 -0
  27. package/dist/cli/cli.d.ts +1 -1
  28. package/dist/cli/cli.js +9658 -1499
  29. package/dist/cli/generate-ai-content.js +354 -244
  30. package/dist/cli/init.js +6 -0
  31. package/dist/cli/site-config.d.ts +14 -2
  32. package/dist/cli/site-config.js +20 -4
  33. package/dist/comment-store.d.ts +8 -0
  34. package/dist/comment-store.js +28 -0
  35. package/dist/content-index-generation.d.ts +1 -4
  36. package/dist/content-index-generation.js +1 -4
  37. package/dist/content-store.d.ts +39 -3
  38. package/dist/content-store.js +48 -15
  39. package/dist/editor/BranchManager.d.ts +4 -1
  40. package/dist/editor/BranchManager.js +9 -5
  41. package/dist/editor/Editor.js +429 -52
  42. package/dist/editor/EntryNavigator.d.ts +10 -2
  43. package/dist/editor/EntryNavigator.js +12 -4
  44. package/dist/editor/FormRenderer.d.ts +8 -2
  45. package/dist/editor/FormRenderer.js +5 -6
  46. package/dist/editor/base-update-wait.d.ts +74 -0
  47. package/dist/editor/base-update-wait.js +91 -0
  48. package/dist/editor/branch-status.js +1 -1
  49. package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
  50. package/dist/editor/components/BaseUpdateNotice.js +24 -0
  51. package/dist/editor/components/BranchPill.d.ts +4 -1
  52. package/dist/editor/components/BranchPill.js +5 -2
  53. package/dist/editor/components/ConflictsGroup.d.ts +100 -0
  54. package/dist/editor/components/ConflictsGroup.js +364 -0
  55. package/dist/editor/components/EditorHeader.d.ts +16 -1
  56. package/dist/editor/components/EditorHeader.js +92 -30
  57. package/dist/editor/components/EntryChangesDrawer.d.ts +16 -2
  58. package/dist/editor/components/EntryChangesDrawer.js +33 -167
  59. package/dist/editor/components/EntryHeader.d.ts +3 -1
  60. package/dist/editor/components/EntryHeader.js +2 -2
  61. package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
  62. package/dist/editor/components/GetLatestChangesButton.js +29 -0
  63. package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
  64. package/dist/editor/components/ReleaseHoldButton.js +36 -0
  65. package/dist/editor/components/RetryToastMessage.js +1 -1
  66. package/dist/editor/components/change-parts.d.ts +51 -0
  67. package/dist/editor/components/change-parts.js +171 -0
  68. package/dist/editor/conflict-cards.d.ts +90 -0
  69. package/dist/editor/conflict-cards.js +279 -0
  70. package/dist/editor/conflict-choices.d.ts +60 -0
  71. package/dist/editor/conflict-choices.js +108 -0
  72. package/dist/editor/conflict-notice.d.ts +25 -0
  73. package/dist/editor/conflict-notice.js +39 -0
  74. package/dist/editor/copy.d.ts +12 -0
  75. package/dist/editor/copy.js +12 -0
  76. package/dist/editor/entry-diff.d.ts +2 -1
  77. package/dist/editor/entry-diff.js +5 -21
  78. package/dist/editor/hooks/useBaseUpdate.d.ts +36 -0
  79. package/dist/editor/hooks/useBaseUpdate.js +269 -0
  80. package/dist/editor/hooks/useBranchManager.d.ts +6 -1
  81. package/dist/editor/hooks/useBranchManager.js +28 -7
  82. package/dist/editor/hooks/useBranchesData.js +3 -0
  83. package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
  84. package/dist/editor/hooks/useConflictChoices.js +75 -0
  85. package/dist/editor/hooks/useConflictsData.d.ts +26 -0
  86. package/dist/editor/hooks/useConflictsData.js +89 -0
  87. package/dist/editor/hooks/useDraftManager.d.ts +5 -0
  88. package/dist/editor/hooks/useDraftManager.js +24 -8
  89. package/dist/editor/hooks/useEntryManager.d.ts +9 -0
  90. package/dist/editor/hooks/useEntryManager.js +5 -0
  91. package/dist/editor/release-hold.d.ts +12 -0
  92. package/dist/editor/release-hold.js +39 -0
  93. package/dist/editor/server-clock.d.ts +4 -0
  94. package/dist/editor/server-clock.js +15 -0
  95. package/dist/editor/undo-change.d.ts +7 -0
  96. package/dist/editor/undo-change.js +19 -4
  97. package/dist/git-manager.d.ts +5 -0
  98. package/dist/git-manager.js +30 -0
  99. package/dist/item-keys.d.ts +13 -0
  100. package/dist/item-keys.js +36 -14
  101. package/dist/merge/conflict-path.d.ts +40 -0
  102. package/dist/merge/conflict-path.js +128 -0
  103. package/dist/merge/diff3.d.ts +27 -0
  104. package/dist/merge/diff3.js +113 -0
  105. package/dist/merge/entry-merge.d.ts +94 -0
  106. package/dist/merge/entry-merge.js +875 -0
  107. package/dist/merge/list-alignment.d.ts +52 -0
  108. package/dist/merge/list-alignment.js +441 -0
  109. package/dist/merge/text-merge.d.ts +29 -0
  110. package/dist/merge/text-merge.js +59 -0
  111. package/dist/merge/value-equality.d.ts +18 -0
  112. package/dist/merge/value-equality.js +50 -0
  113. package/dist/merge-conflicts.d.ts +232 -0
  114. package/dist/merge-conflicts.js +451 -0
  115. package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
  116. package/dist/operating-mode/client-safe-strategy.js +6 -0
  117. package/dist/operating-mode/types.d.ts +5 -0
  118. package/dist/services.d.ts +16 -1
  119. package/dist/services.js +82 -22
  120. package/dist/task-queue/cms-task-queue.d.ts +3 -1
  121. package/dist/types.d.ts +28 -2
  122. package/dist/utils/content-serialize.d.ts +18 -1
  123. package/dist/utils/content-serialize.js +12 -0
  124. package/dist/utils/content-write-lock.d.ts +3 -3
  125. package/dist/utils/content-write-lock.js +3 -3
  126. package/dist/utils/markdown-body-splice.d.ts +1 -1
  127. package/dist/utils/markdown-body-splice.js +1 -19
  128. package/dist/utils/markdown-parse.d.ts +9 -0
  129. package/dist/utils/markdown-parse.js +27 -0
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/worker/branch-merge.d.ts +53 -37
  133. package/dist/worker/branch-merge.js +139 -54
  134. package/dist/worker/cms-worker.d.ts +16 -3
  135. package/dist/worker/cms-worker.js +59 -12
  136. package/dist/worker/field-merge.d.ts +65 -0
  137. package/dist/worker/field-merge.js +636 -0
  138. package/dist/worker/git-sync.d.ts +1 -1
  139. package/dist/worker/git-sync.js +7 -6
  140. package/dist/worker/github-commits.d.ts +42 -10
  141. package/dist/worker/github-commits.js +287 -47
  142. package/dist/worker/github-gateway-http.js +17 -9
  143. package/dist/worker/github-gateway-wire.d.ts +21 -12
  144. package/dist/worker/github-gateway-wire.js +5 -1
  145. package/dist/worker/github-gateway.d.ts +10 -1
  146. package/dist/worker/github-gateway.js +18 -6
  147. package/dist/worker/github-transfer.d.ts +5 -3
  148. package/dist/worker/github-transfer.js +8 -5
  149. package/dist/worker/rebase.d.ts +5 -4
  150. package/dist/worker/rebase.js +293 -156
  151. package/dist/worker/task-runner.d.ts +1 -1
  152. package/dist/worker/task-runner.js +14 -2
  153. package/dist/worker/worker-context.d.ts +18 -2
  154. package/package.json +1 -1
@@ -186,6 +186,21 @@ function withItemKeys(fields, data, fill) {
186
186
  return out;
187
187
  }
188
188
  function keyList(field, items, fill) {
189
+ const keys = listKeys(items, fill);
190
+ let changed = false;
191
+ const out = items.map((item, i) => {
192
+ if (!isPlainRecord(item))
193
+ return item;
194
+ let next = keyInside(field, item, fill);
195
+ if (keys[i] !== item[ITEM_KEY])
196
+ next = { ...next, [ITEM_KEY]: keys[i] };
197
+ if (next !== item)
198
+ changed = true;
199
+ return next;
200
+ });
201
+ return changed ? out : items;
202
+ }
203
+ function listKeys(items, fill) {
189
204
  const held = /* @__PURE__ */ new Set();
190
205
  if (fill !== "fresh") {
191
206
  for (const item of items) {
@@ -194,25 +209,19 @@ function keyList(field, items, fill) {
194
209
  }
195
210
  }
196
211
  const kept = /* @__PURE__ */ new Set();
197
- let changed = false;
198
- const out = items.map((item) => {
212
+ return items.map((item) => {
199
213
  if (!isPlainRecord(item))
200
- return item;
201
- let next = keyInside(field, item, fill);
214
+ return void 0;
202
215
  const own = item[ITEM_KEY];
203
216
  if (fill !== "fresh" && isItemKey(own) && !kept.has(own)) {
204
217
  kept.add(own);
205
- } else {
206
- const key = fill === "content" ? unheld(contentKey(item), held) : mintItemKey();
207
- held.add(key);
208
- kept.add(key);
209
- next = { ...next, [ITEM_KEY]: key };
218
+ return own;
210
219
  }
211
- if (next !== item)
212
- changed = true;
213
- return next;
220
+ const key = fill === "content" ? unheld(contentKey(item), held) : mintItemKey();
221
+ held.add(key);
222
+ kept.add(key);
223
+ return key;
214
224
  });
215
- return changed ? out : items;
216
225
  }
217
226
  function unheld(key, held) {
218
227
  let out = key;
@@ -314,11 +323,11 @@ var init_sanitize_href = __esm({
314
323
  });
315
324
 
316
325
  // dist/paths/normalize.js
317
- function normalizeFilesystemPath(path27) {
318
- return path27.split(/[\\/]+/).filter(Boolean).join("/");
326
+ function normalizeFilesystemPath(path28) {
327
+ return path28.split(/[\\/]+/).filter(Boolean).join("/");
319
328
  }
320
- function hasTraversalSequence(path27) {
321
- const normalized = normalizeFilesystemPath(path27);
329
+ function hasTraversalSequence(path28) {
330
+ const normalized = normalizeFilesystemPath(path28);
322
331
  return normalized.includes("..");
323
332
  }
324
333
  function createLogicalPath(...segments) {
@@ -331,14 +340,14 @@ function createLogicalPath(...segments) {
331
340
  function entryLogicalPath(collectionLogicalPath, slug) {
332
341
  return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
333
342
  }
334
- function trimSlashes(path27) {
343
+ function trimSlashes(path28) {
335
344
  let start = 0;
336
- let end = path27.length;
337
- while (start < end && path27[start] === "/")
345
+ let end = path28.length;
346
+ while (start < end && path28[start] === "/")
338
347
  start++;
339
- while (end > start && path27[end - 1] === "/")
348
+ while (end > start && path28[end - 1] === "/")
340
349
  end--;
341
- return path27.slice(start, end);
350
+ return path28.slice(start, end);
342
351
  }
343
352
  var init_normalize = __esm({
344
353
  "dist/paths/normalize.js"() {
@@ -1175,6 +1184,9 @@ var init_client_safe_strategy = __esm({
1175
1184
  supportsPullRequests() {
1176
1185
  return true;
1177
1186
  }
1187
+ syncsBranchesWithBase() {
1188
+ return true;
1189
+ }
1178
1190
  getPermissionsFileName() {
1179
1191
  return "permissions.json";
1180
1192
  }
@@ -1201,6 +1213,9 @@ var init_client_safe_strategy = __esm({
1201
1213
  supportsPullRequests() {
1202
1214
  return false;
1203
1215
  }
1216
+ syncsBranchesWithBase() {
1217
+ return false;
1218
+ }
1204
1219
  getPermissionsFileName() {
1205
1220
  return "permissions.json";
1206
1221
  }
@@ -1610,8 +1625,7 @@ var init_worker_status = __esm({
1610
1625
  });
1611
1626
 
1612
1627
  // dist/cli/generate-ai-content.js
1613
- import path26 from "node:path";
1614
- import { createJiti } from "jiti";
1628
+ import path27 from "node:path";
1615
1629
 
1616
1630
  // dist/build/generate-ai-content.js
1617
1631
  init_error();
@@ -1626,7 +1640,7 @@ import matter2 from "gray-matter";
1626
1640
  import { parse as yamlParse } from "yaml";
1627
1641
 
1628
1642
  // dist/utils/content-serialize.js
1629
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
1643
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
1630
1644
  import matter from "gray-matter";
1631
1645
  import { isCollection as isCollection2, isMap as isMap2, isNode as isNode2, isScalar as isScalar2, isSeq as isSeq2, parseDocument as parseDocument2, Scalar, stringify as yamlStringify, visit as visit2 } from "yaml";
1632
1646
 
@@ -1805,13 +1819,112 @@ var testLogger = createDebugLogger({
1805
1819
  // dist/utils/content-serialize.js
1806
1820
  init_error();
1807
1821
 
1822
+ // dist/utils/json-source-splice.js
1823
+ import { isDeepStrictEqual } from "node:util";
1824
+ import { applyEdits, modify } from "jsonc-parser";
1825
+ init_error();
1826
+ var log = createDebugLogger({ prefix: "JsonSourceSplice" });
1827
+ function isRecord(value) {
1828
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1829
+ }
1830
+ function plainStringify(data) {
1831
+ return `${JSON.stringify(data, null, 2)}
1832
+ `;
1833
+ }
1834
+ function detectFormatting(text) {
1835
+ const indent = /^([ \t]+)\S/m.exec(text)?.[1];
1836
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
1837
+ if (indent?.startsWith(" "))
1838
+ return { insertSpaces: false, tabSize: 1, eol };
1839
+ return { insertSpaces: true, tabSize: indent?.length ?? 2, eol };
1840
+ }
1841
+ function insertMember(text, path28, key, value, formattingOptions) {
1842
+ const at = [...path28, key];
1843
+ const [edit] = modify(text, at, value, {});
1844
+ const closing = edit ? /^[ \t]*\r?\n([ \t]*)\}/.exec(text.slice(edit.offset)) : null;
1845
+ if (edit && closing && edit.length === 0 && edit.content.startsWith(",")) {
1846
+ const unit = formattingOptions.insertSpaces ? " ".repeat(formattingOptions.tabSize ?? 2) : " ";
1847
+ const eol = formattingOptions.eol ?? "\n";
1848
+ const indent = closing[1] + unit;
1849
+ const rendered = JSON.stringify(value, null, unit).split("\n").join(eol + indent);
1850
+ return applyEdits(text, [
1851
+ { ...edit, content: `,${eol}${indent}${JSON.stringify(key)}: ${rendered}` }
1852
+ ]);
1853
+ }
1854
+ return applyEdits(text, modify(text, at, value, { formattingOptions }));
1855
+ }
1856
+ function removeMember(text, path28) {
1857
+ return applyEdits(text, modify(text, path28, void 0, {}).map((edit) => {
1858
+ const removed = text.slice(edit.offset, edit.offset + edit.length);
1859
+ const lead = /^\s+/.exec(removed)?.[0].length ?? 0;
1860
+ const atMember = /\S/.test(text.charAt(edit.offset + edit.length));
1861
+ return edit.content === "" && lead > 0 && atMember ? { ...edit, offset: edit.offset + lead, length: edit.length - lead } : edit;
1862
+ }));
1863
+ }
1864
+ function reconcile(text, path28, before, after, formattingOptions) {
1865
+ if (isRecord(before) && isRecord(after)) {
1866
+ if (Object.keys(after).length === 0 && Object.keys(before).length > 0) {
1867
+ return applyEdits(text, modify(text, path28, {}, { formattingOptions }));
1868
+ }
1869
+ let next = text;
1870
+ for (const key of Object.keys(before)) {
1871
+ if (!Object.prototype.hasOwnProperty.call(after, key))
1872
+ next = removeMember(next, [...path28, key]);
1873
+ }
1874
+ for (const key of Object.keys(after)) {
1875
+ next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile(next, [...path28, key], before[key], after[key], formattingOptions) : insertMember(next, path28, key, after[key], formattingOptions);
1876
+ }
1877
+ return next;
1878
+ }
1879
+ if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
1880
+ return after.reduce((next, item, index) => reconcile(next, [...path28, index], before[index], item, formattingOptions), text);
1881
+ }
1882
+ if (JSON.stringify(before) === JSON.stringify(after))
1883
+ return text;
1884
+ return applyEdits(text, modify(text, path28, after, { formattingOptions }));
1885
+ }
1886
+ function serializeJson(data, existingRaw) {
1887
+ if (existingRaw === void 0)
1888
+ return plainStringify(data);
1889
+ try {
1890
+ const normalised = JSON.parse(JSON.stringify(data));
1891
+ const existing = JSON.parse(existingRaw);
1892
+ if (!isRecord(normalised) || !isRecord(existing))
1893
+ return plainStringify(data);
1894
+ const spliced = reconcile(existingRaw, [], existing, normalised, detectFormatting(existingRaw));
1895
+ if (isDeepStrictEqual(JSON.parse(spliced), normalised))
1896
+ return spliced;
1897
+ log.debug("json-source-splice", "splice does not read back as data; writing JSON.stringify");
1898
+ } catch (err) {
1899
+ log.debug("json-source-splice", "splice failed; writing JSON.stringify", {
1900
+ error: getErrorMessage(err)
1901
+ });
1902
+ }
1903
+ return plainStringify(data);
1904
+ }
1905
+
1808
1906
  // dist/utils/markdown-body-splice.js
1907
+ init_error();
1908
+
1909
+ // dist/utils/markdown-parse.js
1809
1910
  import { fromMarkdown } from "mdast-util-from-markdown";
1810
1911
  import { gfmFromMarkdown } from "mdast-util-gfm";
1811
1912
  import { mdxFromMarkdown } from "mdast-util-mdx";
1812
1913
  import { gfm } from "micromark-extension-gfm";
1813
1914
  import { mdxjs } from "micromark-extension-mdxjs";
1814
1915
  init_error();
1916
+ var log2 = createDebugLogger({ prefix: "MarkdownParse" });
1917
+ function parseMarkdownBody(text, format) {
1918
+ try {
1919
+ return format === "mdx" ? fromMarkdown(text, {
1920
+ extensions: [gfm(), mdxjs()],
1921
+ mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()]
1922
+ }) : fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
1923
+ } catch (err) {
1924
+ log2.debug("markdown-parse", "body does not parse", { error: getErrorMessage(err) });
1925
+ return void 0;
1926
+ }
1927
+ }
1815
1928
 
1816
1929
  // dist/utils/markdown-export-options.js
1817
1930
  var MARKDOWN_EXPORT_OPTIONS = {
@@ -2236,7 +2349,7 @@ function restyleMarkdown(source, tree, fixed, readsBack) {
2236
2349
  }
2237
2350
 
2238
2351
  // dist/utils/yaml-source-splice.js
2239
- import { isDeepStrictEqual } from "node:util";
2352
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
2240
2353
  import { Document, parseDocument, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, YAMLMap, visit, YAMLSeq } from "yaml";
2241
2354
  var END = Symbol("end");
2242
2355
  var FRESH = Symbol("fresh");
@@ -2639,7 +2752,7 @@ function printsAs(candidate, reconciled, printed) {
2639
2752
  const reparsed = parseDocument(candidate);
2640
2753
  if (reparsed.errors.length > 0)
2641
2754
  return false;
2642
- if (!isDeepStrictEqual(reparsed.toJS(), reconciled.toJS()))
2755
+ if (!isDeepStrictEqual2(reparsed.toJS(), reconciled.toJS()))
2643
2756
  return false;
2644
2757
  return layoutFree(reparsed.toString()) === layoutFree(parseDocument(printed).toString());
2645
2758
  }
@@ -2648,19 +2761,8 @@ function layoutFree(text) {
2648
2761
  }
2649
2762
 
2650
2763
  // dist/utils/markdown-body-splice.js
2651
- var log = createDebugLogger({ prefix: "MarkdownBodySplice" });
2764
+ var log3 = createDebugLogger({ prefix: "MarkdownBodySplice" });
2652
2765
  var MAX_LCS_CELLS = 4e6;
2653
- function parse(text, format) {
2654
- try {
2655
- return format === "mdx" ? fromMarkdown(text, {
2656
- extensions: [gfm(), mdxjs()],
2657
- mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()]
2658
- }) : fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
2659
- } catch (err) {
2660
- log.debug("markdown-body-splice", "body does not parse", { error: getErrorMessage(err) });
2661
- return void 0;
2662
- }
2663
- }
2664
2766
  function canonical(node) {
2665
2767
  return JSON.stringify(node, (key, value) => {
2666
2768
  if (key === "position" || key === "data")
@@ -2941,7 +3043,7 @@ function preserveMarkdownSource(original, updated, format) {
2941
3043
  try {
2942
3044
  return splice(original, updated, format) ?? updated;
2943
3045
  } catch (err) {
2944
- log.debug("markdown-body-splice", "splice failed", { error: getErrorMessage(err) });
3046
+ log3.debug("markdown-body-splice", "splice failed", { error: getErrorMessage(err) });
2945
3047
  return updated;
2946
3048
  }
2947
3049
  }
@@ -2966,14 +3068,14 @@ function diskBlocks(oNodes, nNodes) {
2966
3068
  return fromDisk;
2967
3069
  }
2968
3070
  function splice(original, updated, format) {
2969
- const before = original === void 0 ? void 0 : parse(original, format);
2970
- const after = parse(updated, format);
3071
+ const before = original === void 0 ? void 0 : parseMarkdownBody(original, format);
3072
+ const after = parseMarkdownBody(updated, format);
2971
3073
  if (after === void 0)
2972
3074
  return void 0;
2973
3075
  const target = canonical(after);
2974
3076
  const fromDisk = diskBlocks(before?.children ?? [], after.children);
2975
3077
  const restyled = restyleMarkdown(updated, after, (index) => fromDisk.has(index), (text) => {
2976
- const tree = parse(text, format);
3078
+ const tree = parseMarkdownBody(text, format);
2977
3079
  return tree !== void 0 && canonical(tree) === target ? tree : void 0;
2978
3080
  });
2979
3081
  if (original === void 0 || before === void 0)
@@ -2985,10 +3087,10 @@ function splice(original, updated, format) {
2985
3087
  const candidate = new Splicer(original, edit.text, deep).body(before, edit.tree);
2986
3088
  if (candidate === void 0)
2987
3089
  continue;
2988
- const reparsed = parse(candidate, format);
3090
+ const reparsed = parseMarkdownBody(candidate, format);
2989
3091
  if (reparsed !== void 0 && canonical(reparsed) === target)
2990
3092
  return candidate;
2991
- log.debug("markdown-body-splice", "splice does not read back as the edit", {
3093
+ log3.debug("markdown-body-splice", "splice does not read back as the edit", {
2992
3094
  deep,
2993
3095
  restyled: edit !== asSent
2994
3096
  });
@@ -2998,7 +3100,7 @@ function splice(original, updated, format) {
2998
3100
  }
2999
3101
 
3000
3102
  // dist/utils/content-serialize.js
3001
- var log2 = createDebugLogger({ prefix: "ContentSerialize" });
3103
+ var log4 = createDebugLogger({ prefix: "ContentSerialize" });
3002
3104
  function createNode(ctx, value, asKey = false) {
3003
3105
  const node = ctx.doc.createNode(value);
3004
3106
  const { quote } = ctx;
@@ -3242,9 +3344,9 @@ function reconcileYamlSource(raw, data, { rootAtColumnZero = false, quote } = {}
3242
3344
  if (spliced !== void 0 && !(rootAtColumnZero && /^\s*[ \t]\S/.test(spliced))) {
3243
3345
  return spliced;
3244
3346
  }
3245
- log2.debug("content-serialize", "source splice not used; writing as yaml prints it");
3347
+ log4.debug("content-serialize", "source splice not used; writing as yaml prints it");
3246
3348
  } catch (err) {
3247
- log2.debug("content-serialize", "source splice failed; writing as yaml prints it", {
3349
+ log4.debug("content-serialize", "source splice failed; writing as yaml prints it", {
3248
3350
  error: getErrorMessage(err)
3249
3351
  });
3250
3352
  }
@@ -3252,7 +3354,7 @@ function reconcileYamlSource(raw, data, { rootAtColumnZero = false, quote } = {}
3252
3354
  }
3253
3355
  function readsBackAs(text, doc) {
3254
3356
  const reparsed = parseDocument2(text);
3255
- return reparsed.errors.length === 0 && isDeepStrictEqual2(reparsed.toJS(), doc.toJS());
3357
+ return reparsed.errors.length === 0 && isDeepStrictEqual3(reparsed.toJS(), doc.toJS());
3256
3358
  }
3257
3359
  function serializeYaml(data, existingRaw) {
3258
3360
  if (existingRaw === void 0)
@@ -3389,94 +3491,17 @@ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []
3389
3491
  }
3390
3492
  }
3391
3493
  });
3392
- if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
3494
+ if (isDeepStrictEqual3(frontmatterAsRead(written, fields), asJson(data)))
3393
3495
  return written;
3394
- log2.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
3496
+ log4.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
3395
3497
  return matter.stringify(file, data);
3396
3498
  }
3397
-
3398
- // dist/utils/json-source-splice.js
3399
- import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
3400
- import { applyEdits, modify } from "jsonc-parser";
3401
- init_error();
3402
- var log3 = createDebugLogger({ prefix: "JsonSourceSplice" });
3403
- function isRecord(value) {
3404
- return typeof value === "object" && value !== null && !Array.isArray(value);
3405
- }
3406
- function plainStringify(data) {
3407
- return `${JSON.stringify(data, null, 2)}
3408
- `;
3409
- }
3410
- function detectFormatting(text) {
3411
- const indent = /^([ \t]+)\S/m.exec(text)?.[1];
3412
- const eol = text.includes("\r\n") ? "\r\n" : "\n";
3413
- if (indent?.startsWith(" "))
3414
- return { insertSpaces: false, tabSize: 1, eol };
3415
- return { insertSpaces: true, tabSize: indent?.length ?? 2, eol };
3416
- }
3417
- function insertMember(text, path27, key, value, formattingOptions) {
3418
- const at = [...path27, key];
3419
- const [edit] = modify(text, at, value, {});
3420
- const closing = edit ? /^[ \t]*\r?\n([ \t]*)\}/.exec(text.slice(edit.offset)) : null;
3421
- if (edit && closing && edit.length === 0 && edit.content.startsWith(",")) {
3422
- const unit = formattingOptions.insertSpaces ? " ".repeat(formattingOptions.tabSize ?? 2) : " ";
3423
- const eol = formattingOptions.eol ?? "\n";
3424
- const indent = closing[1] + unit;
3425
- const rendered = JSON.stringify(value, null, unit).split("\n").join(eol + indent);
3426
- return applyEdits(text, [
3427
- { ...edit, content: `,${eol}${indent}${JSON.stringify(key)}: ${rendered}` }
3428
- ]);
3429
- }
3430
- return applyEdits(text, modify(text, at, value, { formattingOptions }));
3431
- }
3432
- function removeMember(text, path27) {
3433
- return applyEdits(text, modify(text, path27, void 0, {}).map((edit) => {
3434
- const removed = text.slice(edit.offset, edit.offset + edit.length);
3435
- const lead = /^\s+/.exec(removed)?.[0].length ?? 0;
3436
- const atMember = /\S/.test(text.charAt(edit.offset + edit.length));
3437
- return edit.content === "" && lead > 0 && atMember ? { ...edit, offset: edit.offset + lead, length: edit.length - lead } : edit;
3438
- }));
3439
- }
3440
- function reconcile(text, path27, before, after, formattingOptions) {
3441
- if (isRecord(before) && isRecord(after)) {
3442
- if (Object.keys(after).length === 0 && Object.keys(before).length > 0) {
3443
- return applyEdits(text, modify(text, path27, {}, { formattingOptions }));
3444
- }
3445
- let next = text;
3446
- for (const key of Object.keys(before)) {
3447
- if (!Object.prototype.hasOwnProperty.call(after, key))
3448
- next = removeMember(next, [...path27, key]);
3449
- }
3450
- for (const key of Object.keys(after)) {
3451
- next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile(next, [...path27, key], before[key], after[key], formattingOptions) : insertMember(next, path27, key, after[key], formattingOptions);
3452
- }
3453
- return next;
3454
- }
3455
- if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
3456
- return after.reduce((next, item, index) => reconcile(next, [...path27, index], before[index], item, formattingOptions), text);
3457
- }
3458
- if (JSON.stringify(before) === JSON.stringify(after))
3459
- return text;
3460
- return applyEdits(text, modify(text, path27, after, { formattingOptions }));
3461
- }
3462
- function serializeJson(data, existingRaw) {
3463
- if (existingRaw === void 0)
3464
- return plainStringify(data);
3465
- try {
3466
- const normalised = JSON.parse(JSON.stringify(data));
3467
- const existing = JSON.parse(existingRaw);
3468
- if (!isRecord(normalised) || !isRecord(existing))
3469
- return plainStringify(data);
3470
- const spliced = reconcile(existingRaw, [], existing, normalised, detectFormatting(existingRaw));
3471
- if (isDeepStrictEqual3(JSON.parse(spliced), normalised))
3472
- return spliced;
3473
- log3.debug("json-source-splice", "splice does not read back as data; writing JSON.stringify");
3474
- } catch (err) {
3475
- log3.debug("json-source-splice", "splice failed; writing JSON.stringify", {
3476
- error: getErrorMessage(err)
3477
- });
3478
- }
3479
- return plainStringify(data);
3499
+ function serializeEntry(entry, existingRaw, fields) {
3500
+ if (entry.format === "json")
3501
+ return serializeJson(entry.data, existingRaw);
3502
+ if (entry.format === "yaml")
3503
+ return serializeYaml(entry.data, existingRaw);
3504
+ return serializeFrontmatter(entry.body, entry.data ?? {}, existingRaw, entry.format, fields);
3480
3505
  }
3481
3506
 
3482
3507
  // dist/utils/async-mutex.js
@@ -3922,8 +3947,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
3922
3947
  if (slug && !isIndexSlug(slug)) {
3923
3948
  segments.push(slug);
3924
3949
  }
3925
- const path27 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3926
- return path27.toLowerCase();
3950
+ const path28 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3951
+ return path28.toLowerCase();
3927
3952
  }
3928
3953
 
3929
3954
  // dist/url-collision.js
@@ -4286,7 +4311,7 @@ function extractIdFromFilename(filename) {
4286
4311
  }
4287
4312
  async function resolveCollectionPath(root, logicalPath) {
4288
4313
  const fs22 = await import("node:fs/promises");
4289
- const path27 = await import("node:path");
4314
+ const path28 = await import("node:path");
4290
4315
  const segments = logicalPath.split("/").filter(Boolean);
4291
4316
  let currentPath = root;
4292
4317
  for (const segment of segments) {
@@ -4299,7 +4324,7 @@ async function resolveCollectionPath(root, logicalPath) {
4299
4324
  return logicalName === segment.toLowerCase();
4300
4325
  });
4301
4326
  if (matchingDir) {
4302
- currentPath = path27.join(currentPath, matchingDir.name);
4327
+ currentPath = path28.join(currentPath, matchingDir.name);
4303
4328
  } else {
4304
4329
  return null;
4305
4330
  }
@@ -4466,7 +4491,7 @@ import fs5 from "node:fs/promises";
4466
4491
  import path7 from "node:path";
4467
4492
  import { randomUUID } from "node:crypto";
4468
4493
  init_logger();
4469
- var log4 = createDebugLogger({ prefix: "ResourceGeneration" });
4494
+ var log5 = createDebugLogger({ prefix: "ResourceGeneration" });
4470
4495
  var META_DIR2 = ".canopy-meta";
4471
4496
  function resourceGenerationPath(root, resource) {
4472
4497
  return path7.resolve(root, META_DIR2, `${resource}.generation`);
@@ -4490,7 +4515,7 @@ async function readResourceGeneration(root, resource) {
4490
4515
  } catch (err) {
4491
4516
  if (isNodeError(err) && err.code === "ENOENT")
4492
4517
  return { ok: true, token: null };
4493
- log4.warn("generation", `Failed to read ${resource} generation for ${root}`, {
4518
+ log5.warn("generation", `Failed to read ${resource} generation for ${root}`, {
4494
4519
  error: getErrorMessage(err)
4495
4520
  });
4496
4521
  return { ok: false };
@@ -4858,7 +4883,7 @@ var STATIC_DEPLOY_USER = Object.freeze({
4858
4883
  // dist/utils/request-timing.js
4859
4884
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
4860
4885
  var storage = new AsyncLocalStorage2();
4861
- var log5 = createDebugLogger({ prefix: "CanopyCMS" });
4886
+ var log6 = createDebugLogger({ prefix: "CanopyCMS" });
4862
4887
  async function timeRequestPhase(phase, fn) {
4863
4888
  const parent = storage.getStore();
4864
4889
  if (!parent)
@@ -5389,13 +5414,13 @@ function entryRecord(parsed, emptyIsRecord) {
5389
5414
  }
5390
5415
  function fillEmptyTopLevelField(raw, format, field, value) {
5391
5416
  if (format === "json" || format === "yaml") {
5392
- const parse2 = (text) => format === "json" ? entryRecord(JSON.parse(text), false) : entryRecord(yamlParse(text), true);
5393
- const data = parse2(raw);
5417
+ const parse = (text) => format === "json" ? entryRecord(JSON.parse(text), false) : entryRecord(yamlParse(text), true);
5418
+ const data = parse(raw);
5394
5419
  if (!isEmptyFieldValue(data[field]))
5395
5420
  return void 0;
5396
5421
  const next = { ...data, [field]: value };
5397
5422
  const content2 = format === "json" ? serializeJson(next, raw) : serializeYaml(next, raw);
5398
- if (parse2(content2)[field] !== value)
5423
+ if (parse(content2)[field] !== value)
5399
5424
  throw new Error(`"${field}" does not read back as written`);
5400
5425
  return content2;
5401
5426
  }
@@ -5585,11 +5610,11 @@ var ContentStore = class {
5585
5610
  getSchemaItems() {
5586
5611
  return this.schemaIndex.values();
5587
5612
  }
5588
- assertSchemaItem(path27) {
5589
- const normalized = normalizeFilesystemPath(path27);
5613
+ assertSchemaItem(path28) {
5614
+ const normalized = normalizeFilesystemPath(path28);
5590
5615
  const item = this.schemaIndex.get(normalized);
5591
5616
  if (!item) {
5592
- throw new ContentStoreError(`Unknown schema item: ${path27}`, "NO_SCHEMA_ITEM");
5617
+ throw new ContentStoreError(`Unknown schema item: ${path28}`, "NO_SCHEMA_ITEM");
5593
5618
  }
5594
5619
  return item;
5595
5620
  }
@@ -5907,7 +5932,7 @@ var ContentStore = class {
5907
5932
  doc.version = stat.mtimeMs;
5908
5933
  return doc;
5909
5934
  }
5910
- async write(collectionPath, slug = "", input, entryTypeName, existingId) {
5935
+ async write(collectionPath, slug = "", input, entryTypeName, existingId, options = {}) {
5911
5936
  await this.idIndex();
5912
5937
  const schemaItem = this.assertSchemaItem(collectionPath);
5913
5938
  let expectedFormat;
@@ -5995,14 +6020,7 @@ var ContentStore = class {
5995
6020
  }
5996
6021
  }
5997
6022
  const existingRaw = current?.content;
5998
- let content;
5999
- if (input.format === "json") {
6000
- content = serializeJson(input.data ?? {}, existingRaw);
6001
- } else if (input.format === "yaml") {
6002
- content = serializeYaml(input.data ?? {}, existingRaw);
6003
- } else {
6004
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
6005
- }
6023
+ const content = serializeEntry(input, existingRaw, fields);
6006
6024
  await atomicWriteFile(absolutePath, content);
6007
6025
  const liveIndex = this._idIndex;
6008
6026
  let staleOldAbsPath = null;
@@ -6029,6 +6047,7 @@ var ContentStore = class {
6029
6047
  });
6030
6048
  }
6031
6049
  await this.recordOwnMutation(liveIndex);
6050
+ await options.afterWrite?.({ id, relativePath });
6032
6051
  const afterStat = await fs9.stat(absolutePath);
6033
6052
  const base = {
6034
6053
  collection: schemaItem.logicalPath,
@@ -6181,14 +6200,21 @@ var ContentStore = class {
6181
6200
  */
6182
6201
  async afterPrePassForTesting() {
6183
6202
  }
6184
- /** Delete an entry and remove it from the index. */
6185
- async delete(collectionPath, slug) {
6203
+ /**
6204
+ * Delete an entry and remove it from the index. With `expectId`, only the entry of that content
6205
+ * ID: another at the slug, or none, is `EntryGoneError`. `beforeDelete` runs under both of the
6206
+ * delete's locks before anything changes, and throwing refuses; `afterDelete` runs once it is
6207
+ * gone, under them too.
6208
+ */
6209
+ async delete(collectionPath, slug, options = {}) {
6186
6210
  await this.idIndex();
6187
6211
  const collection = this.assertCollection(collectionPath);
6188
6212
  const prePass = await this.buildPaths(collection, slug);
6189
6213
  if (prePass.existed)
6190
6214
  this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
6191
- if (!prePass.existed) {
6215
+ if (!prePass.existed || options.expectId !== void 0 && prePass.id !== options.expectId) {
6216
+ if (options.expectId !== void 0)
6217
+ throw new EntryGoneError();
6192
6218
  await fs9.unlink(prePass.absolutePath);
6193
6219
  return;
6194
6220
  }
@@ -6205,6 +6231,10 @@ var ContentStore = class {
6205
6231
  return RETRY_KEY;
6206
6232
  }
6207
6233
  const { absolutePath, relativePath } = inLock;
6234
+ if (options.expectId !== void 0 && (!inLock.existed || inLock.id !== options.expectId)) {
6235
+ throw new EntryGoneError();
6236
+ }
6237
+ await options.beforeDelete?.();
6208
6238
  await fs9.unlink(absolutePath);
6209
6239
  const liveIndex = this._idIndex;
6210
6240
  const id = liveIndex.findByPath(relativePath);
@@ -6212,6 +6242,7 @@ var ContentStore = class {
6212
6242
  liveIndex.remove(id);
6213
6243
  }
6214
6244
  await this.recordOwnMutation(liveIndex);
6245
+ await options.afterDelete?.({ id: id ?? inLock.id });
6215
6246
  return void 0;
6216
6247
  });
6217
6248
  if (outcome !== RETRY_KEY)
@@ -6275,10 +6306,38 @@ var ContentStore = class {
6275
6306
  });
6276
6307
  }
6277
6308
  await this.recordOwnMutation(index);
6309
+ await options.afterWrite?.();
6278
6310
  return { relativePath: target.relativePath };
6279
6311
  });
6280
6312
  });
6281
6313
  }
6314
+ /**
6315
+ * Replace the file of entry `id`, at `slug` in `collectionPath`, with `raw`, verbatim, under
6316
+ * write()'s locks; `beforeWrite` runs under them first, and throwing refuses, and `afterWrite`
6317
+ * after. For a file no parse can be trusted with, so nothing is re-serialised. `EntryGoneError`
6318
+ * when no file of that id is there.
6319
+ */
6320
+ async replaceRaw(collectionPath, slug, entry, options = {}) {
6321
+ await this.idIndex();
6322
+ const collection = this.assertCollection(collectionPath);
6323
+ const prePass = await this.buildPaths(collection, slug);
6324
+ if (!prePass.existed || prePass.id !== entry.id)
6325
+ throw new EntryGoneError();
6326
+ this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
6327
+ const lockKey = this.entryLockKey(collection, slug, prePass);
6328
+ return this.withContentWriteExclusion(() => withLock(lockKey, async () => {
6329
+ const inLock = await this.buildPaths(collection, slug);
6330
+ if (!inLock.existed || inLock.id !== entry.id)
6331
+ throw new EntryGoneError();
6332
+ if (this.entryLockKey(collection, slug, inLock) !== lockKey) {
6333
+ throw new ContentConflictError();
6334
+ }
6335
+ await options.beforeWrite?.();
6336
+ await atomicWriteFile(inLock.absolutePath, entry.raw);
6337
+ await this.recordOwnMutation(this._idIndex);
6338
+ await options.afterWrite?.();
6339
+ }));
6340
+ }
6282
6341
  /**
6283
6342
  * Rename an entry by changing its slug (middle segment of filename).
6284
6343
  * Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
@@ -7239,7 +7298,7 @@ function yamlValue(value) {
7239
7298
  import path12 from "node:path";
7240
7299
 
7241
7300
  // dist/entry-link-resolver.js
7242
- var log6 = createDebugLogger({ prefix: "EntryLinks" });
7301
+ var log7 = createDebugLogger({ prefix: "EntryLinks" });
7243
7302
  var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
7244
7303
  var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
7245
7304
  function resolveEntryUrl(location, contentRoot) {
@@ -7255,7 +7314,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver, opt
7255
7314
  return partText.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
7256
7315
  const location = findEntry(idIndex, id);
7257
7316
  if (!location) {
7258
- log6.warn("resolve", `Entry link target not found: entry:${id}`);
7317
+ log7.warn("resolve", `Entry link target not found: entry:${id}`);
7259
7318
  return isLinkable ? "#" : anchor ?? "#";
7260
7319
  }
7261
7320
  if (isLinkable && !isLinkable(id))
@@ -7991,6 +8050,14 @@ var branchMetadataShape = {
7991
8050
  state: z7.enum(["approved", "changes-requested"]),
7992
8051
  by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
7993
8052
  at: z7.string()
8053
+ }).optional().catch(void 0),
8054
+ baseUpdateRequestedAt: z7.string().optional().catch(void 0),
8055
+ // Dropped when malformed: it only ends the editor's wait, which then times out instead.
8056
+ lastBaseUpdate: z7.object({
8057
+ at: z7.string(),
8058
+ outcome: z7.enum(["merged", "up-to-date", "conflicts", "failed"]),
8059
+ baseSha: z7.string().optional(),
8060
+ keptEntries: z7.number().int().nonnegative().optional()
7994
8061
  }).optional().catch(void 0)
7995
8062
  };
7996
8063
  var branchMetadataSchema = z7.object(branchMetadataShape).passthrough();
@@ -8046,7 +8113,7 @@ function parseBranchMetadataFile(branchRoot, raw) {
8046
8113
  // dist/branch-registry.js
8047
8114
  init_error();
8048
8115
  init_logger();
8049
- var log7 = createDebugLogger({ prefix: "BranchRegistry" });
8116
+ var log8 = createDebugLogger({ prefix: "BranchRegistry" });
8050
8117
  var REGISTRY_FILE = "branches.json";
8051
8118
  var REGISTRY_STALE_FILE = "branches.stale.json";
8052
8119
  var REGISTRY_TEMP_FILE = "branches.tmp.json";
@@ -8172,7 +8239,7 @@ var BranchRegistry = class {
8172
8239
  });
8173
8240
  await this.regenerate();
8174
8241
  } catch (err) {
8175
- log7.warn("invalidate", "Eager regeneration after invalidate() failed", {
8242
+ log8.warn("invalidate", "Eager regeneration after invalidate() failed", {
8176
8243
  error: getErrorMessage(err)
8177
8244
  });
8178
8245
  }
@@ -8268,7 +8335,7 @@ import path16 from "node:path";
8268
8335
  import { randomUUID as randomUUID2 } from "node:crypto";
8269
8336
  import lockfile2 from "proper-lockfile";
8270
8337
  init_logger();
8271
- var log8 = createDebugLogger({ prefix: "OccJsonWrite" });
8338
+ var log9 = createDebugLogger({ prefix: "OccJsonWrite" });
8272
8339
  var OccWriteConflictError = class extends Error {
8273
8340
  constructor(message = "Concurrent modification detected") {
8274
8341
  super(message);
@@ -8386,7 +8453,7 @@ async function withOccFileLock(filePath, fn) {
8386
8453
  await release().catch((err) => {
8387
8454
  if (isNodeError(err) && err.code === "ENOENT")
8388
8455
  return;
8389
- log8.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
8456
+ log9.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
8390
8457
  });
8391
8458
  }
8392
8459
  }
@@ -8839,7 +8906,7 @@ function feedStdin(child, source, touch, onSourceError) {
8839
8906
  }
8840
8907
 
8841
8908
  // dist/git-manager.js
8842
- var log9 = createDebugLogger({ prefix: "GitManager" });
8909
+ var log10 = createDebugLogger({ prefix: "GitManager" });
8843
8910
  var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
8844
8911
  var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
8845
8912
  function gitChildEnv(overrides) {
@@ -8961,13 +9028,13 @@ async function ensureGitExcludePattern(repoPath, pattern) {
8961
9028
  }
8962
9029
  const lines = content.split("\n");
8963
9030
  if (lines.some((line) => line.trim() === pattern)) {
8964
- log9.debug("git", "Pattern already in .git/info/exclude", { pattern });
9031
+ log10.debug("git", "Pattern already in .git/info/exclude", { pattern });
8965
9032
  return;
8966
9033
  }
8967
9034
  const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
8968
9035
  const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
8969
9036
  await fs16.writeFile(excludePath, newContent, "utf-8");
8970
- log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
9037
+ log10.debug("git", "Added pattern to .git/info/exclude", { pattern });
8971
9038
  }
8972
9039
  var BaseBranchMissingError = class extends Error {
8973
9040
  constructor(remote, baseBranch) {
@@ -9053,7 +9120,7 @@ var GitManager = class _GitManager {
9053
9120
  * target that held files beforehand is never touched.
9054
9121
  */
9055
9122
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main", options = {}) {
9056
- log9.debug("git", "Cloning repository", {
9123
+ log10.debug("git", "Cloning repository", {
9057
9124
  remoteUrl,
9058
9125
  targetPath,
9059
9126
  baseBranch
@@ -9085,7 +9152,7 @@ var GitManager = class _GitManager {
9085
9152
  await fs16.mkdir(targetPath);
9086
9153
  await git.clone(remoteUrl, targetPath, args);
9087
9154
  }
9088
- log9.debug("git", "Clone complete");
9155
+ log10.debug("git", "Clone complete");
9089
9156
  }
9090
9157
  /**
9091
9158
  * Initializes a local bare git repository to simulate a remote for dev mode,
@@ -9099,15 +9166,15 @@ var GitManager = class _GitManager {
9099
9166
  static async ensureLocalSimulatedRemote(options) {
9100
9167
  const existingLock = remoteInitLocks.get(options.remotePath);
9101
9168
  if (existingLock) {
9102
- log9.debug("git", "Waiting for existing remote initialization", {
9169
+ log10.debug("git", "Waiting for existing remote initialization", {
9103
9170
  remotePath: options.remotePath
9104
9171
  });
9105
9172
  await existingLock;
9106
9173
  }
9107
- const lockPromise = log9.timed("git", "ensureLocalSimulatedRemote", async () => {
9174
+ const lockPromise = log10.timed("git", "ensureLocalSimulatedRemote", async () => {
9108
9175
  let releaseLock;
9109
9176
  try {
9110
- log9.debug("git", "Initializing local simulated remote", {
9177
+ log10.debug("git", "Initializing local simulated remote", {
9111
9178
  remotePath: options.remotePath,
9112
9179
  baseBranch: options.baseBranch
9113
9180
  });
@@ -9125,15 +9192,15 @@ var GitManager = class _GitManager {
9125
9192
  try {
9126
9193
  const repack = await repackBareRemoteIfNeeded(options.remotePath);
9127
9194
  if (repack.repacked)
9128
- log9.debug("git", "Repacked local simulated remote", repack);
9195
+ log10.debug("git", "Repacked local simulated remote", repack);
9129
9196
  } catch (err) {
9130
- log9.debug("git", "Could not repack local simulated remote", {
9197
+ log10.debug("git", "Could not repack local simulated remote", {
9131
9198
  error: getErrorMessage(err)
9132
9199
  });
9133
9200
  }
9134
9201
  }
9135
9202
  if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
9136
- log9.debug("git", "Remote already has base branch, skipping");
9203
+ log10.debug("git", "Remote already has base branch, skipping");
9137
9204
  await ensureRemoteGitConfig(options.remotePath);
9138
9205
  return;
9139
9206
  }
@@ -9153,8 +9220,8 @@ var GitManager = class _GitManager {
9153
9220
  }
9154
9221
  let hasCommits = false;
9155
9222
  try {
9156
- const log11 = await sourceGit.log(["-1"]);
9157
- hasCommits = log11.total > 0;
9223
+ const log12 = await sourceGit.log(["-1"]);
9224
+ hasCommits = log12.total > 0;
9158
9225
  } catch {
9159
9226
  hasCommits = false;
9160
9227
  }
@@ -9166,12 +9233,12 @@ var GitManager = class _GitManager {
9166
9233
  throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
9167
9234
  }
9168
9235
  if (remoteExists) {
9169
- log9.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
9236
+ log10.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
9170
9237
  remotePath: options.remotePath,
9171
9238
  baseBranch: options.baseBranch
9172
9239
  });
9173
9240
  } else {
9174
- log9.debug("git", "Creating bare remote repository");
9241
+ log10.debug("git", "Creating bare remote repository");
9175
9242
  await simpleGit2().raw([
9176
9243
  "init",
9177
9244
  "--bare",
@@ -9186,13 +9253,13 @@ var GitManager = class _GitManager {
9186
9253
  baseBranch: options.baseBranch,
9187
9254
  subdirectory: options.subdirectory
9188
9255
  });
9189
- log9.debug("git", "Remote initialization complete");
9256
+ log10.debug("git", "Remote initialization complete");
9190
9257
  } finally {
9191
9258
  if (releaseLock) {
9192
9259
  try {
9193
9260
  await releaseLock();
9194
9261
  } catch (err) {
9195
- log9.debug("git", "Failed to release remote-init lock", { err });
9262
+ log10.debug("git", "Failed to release remote-init lock", { err });
9196
9263
  }
9197
9264
  }
9198
9265
  remoteInitLocks.delete(options.remotePath);
@@ -9474,7 +9541,7 @@ var GitManager = class _GitManager {
9474
9541
  throw new Error(`CanopyCMS: cannot read the git remote at ${remotePath}: ${getErrorMessage(err)}`);
9475
9542
  });
9476
9543
  if (stat?.isDirectory()) {
9477
- log9.debug("git", "Auto-detected local remote", { path: remotePath });
9544
+ log10.debug("git", "Auto-detected local remote", { path: remotePath });
9478
9545
  return remotePath;
9479
9546
  }
9480
9547
  if (stat) {
@@ -9600,7 +9667,7 @@ var GitManager = class _GitManager {
9600
9667
  try {
9601
9668
  const stat = await fs16.stat(gitPath);
9602
9669
  if (stat.isDirectory()) {
9603
- log9.debug("git", "Removing corrupt .git directory", {
9670
+ log10.debug("git", "Removing corrupt .git directory", {
9604
9671
  workspacePath: options.workspacePath
9605
9672
  });
9606
9673
  await fs16.rm(gitPath, { recursive: true });
@@ -10066,17 +10133,17 @@ var GitManager = class _GitManager {
10066
10133
  const changes = [];
10067
10134
  for (let i = 0; i + 1 < tokens.length; i += 2) {
10068
10135
  const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
10069
- const path27 = tokens[i + 1];
10136
+ const path28 = tokens[i + 1];
10070
10137
  const letter = code?.charAt(0);
10071
10138
  if (letter === "A" || letter === "D")
10072
- changes.push({ status: letter, path: path27 });
10139
+ changes.push({ status: letter, path: path28 });
10073
10140
  else if (letter === "M" || letter === "T") {
10074
10141
  const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
10075
- changes.push({ status: "M", path: path27, unhashed });
10142
+ changes.push({ status: "M", path: path28, unhashed });
10076
10143
  }
10077
10144
  }
10078
10145
  const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
10079
- return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path27 }) => ({ status, path: path27 }));
10146
+ return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path28 }) => ({ status, path: path28 }));
10080
10147
  }
10081
10148
  /**
10082
10149
  * The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
@@ -10171,6 +10238,32 @@ var GitManager = class _GitManager {
10171
10238
  throw err;
10172
10239
  }
10173
10240
  }
10241
+ /**
10242
+ * The text of the blob `sha`, `'too-large'` when it is over `maxBytes`, or `undefined` when this
10243
+ * clone has no such blob. Any other failure throws.
10244
+ */
10245
+ async readBlob(sha, maxBytes) {
10246
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha))
10247
+ return void 0;
10248
+ let sizeText;
10249
+ try {
10250
+ sizeText = (await this.git.raw(["cat-file", "-s", "--end-of-options", sha])).trim();
10251
+ } catch (err) {
10252
+ if (/could not get object info|Not a valid object name/.test(getErrorMessage(err))) {
10253
+ return void 0;
10254
+ }
10255
+ throw err;
10256
+ }
10257
+ if (!/^\d+$/.test(sizeText))
10258
+ throw new Error(`git cat-file -s ${sha} printed no size`);
10259
+ const size = Number(sizeText);
10260
+ if (size > maxBytes)
10261
+ return "too-large";
10262
+ const text = await this.git.raw(["cat-file", "blob", "--end-of-options", sha]);
10263
+ if (size > 0 && text === "")
10264
+ throw new Error(`git cat-file blob ${sha} printed nothing`);
10265
+ return text;
10266
+ }
10174
10267
  async ensureAuthor(author) {
10175
10268
  const config = await this.git.listConfig();
10176
10269
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -10259,7 +10352,7 @@ var GitManager = class _GitManager {
10259
10352
  }
10260
10353
  }
10261
10354
  async createOrphanSettingsBranchInner(branchName, initialFiles) {
10262
- log9.debug("git", "Ensuring settings branch", { branchName });
10355
+ log10.debug("git", "Ensuring settings branch", { branchName });
10263
10356
  const branches = await this.git.branch();
10264
10357
  const localExists = branches.all.includes(branchName);
10265
10358
  if (localExists) {
@@ -10270,7 +10363,7 @@ var GitManager = class _GitManager {
10270
10363
  remoteTip = await this.remoteBranchTip(branchName);
10271
10364
  } catch (err) {
10272
10365
  if (localExists && !await this.isEmptyInitialBranch(branchName)) {
10273
- log9.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
10366
+ log10.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
10274
10367
  return;
10275
10368
  }
10276
10369
  throw new SettingsRemoteUnreadableError(branchName, this.remote, err);
@@ -10283,7 +10376,7 @@ var GitManager = class _GitManager {
10283
10376
  const fetchedTip = await this.fetchBranchTip(branchName);
10284
10377
  await this.assertNotContentHistory(branchName, fetchedTip);
10285
10378
  await this.git.raw(["checkout", "-b", branchName, fetchedTip]);
10286
- log9.debug("git", "Checked out the remote settings branch", { branchName });
10379
+ log10.debug("git", "Checked out the remote settings branch", { branchName });
10287
10380
  return;
10288
10381
  }
10289
10382
  await this.git.raw(["switch", "--orphan", branchName]);
@@ -10299,7 +10392,7 @@ var GitManager = class _GitManager {
10299
10392
  await this.git.add(filePath);
10300
10393
  }
10301
10394
  await this.git.commit("Initialize settings branch", ["--allow-empty"]);
10302
- log9.debug("git", "Orphan settings branch created", { branchName });
10395
+ log10.debug("git", "Orphan settings branch created", { branchName });
10303
10396
  }
10304
10397
  /**
10305
10398
  * Compares the checked-out local settings branch with the remote's tip.
@@ -10332,7 +10425,7 @@ var GitManager = class _GitManager {
10332
10425
  }
10333
10426
  await this.assertNotContentHistory(branchName, fetchedTip);
10334
10427
  await this.git.raw(["checkout", "-B", branchName, fetchedTip]);
10335
- log9.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
10428
+ log10.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
10336
10429
  }
10337
10430
  /**
10338
10431
  * The remote's tip for `branch`, or `undefined` when the remote has no such
@@ -10602,10 +10695,10 @@ async function hasBranchMetadata(branchRoot) {
10602
10695
  return fs17.access(path21.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE)).then(() => true, () => false);
10603
10696
  }
10604
10697
  async function stageBranchWorkspace(options) {
10605
- const { provisionLog: log11 } = options;
10698
+ const { provisionLog: log12 } = options;
10606
10699
  const stagingPath = path21.join(options.baseRoot, stagingDirName(options.dirName));
10607
10700
  try {
10608
- await log11.step("clone", () => GitManager.cloneWorkspace(options.remoteUrl, stagingPath, options.baseBranch, {
10701
+ await log12.step("clone", () => GitManager.cloneWorkspace(options.remoteUrl, stagingPath, options.baseBranch, {
10609
10702
  ...options.clone,
10610
10703
  noCheckout: true
10611
10704
  }));
@@ -10616,14 +10709,14 @@ async function stageBranchWorkspace(options) {
10616
10709
  });
10617
10710
  const cone = options.sparseCone;
10618
10711
  if (cone)
10619
- await log11.step("sparse", () => git.setSparseCone(cone));
10712
+ await log12.step("sparse", () => git.setSparseCone(cone));
10620
10713
  if (await hasBranchMetadata(path21.join(options.baseRoot, options.dirName))) {
10621
10714
  await removeLeftoverDir(stagingPath);
10622
10715
  return { kind: "exists" };
10623
10716
  }
10624
- await log11.step("checkout", () => git.checkoutFreshClone(options.branchName));
10625
- await log11.step("exclude", () => git.ensureGitExclude(options.gitExcludePattern));
10626
- const written = await log11.step("metadata", () => writeOccJsonFile(path21.join(stagingPath, BRANCH_META_DIR, BRANCH_META_FILE), { ...options.metadata }, { expectedVersion: null, trailingNewline: true }));
10717
+ await log12.step("checkout", () => git.checkoutFreshClone(options.branchName));
10718
+ await log12.step("exclude", () => git.ensureGitExclude(options.gitExcludePattern));
10719
+ const written = await log12.step("metadata", () => writeOccJsonFile(path21.join(stagingPath, BRANCH_META_DIR, BRANCH_META_FILE), { ...options.metadata }, { expectedVersion: null, trailingNewline: true }));
10627
10720
  return {
10628
10721
  kind: "staged",
10629
10722
  stagingPath,
@@ -11095,7 +11188,7 @@ import path24 from "node:path";
11095
11188
  import matter3 from "gray-matter";
11096
11189
  import { parse as yamlParse2 } from "yaml";
11097
11190
  init_error();
11098
- var log10 = createDebugLogger({ prefix: "ContentListing" });
11191
+ var log11 = createDebugLogger({ prefix: "ContentListing" });
11099
11192
  var normalizePath = (root, target) => {
11100
11193
  const result = validateAndNormalizePath(root, target);
11101
11194
  if (!result.valid) {
@@ -11123,7 +11216,7 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
11123
11216
  } catch (err) {
11124
11217
  if (isNotFoundError(err))
11125
11218
  return {};
11126
- log10.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
11219
+ log11.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
11127
11220
  return {};
11128
11221
  }
11129
11222
  };
@@ -11249,7 +11342,7 @@ var listCollectionEntries = async (root, collection, onSkip, readsId) => {
11249
11342
  const relativePath = normalizePath(root, absolutePath);
11250
11343
  const parsed = parseTypedFilename(file.name, entryTypes);
11251
11344
  if (!parsed) {
11252
- log10.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
11345
+ log11.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
11253
11346
  if (looksLikeMalformedEntry(file.name, entryTypes)) {
11254
11347
  onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
11255
11348
  }
@@ -11338,77 +11431,77 @@ function isValidImageCropValue(value) {
11338
11431
  const rounded = roundCropRect({ x, y, w, h });
11339
11432
  return isValidCropRect(rounded.x, rounded.y, rounded.w, rounded.h);
11340
11433
  }
11341
- function validateImageValue(field, value, path27) {
11434
+ function validateImageValue(field, value, path28) {
11342
11435
  if (!isPlainRecord3(value)) {
11343
- return [{ fieldPath: path27, message: "Expected an image object with { src, alt }" }];
11436
+ return [{ fieldPath: path28, message: "Expected an image object with { src, alt }" }];
11344
11437
  }
11345
11438
  const errors = [];
11346
11439
  if (typeof value.src !== "string" || value.src.trim() === "") {
11347
- errors.push({ fieldPath: `${path27}.src`, message: "Image src is required" });
11440
+ errors.push({ fieldPath: `${path28}.src`, message: "Image src is required" });
11348
11441
  }
11349
11442
  if (typeof value.alt !== "string") {
11350
- errors.push({ fieldPath: `${path27}.alt`, message: "Image alt text is required" });
11443
+ errors.push({ fieldPath: `${path28}.alt`, message: "Image alt text is required" });
11351
11444
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
11352
- errors.push({ fieldPath: `${path27}.alt`, message: "Image alt text is required" });
11445
+ errors.push({ fieldPath: `${path28}.alt`, message: "Image alt text is required" });
11353
11446
  }
11354
11447
  if (value.width !== void 0 && !isPositiveInt(value.width)) {
11355
- errors.push({ fieldPath: `${path27}.width`, message: "Image width must be a positive integer" });
11448
+ errors.push({ fieldPath: `${path28}.width`, message: "Image width must be a positive integer" });
11356
11449
  }
11357
11450
  if (value.height !== void 0 && !isPositiveInt(value.height)) {
11358
11451
  errors.push({
11359
- fieldPath: `${path27}.height`,
11452
+ fieldPath: `${path28}.height`,
11360
11453
  message: "Image height must be a positive integer"
11361
11454
  });
11362
11455
  }
11363
11456
  if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
11364
- errors.push({ fieldPath: `${path27}.crop`, message: "Invalid image crop rect" });
11457
+ errors.push({ fieldPath: `${path28}.crop`, message: "Invalid image crop rect" });
11365
11458
  }
11366
11459
  return errors;
11367
11460
  }
11368
11461
  var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
11369
- function validateScalar(field, value, path27) {
11462
+ function validateScalar(field, value, path28) {
11370
11463
  switch (field.type) {
11371
11464
  case "number":
11372
11465
  if (typeof value !== "number" || !Number.isFinite(value)) {
11373
- return [{ fieldPath: path27, message: "Expected a number" }];
11466
+ return [{ fieldPath: path28, message: "Expected a number" }];
11374
11467
  }
11375
11468
  return [];
11376
11469
  case "boolean":
11377
11470
  if (typeof value !== "boolean") {
11378
- return [{ fieldPath: path27, message: "Expected true or false" }];
11471
+ return [{ fieldPath: path28, message: "Expected true or false" }];
11379
11472
  }
11380
11473
  return [];
11381
11474
  case "date":
11382
11475
  if (value !== "" && !isCalendarDay(value)) {
11383
- return [{ fieldPath: path27, message: "Expected a real date, written YYYY-MM-DD" }];
11476
+ return [{ fieldPath: path28, message: "Expected a real date, written YYYY-MM-DD" }];
11384
11477
  }
11385
11478
  return [];
11386
11479
  case "datetime":
11387
11480
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
11388
- return [{ fieldPath: path27, message: "Expected a valid date/time string" }];
11481
+ return [{ fieldPath: path28, message: "Expected a valid date/time string" }];
11389
11482
  }
11390
11483
  return [];
11391
11484
  case "select": {
11392
11485
  if (typeof value !== "string") {
11393
- return [{ fieldPath: path27, message: "Expected a selection" }];
11486
+ return [{ fieldPath: path28, message: "Expected a selection" }];
11394
11487
  }
11395
11488
  const options = selectOptionValues(field);
11396
11489
  if (value !== "" && !options.includes(value)) {
11397
- return [{ fieldPath: path27, message: `Must be one of: ${options.join(", ")}` }];
11490
+ return [{ fieldPath: path28, message: `Must be one of: ${options.join(", ")}` }];
11398
11491
  }
11399
11492
  return [];
11400
11493
  }
11401
11494
  case "reference":
11402
11495
  if (referenceValueId(value) === void 0) {
11403
- return [{ fieldPath: path27, message: "Expected a reference id" }];
11496
+ return [{ fieldPath: path28, message: "Expected a reference id" }];
11404
11497
  }
11405
11498
  return [];
11406
11499
  case "image":
11407
- return validateImageValue(field, value, path27);
11500
+ return validateImageValue(field, value, path28);
11408
11501
  default:
11409
11502
  if (STRING_FIELD_TYPES.has(field.type)) {
11410
11503
  if (typeof value !== "string") {
11411
- return [{ fieldPath: path27, message: "Expected text" }];
11504
+ return [{ fieldPath: path28, message: "Expected text" }];
11412
11505
  }
11413
11506
  }
11414
11507
  return [];
@@ -11431,12 +11524,12 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
11431
11524
  errors.push(...validateEntryData(field.fields, data, pathPrefix, options));
11432
11525
  continue;
11433
11526
  }
11434
- const path27 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
11527
+ const path28 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
11435
11528
  const value = data[field.name];
11436
11529
  const isRequired = options.required !== false && "required" in field && field.required === true;
11437
11530
  if (isRequired && isEmptyForRequired(field, value)) {
11438
11531
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
11439
- errors.push({ fieldPath: path27, message });
11532
+ errors.push({ fieldPath: path28, message });
11440
11533
  continue;
11441
11534
  }
11442
11535
  if (value === void 0 || value === null)
@@ -11444,7 +11537,7 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
11444
11537
  if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
11445
11538
  const cap = field.maxItems;
11446
11539
  errors.push({
11447
- fieldPath: path27,
11540
+ fieldPath: path28,
11448
11541
  message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
11449
11542
  maxItems: { count: value.length, cap }
11450
11543
  });
@@ -11452,11 +11545,11 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
11452
11545
  if (field.type === "block") {
11453
11546
  const blockField = field;
11454
11547
  if (!Array.isArray(value)) {
11455
- errors.push({ fieldPath: path27, message: "Expected a list of blocks" });
11548
+ errors.push({ fieldPath: path28, message: "Expected a list of blocks" });
11456
11549
  continue;
11457
11550
  }
11458
11551
  value.forEach((item, index) => {
11459
- const itemPath = `${path27}[${index}]`;
11552
+ const itemPath = `${path28}[${index}]`;
11460
11553
  if (!isPlainRecord3(item)) {
11461
11554
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
11462
11555
  return;
@@ -11478,11 +11571,11 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
11478
11571
  const objectField = field;
11479
11572
  if (objectField.list) {
11480
11573
  if (!Array.isArray(value)) {
11481
- errors.push({ fieldPath: path27, message: "Expected a list of items" });
11574
+ errors.push({ fieldPath: path28, message: "Expected a list of items" });
11482
11575
  continue;
11483
11576
  }
11484
11577
  value.forEach((item, index) => {
11485
- const itemPath = `${path27}[${index}]`;
11578
+ const itemPath = `${path28}[${index}]`;
11486
11579
  if (!isPlainRecord3(item)) {
11487
11580
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
11488
11581
  return;
@@ -11492,27 +11585,27 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
11492
11585
  }
11493
11586
  });
11494
11587
  } else if (!isPlainRecord3(value)) {
11495
- errors.push({ fieldPath: path27, message: "Expected an object" });
11588
+ errors.push({ fieldPath: path28, message: "Expected an object" });
11496
11589
  } else if (objectField.fields) {
11497
- errors.push(...validateEntryData(objectField.fields, value, path27, options));
11590
+ errors.push(...validateEntryData(objectField.fields, value, path28, options));
11498
11591
  }
11499
11592
  continue;
11500
11593
  }
11501
11594
  if ("list" in field && field.list) {
11502
11595
  if (!Array.isArray(value)) {
11503
- errors.push({ fieldPath: path27, message: "Expected a list of values" });
11596
+ errors.push({ fieldPath: path28, message: "Expected a list of values" });
11504
11597
  continue;
11505
11598
  }
11506
11599
  value.forEach((item, index) => {
11507
- errors.push(...validateScalar(field, item, `${path27}[${index}]`));
11600
+ errors.push(...validateScalar(field, item, `${path28}[${index}]`));
11508
11601
  });
11509
11602
  continue;
11510
11603
  }
11511
11604
  if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
11512
- errors.push({ fieldPath: path27, message: "Expected a single value, not a list" });
11605
+ errors.push({ fieldPath: path28, message: "Expected a single value, not a list" });
11513
11606
  continue;
11514
11607
  }
11515
- errors.push(...validateScalar(field, value, path27));
11608
+ errors.push(...validateScalar(field, value, path28));
11516
11609
  }
11517
11610
  return errors;
11518
11611
  }
@@ -11714,33 +11807,50 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
11714
11807
 
11715
11808
  // dist/cli/generate-ai-content.js
11716
11809
  init_error();
11810
+
11811
+ // dist/cli/site-config.js
11812
+ import path26 from "node:path";
11813
+ import { createJiti } from "jiti";
11717
11814
  var jiti = createJiti(import.meta.url);
11815
+ async function importSiteModule(filePath) {
11816
+ return await jiti.import(filePath);
11817
+ }
11818
+ async function importSiteServerConfig(projectDir) {
11819
+ const configPath = path26.join(projectDir, "canopycms.config.ts");
11820
+ const mod = await importSiteModule(configPath);
11821
+ const configExport = mod.default ?? mod.config ?? mod;
11822
+ const server = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
11823
+ return { configPath, server };
11824
+ }
11825
+ async function importSiteEntrySchemaRegistry(projectDir, appDir) {
11826
+ const schemasPath = path26.join(projectDir, appDir, "schemas.ts");
11827
+ const mod = await importSiteModule(schemasPath);
11828
+ const registry2 = mod.entrySchemaRegistry ?? mod;
11829
+ return { schemasPath, registry: registry2 };
11830
+ }
11831
+
11832
+ // dist/cli/generate-ai-content.js
11718
11833
  async function generateAIContentCLI(options) {
11719
11834
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
11720
11835
  console.log("\nCanopyCMS generate-ai-content\n");
11721
- const canopyConfigPath = path26.join(projectDir, "canopycms.config.ts");
11722
- let canopyConfigModule;
11836
+ let serverConfig;
11723
11837
  try {
11724
- canopyConfigModule = await jiti.import(canopyConfigPath);
11838
+ serverConfig = (await importSiteServerConfig(projectDir)).server;
11725
11839
  } catch (err) {
11726
- console.error(`Could not load config from ${canopyConfigPath}`);
11840
+ console.error(`Could not load config from ${path27.join(projectDir, "canopycms.config.ts")}`);
11727
11841
  console.error(getErrorMessage(err));
11728
11842
  process.exit(1);
11729
11843
  }
11730
- const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
11731
- const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
11732
- const schemasPath = path26.join(projectDir, appDir, "schemas.ts");
11733
11844
  let entrySchemaRegistry = {};
11734
11845
  try {
11735
- const schemasModule = await jiti.import(schemasPath);
11736
- entrySchemaRegistry = schemasModule.entrySchemaRegistry ?? schemasModule;
11846
+ entrySchemaRegistry = (await importSiteEntrySchemaRegistry(projectDir, appDir)).registry;
11737
11847
  } catch {
11738
11848
  console.warn(` No ${appDir}/schemas.ts found, using empty entry schema registry`);
11739
11849
  }
11740
11850
  let aiConfig;
11741
11851
  if (configPath) {
11742
11852
  try {
11743
- const aiConfigModule = await jiti.import(path26.resolve(configPath));
11853
+ const aiConfigModule = await importSiteModule(path27.resolve(configPath));
11744
11854
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
11745
11855
  } catch (err) {
11746
11856
  console.error(`Could not load AI config from ${configPath}`);
@@ -11757,7 +11867,7 @@ async function generateAIContentCLI(options) {
11757
11867
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
11758
11868
  process.exit(1);
11759
11869
  }
11760
- const resolvedOutput = path26.resolve(projectDir, outputDir);
11870
+ const resolvedOutput = path27.resolve(projectDir, outputDir);
11761
11871
  console.log(` Output: ${resolvedOutput}`);
11762
11872
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
11763
11873
  const result = await generateAIContentFiles({