canopycms 0.0.53 → 0.0.55

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 (216) hide show
  1. package/dist/ai/generate.d.ts.map +1 -1
  2. package/dist/ai/generate.js +68 -2
  3. package/dist/ai/generate.js.map +1 -1
  4. package/dist/ai/index.d.ts +1 -1
  5. package/dist/ai/index.d.ts.map +1 -1
  6. package/dist/ai/json-to-markdown.d.ts.map +1 -1
  7. package/dist/ai/json-to-markdown.js +87 -0
  8. package/dist/ai/json-to-markdown.js.map +1 -1
  9. package/dist/ai/types.d.ts +54 -0
  10. package/dist/ai/types.d.ts.map +1 -1
  11. package/dist/ai/types.js.map +1 -1
  12. package/dist/api/assets.d.ts +14 -2
  13. package/dist/api/assets.d.ts.map +1 -1
  14. package/dist/api/assets.js +14 -3
  15. package/dist/api/assets.js.map +1 -1
  16. package/dist/api/branch-status.d.ts.map +1 -1
  17. package/dist/api/branch-status.js +8 -2
  18. package/dist/api/branch-status.js.map +1 -1
  19. package/dist/api/client.d.ts +2 -2
  20. package/dist/api/client.d.ts.map +1 -1
  21. package/dist/api/client.js +29 -14
  22. package/dist/api/client.js.map +1 -1
  23. package/dist/api/content.d.ts +2 -2
  24. package/dist/api/content.d.ts.map +1 -1
  25. package/dist/api/content.js +160 -26
  26. package/dist/api/content.js.map +1 -1
  27. package/dist/api/entries.d.ts.map +1 -1
  28. package/dist/api/entries.js +8 -4
  29. package/dist/api/entries.js.map +1 -1
  30. package/dist/api/github-sync.d.ts.map +1 -1
  31. package/dist/api/github-sync.js +21 -4
  32. package/dist/api/github-sync.js.map +1 -1
  33. package/dist/api/groups.d.ts.map +1 -1
  34. package/dist/api/groups.js +15 -5
  35. package/dist/api/groups.js.map +1 -1
  36. package/dist/api/permissions.d.ts +6 -6
  37. package/dist/api/permissions.d.ts.map +1 -1
  38. package/dist/api/permissions.js +24 -9
  39. package/dist/api/permissions.js.map +1 -1
  40. package/dist/api/request-body-hash.d.ts +33 -0
  41. package/dist/api/request-body-hash.d.ts.map +1 -0
  42. package/dist/api/request-body-hash.js +43 -0
  43. package/dist/api/request-body-hash.js.map +1 -0
  44. package/dist/api/resolve-references.d.ts.map +1 -1
  45. package/dist/api/resolve-references.js +8 -1
  46. package/dist/api/resolve-references.js.map +1 -1
  47. package/dist/api/schema.js +9 -9
  48. package/dist/api/schema.js.map +1 -1
  49. package/dist/api/settings-helpers.d.ts +14 -1
  50. package/dist/api/settings-helpers.d.ts.map +1 -1
  51. package/dist/api/settings-helpers.js +11 -2
  52. package/dist/api/settings-helpers.js.map +1 -1
  53. package/dist/api/types.d.ts +10 -0
  54. package/dist/api/types.d.ts.map +1 -1
  55. package/dist/auth/index.d.ts +1 -0
  56. package/dist/auth/index.d.ts.map +1 -1
  57. package/dist/auth/index.js +1 -0
  58. package/dist/auth/index.js.map +1 -1
  59. package/dist/auth/plugin.d.ts +26 -0
  60. package/dist/auth/plugin.d.ts.map +1 -1
  61. package/dist/auth/plugin.js +25 -1
  62. package/dist/auth/plugin.js.map +1 -1
  63. package/dist/authorization/helpers.d.ts +16 -0
  64. package/dist/authorization/helpers.d.ts.map +1 -1
  65. package/dist/authorization/helpers.js +18 -0
  66. package/dist/authorization/helpers.js.map +1 -1
  67. package/dist/authorization/index.d.ts +1 -1
  68. package/dist/authorization/index.d.ts.map +1 -1
  69. package/dist/authorization/index.js +1 -1
  70. package/dist/authorization/index.js.map +1 -1
  71. package/dist/cli/cli.d.ts.map +1 -1
  72. package/dist/cli/cli.js +2 -1
  73. package/dist/cli/cli.js.map +1 -1
  74. package/dist/cli/generate-ai-content.js +834 -339
  75. package/dist/cli/init.d.ts +1 -1
  76. package/dist/cli/init.d.ts.map +1 -1
  77. package/dist/cli/init.js +28 -9
  78. package/dist/cli/init.js.map +1 -1
  79. package/dist/cli/sync.d.ts.map +1 -1
  80. package/dist/cli/sync.js +88 -60
  81. package/dist/cli/sync.js.map +1 -1
  82. package/dist/cli/template-files/canopy.ts.template +2 -1
  83. package/dist/cli/template-files/edit-page.tsx.template +8 -1
  84. package/dist/cli/template-files/middleware.ts.template +18 -0
  85. package/dist/cli/templates.d.ts.map +1 -1
  86. package/dist/cli/templates.js +16 -4
  87. package/dist/cli/templates.js.map +1 -1
  88. package/dist/config/helpers.d.ts +9 -1
  89. package/dist/config/helpers.d.ts.map +1 -1
  90. package/dist/config/helpers.js +30 -75
  91. package/dist/config/helpers.js.map +1 -1
  92. package/dist/config/schemas/config.d.ts +5 -20
  93. package/dist/config/schemas/config.d.ts.map +1 -1
  94. package/dist/config/schemas/config.js +6 -2
  95. package/dist/config/schemas/config.js.map +1 -1
  96. package/dist/config/schemas/media.d.ts +1 -10
  97. package/dist/config/schemas/media.d.ts.map +1 -1
  98. package/dist/config/schemas/media.js +8 -6
  99. package/dist/config/schemas/media.js.map +1 -1
  100. package/dist/config/types.d.ts +0 -3
  101. package/dist/config/types.d.ts.map +1 -1
  102. package/dist/content-id-index.d.ts +12 -6
  103. package/dist/content-id-index.d.ts.map +1 -1
  104. package/dist/content-id-index.js +12 -6
  105. package/dist/content-id-index.js.map +1 -1
  106. package/dist/content-index-generation.d.ts +30 -0
  107. package/dist/content-index-generation.d.ts.map +1 -0
  108. package/dist/content-index-generation.js +131 -0
  109. package/dist/content-index-generation.js.map +1 -0
  110. package/dist/content-index-registry.d.ts +38 -0
  111. package/dist/content-index-registry.d.ts.map +1 -0
  112. package/dist/content-index-registry.js +53 -0
  113. package/dist/content-index-registry.js.map +1 -0
  114. package/dist/content-store.d.ts +130 -6
  115. package/dist/content-store.d.ts.map +1 -1
  116. package/dist/content-store.js +333 -31
  117. package/dist/content-store.js.map +1 -1
  118. package/dist/editor/Editor.d.ts.map +1 -1
  119. package/dist/editor/Editor.js +8 -4
  120. package/dist/editor/Editor.js.map +1 -1
  121. package/dist/editor/FormRenderer.d.ts +7 -0
  122. package/dist/editor/FormRenderer.d.ts.map +1 -1
  123. package/dist/editor/FormRenderer.js +12 -3
  124. package/dist/editor/FormRenderer.js.map +1 -1
  125. package/dist/editor/components/EditorHeader.d.ts.map +1 -1
  126. package/dist/editor/components/EditorHeader.js +3 -4
  127. package/dist/editor/components/EditorHeader.js.map +1 -1
  128. package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
  129. package/dist/editor/components/EditorSidebar.js +2 -4
  130. package/dist/editor/components/EditorSidebar.js.map +1 -1
  131. package/dist/editor/fields/ReferenceField.d.ts.map +1 -1
  132. package/dist/editor/fields/ReferenceField.js +19 -2
  133. package/dist/editor/fields/ReferenceField.js.map +1 -1
  134. package/dist/editor/group-manager/hooks/useGroupState.d.ts.map +1 -1
  135. package/dist/editor/group-manager/hooks/useGroupState.js +5 -2
  136. package/dist/editor/group-manager/hooks/useGroupState.js.map +1 -1
  137. package/dist/editor/hooks/useDraftManager.d.ts +8 -0
  138. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  139. package/dist/editor/hooks/useDraftManager.js +102 -1
  140. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  141. package/dist/editor/hooks/useEntryManager.d.ts +9 -2
  142. package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
  143. package/dist/editor/hooks/useEntryManager.js +9 -3
  144. package/dist/editor/hooks/useEntryManager.js.map +1 -1
  145. package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
  146. package/dist/editor/hooks/useSchemaManager.js +2 -1
  147. package/dist/editor/hooks/useSchemaManager.js.map +1 -1
  148. package/dist/editor/schema-editor/CollectionEditor.d.ts +4 -4
  149. package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
  150. package/dist/editor/schema-editor/CollectionEditor.js +32 -12
  151. package/dist/editor/schema-editor/CollectionEditor.js.map +1 -1
  152. package/dist/entry-link-resolver.d.ts.map +1 -1
  153. package/dist/entry-link-resolver.js +5 -0
  154. package/dist/entry-link-resolver.js.map +1 -1
  155. package/dist/git-manager.d.ts +38 -3
  156. package/dist/git-manager.d.ts.map +1 -1
  157. package/dist/git-manager.js +141 -33
  158. package/dist/git-manager.js.map +1 -1
  159. package/dist/github-service.d.ts +13 -3
  160. package/dist/github-service.d.ts.map +1 -1
  161. package/dist/github-service.js +23 -7
  162. package/dist/github-service.js.map +1 -1
  163. package/dist/http/handler.d.ts +1 -1
  164. package/dist/http/handler.d.ts.map +1 -1
  165. package/dist/http/handler.js +36 -7
  166. package/dist/http/handler.js.map +1 -1
  167. package/dist/http/router.d.ts +11 -0
  168. package/dist/http/router.d.ts.map +1 -1
  169. package/dist/http/router.js +80 -16
  170. package/dist/http/router.js.map +1 -1
  171. package/dist/paths/validation.d.ts.map +1 -1
  172. package/dist/paths/validation.js +12 -0
  173. package/dist/paths/validation.js.map +1 -1
  174. package/dist/schema/schema-store.d.ts +9 -0
  175. package/dist/schema/schema-store.d.ts.map +1 -1
  176. package/dist/schema/schema-store.js +57 -8
  177. package/dist/schema/schema-store.js.map +1 -1
  178. package/dist/services.d.ts +1 -0
  179. package/dist/services.d.ts.map +1 -1
  180. package/dist/services.js +5 -2
  181. package/dist/services.js.map +1 -1
  182. package/dist/sync-core.d.ts.map +1 -1
  183. package/dist/sync-core.js +22 -9
  184. package/dist/sync-core.js.map +1 -1
  185. package/dist/user.d.ts +0 -18
  186. package/dist/user.d.ts.map +1 -1
  187. package/dist/user.js +7 -22
  188. package/dist/user.js.map +1 -1
  189. package/dist/utils/error.d.ts +8 -0
  190. package/dist/utils/error.d.ts.map +1 -1
  191. package/dist/utils/error.js +35 -7
  192. package/dist/utils/error.js.map +1 -1
  193. package/dist/validation/deletion-checker.d.ts.map +1 -1
  194. package/dist/validation/deletion-checker.js +6 -5
  195. package/dist/validation/deletion-checker.js.map +1 -1
  196. package/dist/validation/entry-validator.d.ts +67 -0
  197. package/dist/validation/entry-validator.d.ts.map +1 -0
  198. package/dist/validation/entry-validator.js +277 -0
  199. package/dist/validation/entry-validator.js.map +1 -0
  200. package/dist/validation/field-traversal.d.ts +23 -1
  201. package/dist/validation/field-traversal.d.ts.map +1 -1
  202. package/dist/validation/field-traversal.js +29 -9
  203. package/dist/validation/field-traversal.js.map +1 -1
  204. package/dist/worker/cms-worker.d.ts +67 -6
  205. package/dist/worker/cms-worker.d.ts.map +1 -1
  206. package/dist/worker/cms-worker.js +190 -67
  207. package/dist/worker/cms-worker.js.map +1 -1
  208. package/package.json +1 -2
  209. package/dist/editor/components/EditorContext.d.ts +0 -35
  210. package/dist/editor/components/EditorContext.d.ts.map +0 -1
  211. package/dist/editor/components/EditorContext.js +0 -19
  212. package/dist/editor/components/EditorContext.js.map +0 -1
  213. package/dist/editor/env.d.ts +0 -5
  214. package/dist/editor/env.d.ts.map +0 -1
  215. package/dist/editor/env.js +0 -5
  216. package/dist/editor/env.js.map +0 -1
@@ -9,11 +9,11 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // dist/paths/normalize.js
12
- function normalizeFilesystemPath(path15) {
13
- return path15.split(/[\\/]+/).filter(Boolean).join("/");
12
+ function normalizeFilesystemPath(path17) {
13
+ return path17.split(/[\\/]+/).filter(Boolean).join("/");
14
14
  }
15
- function hasTraversalSequence(path15) {
16
- const normalized = normalizeFilesystemPath(path15);
15
+ function hasTraversalSequence(path17) {
16
+ const normalized = normalizeFilesystemPath(path17);
17
17
  return normalized.includes("..");
18
18
  }
19
19
  function createLogicalPath(...segments) {
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
23
23
  }
24
24
  return normalized;
25
25
  }
26
- function trimSlashes(path15) {
26
+ function trimSlashes(path17) {
27
27
  let start = 0;
28
- let end = path15.length;
29
- while (start < end && path15[start] === "/")
28
+ let end = path17.length;
29
+ while (start < end && path17[start] === "/")
30
30
  start++;
31
- while (end > start && path15[end - 1] === "/")
31
+ while (end > start && path17[end - 1] === "/")
32
32
  end--;
33
- return path15.slice(start, end);
33
+ return path17.slice(start, end);
34
34
  }
35
35
  var init_normalize = __esm({
36
36
  "dist/paths/normalize.js"() {
@@ -284,7 +284,7 @@ var mediaSchema;
284
284
  var init_media = __esm({
285
285
  "dist/config/schemas/media.js"() {
286
286
  "use strict";
287
- mediaSchema = z3.union([
287
+ mediaSchema = z3.discriminatedUnion("adapter", [
288
288
  z3.object({
289
289
  adapter: z3.literal("local"),
290
290
  publicBaseUrl: z3.string().url().optional()
@@ -298,10 +298,6 @@ var init_media = __esm({
298
298
  z3.object({
299
299
  adapter: z3.literal("lfs"),
300
300
  publicBaseUrl: z3.string().url().optional()
301
- }),
302
- z3.object({
303
- adapter: z3.string().min(1),
304
- publicBaseUrl: z3.string().url().optional()
305
301
  })
306
302
  ]);
307
303
  }
@@ -345,8 +341,12 @@ var init_config = __esm({
345
341
  });
346
342
  CanopyConfigSchema = z4.object({
347
343
  media: mediaSchema.optional(),
348
- defaultBranchAccess: defaultBranchAccessSchema.optional(),
349
- defaultPathAccess: defaultPathAccessSchema.optional(),
344
+ // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
345
+ // have .default('deny'), so the field is optional on input but always resolves to
346
+ // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
347
+ // before the inner default runs, defeating the fail-closed default (SCH-M1).
348
+ defaultBranchAccess: defaultBranchAccessSchema,
349
+ defaultPathAccess: defaultPathAccessSchema,
350
350
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
351
351
  // unset must stay undefined after parsing so dev mode can detect the fork
352
352
  // point from git HEAD (see resolveBaseBranch in utils/git.ts).
@@ -541,7 +541,7 @@ var init_config2 = __esm({
541
541
  });
542
542
 
543
543
  // dist/operating-mode/client-unsafe-strategy.js
544
- import path3 from "node:path";
544
+ import path5 from "node:path";
545
545
  function operatingStrategy(mode) {
546
546
  const cached = strategyCache.get(mode);
547
547
  if (cached)
@@ -578,32 +578,32 @@ var init_client_unsafe_strategy = __esm({
578
578
  // - shouldCommit(), shouldPush()
579
579
  // Add client-unsafe methods (use Node.js APIs)
580
580
  getWorkspaceRoot(_sourceRoot) {
581
- return path3.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
581
+ return path5.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
582
582
  }
583
583
  getContentRoot(sourceRoot) {
584
- return path3.resolve(sourceRoot ?? process.cwd(), "content");
584
+ return path5.resolve(sourceRoot ?? process.cwd(), "content");
585
585
  }
586
586
  getContentBranchesRoot(sourceRoot) {
587
- return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
587
+ return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
588
588
  }
589
589
  getContentBranchRoot(branchName, sourceRoot) {
590
- return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
590
+ return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
591
591
  }
592
592
  getGitExcludePattern() {
593
593
  return ".canopy-meta/";
594
594
  }
595
595
  getPermissionsFilePath(root) {
596
- return path3.join(root, this.getPermissionsFileName());
596
+ return path5.join(root, this.getPermissionsFileName());
597
597
  }
598
598
  getGroupsFilePath(root) {
599
- return path3.join(root, this.getGroupsFileName());
599
+ return path5.join(root, this.getGroupsFileName());
600
600
  }
601
601
  getRemoteUrlConfig() {
602
602
  return {
603
603
  shouldAutoInitLocal: false,
604
604
  defaultRemotePath: "",
605
605
  envVarName: "CANOPYCMS_REMOTE_URL",
606
- autoDetectRemotePath: path3.join(this.getWorkspaceRoot(), "remote.git")
606
+ autoDetectRemotePath: path5.join(this.getWorkspaceRoot(), "remote.git")
607
607
  };
608
608
  }
609
609
  requiresExistingRepo() {
@@ -616,7 +616,7 @@ var init_client_unsafe_strategy = __esm({
616
616
  return `canopycms-settings-${deploymentName}`;
617
617
  }
618
618
  getSettingsRoot(sourceRoot) {
619
- return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
619
+ return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
620
620
  }
621
621
  usesSeparateSettingsBranch() {
622
622
  return true;
@@ -633,25 +633,25 @@ var init_client_unsafe_strategy = __esm({
633
633
  DevStrategy = class extends DevClientSafeStrategy {
634
634
  // Inherits client-safe methods from DevClientSafeStrategy
635
635
  getWorkspaceRoot(sourceRoot) {
636
- return path3.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
636
+ return path5.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
637
637
  }
638
638
  getContentRoot(sourceRoot) {
639
- return path3.resolve(sourceRoot ?? process.cwd(), "content");
639
+ return path5.resolve(sourceRoot ?? process.cwd(), "content");
640
640
  }
641
641
  getContentBranchesRoot(sourceRoot) {
642
- return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
642
+ return path5.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
643
643
  }
644
644
  getContentBranchRoot(branchName, sourceRoot) {
645
- return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
645
+ return path5.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
646
646
  }
647
647
  getGitExcludePattern() {
648
648
  return ".canopy-meta/";
649
649
  }
650
650
  getPermissionsFilePath(root) {
651
- return path3.join(root, this.getPermissionsFileName());
651
+ return path5.join(root, this.getPermissionsFileName());
652
652
  }
653
653
  getGroupsFilePath(root) {
654
- return path3.join(root, this.getGroupsFileName());
654
+ return path5.join(root, this.getGroupsFileName());
655
655
  }
656
656
  getRemoteUrlConfig() {
657
657
  return {
@@ -670,7 +670,7 @@ var init_client_unsafe_strategy = __esm({
670
670
  return `canopycms-settings-${deploymentName}`;
671
671
  }
672
672
  getSettingsRoot(sourceRoot) {
673
- return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
673
+ return path5.join(this.getWorkspaceRoot(sourceRoot), "settings");
674
674
  }
675
675
  usesSeparateSettingsBranch() {
676
676
  return true;
@@ -702,16 +702,16 @@ var init_operating_mode = __esm({
702
702
  });
703
703
 
704
704
  // dist/cli/generate-ai-content.js
705
- import path14 from "node:path";
705
+ import path16 from "node:path";
706
706
  import { createJiti } from "jiti";
707
707
 
708
708
  // dist/build/generate-ai-content.js
709
- import fs11 from "node:fs/promises";
710
- import path13 from "node:path";
709
+ import fs14 from "node:fs/promises";
710
+ import path15 from "node:path";
711
711
 
712
712
  // dist/content-store.js
713
- import fs4 from "node:fs/promises";
714
- import path5 from "node:path";
713
+ import fs6 from "node:fs/promises";
714
+ import path7 from "node:path";
715
715
  import matter from "gray-matter";
716
716
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
717
717
 
@@ -1076,13 +1076,13 @@ function extractIdFromFilename(filename) {
1076
1076
  return null;
1077
1077
  }
1078
1078
  async function resolveCollectionPath(root, logicalPath) {
1079
- const fs12 = await import("node:fs/promises");
1080
- const path15 = await import("node:path");
1079
+ const fs15 = await import("node:fs/promises");
1080
+ const path17 = await import("node:path");
1081
1081
  const segments = logicalPath.split("/").filter(Boolean);
1082
1082
  let currentPath = root;
1083
1083
  for (const segment of segments) {
1084
1084
  try {
1085
- const entries = await fs12.readdir(currentPath, { withFileTypes: true });
1085
+ const entries = await fs15.readdir(currentPath, { withFileTypes: true });
1086
1086
  const matchingDir = entries.find((entry) => {
1087
1087
  if (!entry.isDirectory())
1088
1088
  return false;
@@ -1090,7 +1090,7 @@ async function resolveCollectionPath(root, logicalPath) {
1090
1090
  return logicalName === segment.toLowerCase();
1091
1091
  });
1092
1092
  if (matchingDir) {
1093
- currentPath = path15.join(currentPath, matchingDir.name);
1093
+ currentPath = path17.join(currentPath, matchingDir.name);
1094
1094
  } else {
1095
1095
  return null;
1096
1096
  }
@@ -1140,6 +1140,192 @@ function extractSlugFromFilename(filename, entryTypeName) {
1140
1140
  return filename.toLowerCase();
1141
1141
  }
1142
1142
 
1143
+ // dist/content-index-registry.js
1144
+ import path3 from "node:path";
1145
+ var registry = /* @__PURE__ */ new Map();
1146
+ var finalization = new FinalizationRegistry(({ rootKey, ref }) => {
1147
+ const refs = registry.get(rootKey);
1148
+ if (!refs)
1149
+ return;
1150
+ refs.delete(ref);
1151
+ if (refs.size === 0)
1152
+ registry.delete(rootKey);
1153
+ });
1154
+ function registerContentIndexForInvalidation(root, target) {
1155
+ const rootKey = path3.resolve(root);
1156
+ let refs = registry.get(rootKey);
1157
+ if (!refs) {
1158
+ refs = /* @__PURE__ */ new Set();
1159
+ registry.set(rootKey, refs);
1160
+ }
1161
+ const ref = new WeakRef(target);
1162
+ refs.add(ref);
1163
+ finalization.register(target, { rootKey, ref });
1164
+ }
1165
+ function invalidateContentIndexesForRoot(root) {
1166
+ const rootKey = path3.resolve(root);
1167
+ const prefix = rootKey + path3.sep;
1168
+ for (const [storeRoot, refs] of registry) {
1169
+ if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
1170
+ continue;
1171
+ for (const ref of refs) {
1172
+ const target = ref.deref();
1173
+ if (target) {
1174
+ target.invalidateIndex();
1175
+ } else {
1176
+ refs.delete(ref);
1177
+ }
1178
+ }
1179
+ if (refs.size === 0)
1180
+ registry.delete(storeRoot);
1181
+ }
1182
+ }
1183
+
1184
+ // dist/content-index-generation.js
1185
+ import fs3 from "node:fs/promises";
1186
+ import path4 from "node:path";
1187
+ import { randomUUID } from "node:crypto";
1188
+
1189
+ // dist/utils/debug.js
1190
+ var LOG_LEVELS = {
1191
+ DEBUG: 0,
1192
+ INFO: 1,
1193
+ WARN: 2,
1194
+ ERROR: 3
1195
+ };
1196
+ var DebugLogger = class {
1197
+ constructor(options = {}) {
1198
+ this.timers = /* @__PURE__ */ new Map();
1199
+ this.options = options;
1200
+ }
1201
+ shouldLog(level) {
1202
+ const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
1203
+ if (!enabled)
1204
+ return false;
1205
+ const minLevel = this.options.minLevel ?? "DEBUG";
1206
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
1207
+ }
1208
+ formatMessage(level, category, message) {
1209
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1210
+ const prefix = this.options.prefix ?? "CanopyCMS";
1211
+ return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
1212
+ }
1213
+ debug(category, message, data) {
1214
+ if (this.shouldLog("DEBUG")) {
1215
+ console.log(this.formatMessage("DEBUG", category, message), data ?? "");
1216
+ }
1217
+ }
1218
+ info(category, message, data) {
1219
+ if (this.shouldLog("INFO")) {
1220
+ console.log(this.formatMessage("INFO", category, message), data ?? "");
1221
+ }
1222
+ }
1223
+ warn(category, message, data) {
1224
+ if (this.shouldLog("WARN")) {
1225
+ console.warn(this.formatMessage("WARN", category, message), data ?? "");
1226
+ }
1227
+ }
1228
+ error(category, message, data) {
1229
+ const msg = this.formatMessage("ERROR", category, message);
1230
+ if (this.shouldLog("ERROR")) {
1231
+ console.error(msg, data ?? "");
1232
+ }
1233
+ const throwOnError = this.options.throwOnError ?? false;
1234
+ if (throwOnError) {
1235
+ const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
1236
+ throw new Error(errorMsg);
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Start timing an operation
1241
+ */
1242
+ time(label) {
1243
+ this.timers.set(label, Date.now());
1244
+ }
1245
+ /**
1246
+ * End timing an operation and log the duration
1247
+ */
1248
+ timeEnd(category, label) {
1249
+ const start = this.timers.get(label);
1250
+ if (start === void 0) {
1251
+ this.warn(category, `Timer '${label}' does not exist`);
1252
+ return;
1253
+ }
1254
+ const duration = Date.now() - start;
1255
+ this.timers.delete(label);
1256
+ this.debug(category, `${label} completed`, { durationMs: duration });
1257
+ return duration;
1258
+ }
1259
+ /**
1260
+ * Wrap an async function with automatic timing
1261
+ */
1262
+ async timed(category, label, fn) {
1263
+ this.time(label);
1264
+ try {
1265
+ return await fn();
1266
+ } finally {
1267
+ this.timeEnd(category, label);
1268
+ }
1269
+ }
1270
+ };
1271
+ function createDebugLogger(options) {
1272
+ return new DebugLogger(options);
1273
+ }
1274
+ var testLogger = createDebugLogger({
1275
+ enabled: process.env.E2E_DEBUG === "true",
1276
+ prefix: "E2E",
1277
+ throwOnError: false
1278
+ });
1279
+
1280
+ // dist/content-index-generation.js
1281
+ var log = createDebugLogger({ prefix: "ContentIndex" });
1282
+ var META_DIR = ".canopy-meta";
1283
+ var GENERATION_FILE = "content-index.generation";
1284
+ function contentIndexGenerationPath(root) {
1285
+ return path4.join(path4.resolve(root), META_DIR, GENERATION_FILE);
1286
+ }
1287
+ async function bumpContentIndexGeneration(root) {
1288
+ const token = randomUUID();
1289
+ try {
1290
+ await atomicWriteFile(contentIndexGenerationPath(root), token);
1291
+ return token;
1292
+ } catch (err) {
1293
+ log.warn("generation", `Failed to bump content index generation for ${root}`, {
1294
+ error: getErrorMessage(err)
1295
+ });
1296
+ return null;
1297
+ }
1298
+ }
1299
+ async function readContentIndexGeneration(root) {
1300
+ try {
1301
+ return await fs3.readFile(contentIndexGenerationPath(root), "utf-8");
1302
+ } catch (err) {
1303
+ if (isNodeError(err) && err.code === "ENOENT")
1304
+ return null;
1305
+ log.warn("generation", `Failed to read content index generation for ${root}`, {
1306
+ error: getErrorMessage(err)
1307
+ });
1308
+ return null;
1309
+ }
1310
+ }
1311
+ async function invalidateContentIndexesDurable(root) {
1312
+ await bumpContentIndexGeneration(root);
1313
+ invalidateContentIndexesForRoot(root);
1314
+ }
1315
+
1316
+ // dist/utils/fs.js
1317
+ import fs4 from "node:fs/promises";
1318
+ async function filePathExists(filePath) {
1319
+ try {
1320
+ await fs4.stat(filePath);
1321
+ return true;
1322
+ } catch (err) {
1323
+ if (isNotFoundError(err))
1324
+ return false;
1325
+ throw err;
1326
+ }
1327
+ }
1328
+
1143
1329
  // dist/utils/format.js
1144
1330
  var getFormatExtension = (format) => {
1145
1331
  if (format === "md")
@@ -1158,8 +1344,8 @@ init_normalize();
1158
1344
 
1159
1345
  // dist/paths/branch.js
1160
1346
  init_operating_mode();
1161
- import fs3 from "node:fs/promises";
1162
- import path4 from "node:path";
1347
+ import fs5 from "node:fs/promises";
1348
+ import path6 from "node:path";
1163
1349
  var BranchPathError = class extends Error {
1164
1350
  };
1165
1351
  function sanitizeBranchName(branchName) {
@@ -1178,11 +1364,11 @@ function resolveBranchPath(options) {
1178
1364
  const safeBranch = sanitizeBranchName(options.branchName);
1179
1365
  const strategy = operatingStrategy(options.mode);
1180
1366
  const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
1181
- const normalizedBase = path4.resolve(baseRoot);
1182
- const baseWithSep = normalizedBase.endsWith(path4.sep) ? normalizedBase : `${normalizedBase}${path4.sep}`;
1367
+ const normalizedBase = path6.resolve(baseRoot);
1368
+ const baseWithSep = normalizedBase.endsWith(path6.sep) ? normalizedBase : `${normalizedBase}${path6.sep}`;
1183
1369
  const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
1184
1370
  const withinBase = (target) => {
1185
- const resolved = path4.resolve(target);
1371
+ const resolved = path6.resolve(target);
1186
1372
  return resolved === normalizedBase || resolved.startsWith(baseWithSep);
1187
1373
  };
1188
1374
  if (!withinBase(branchRoot)) {
@@ -1192,7 +1378,7 @@ function resolveBranchPath(options) {
1192
1378
  }
1193
1379
  async function ensureBranchRoot(options) {
1194
1380
  const result = resolveBranchPath(options);
1195
- await fs3.mkdir(result.branchRoot, { recursive: true });
1381
+ await fs5.mkdir(result.branchRoot, { recursive: true });
1196
1382
  return result;
1197
1383
  }
1198
1384
 
@@ -1222,24 +1408,147 @@ function validateSlug(slug) {
1222
1408
  throw new ContentStoreError("Slugs cannot contain backslashes. Use nested collections instead.", "VALIDATION");
1223
1409
  }
1224
1410
  }
1411
+ var DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
1412
+ var FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
1413
+ var STALE_LOOKUP = Symbol("stale-index-lookup");
1225
1414
  var ContentStore = class {
1226
- constructor(root, flatSchema) {
1227
- this.indexLoaded = false;
1228
- this.root = path5.resolve(root);
1415
+ constructor(root, flatSchema, options = {}) {
1416
+ this.indexGeneration = 0;
1417
+ this.loadedIndexGeneration = -1;
1418
+ this.loadedDiskGeneration = void 0;
1419
+ this.lastDiskProbeMs = 0;
1420
+ this.lastForcedRefreshMs = 0;
1421
+ this.indexBuild = null;
1422
+ this.root = path7.resolve(root);
1423
+ this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
1229
1424
  this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
1230
1425
  this._idIndex = new ContentIdIndex(this.root);
1426
+ registerContentIndexForInvalidation(this.root, this);
1427
+ }
1428
+ /**
1429
+ * Mark the ID index stale so the next idIndex() access rebuilds it from disk.
1430
+ *
1431
+ * Called (via content-index-registry) after in-process operations that change the
1432
+ * working tree underneath this store — git checkout/merge/rebase, the worker's
1433
+ * rebase loop, and sync-core's content replacement. Without this, ID→path lookups
1434
+ * keep resolving to pre-mutation paths and saves can target moved/deleted files.
1435
+ *
1436
+ * Cheap: only bumps a generation counter; the rebuild happens lazily.
1437
+ */
1438
+ invalidateIndex() {
1439
+ this.indexGeneration++;
1231
1440
  }
1232
1441
  /**
1233
- * Get the ID index, ensuring it's loaded first.
1234
- * This getter automatically loads the index on first access.
1442
+ * Get the ID index, ensuring it's loaded and current first.
1443
+ * Loads lazily on first access and rebuilds after invalidateIndex(); repeated
1444
+ * accesses with no intervening invalidation reuse the already-built index,
1445
+ * except for a throttled probe of the on-disk generation marker that detects
1446
+ * mutations made by OTHER processes sharing this root (e.g. on EFS).
1235
1447
  */
1236
1448
  async idIndex() {
1237
- if (!this.indexLoaded) {
1238
- await this._idIndex.buildFromFilenames("content");
1239
- this.indexLoaded = true;
1449
+ if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
1450
+ const diskToken = await readContentIndexGeneration(this.root);
1451
+ if (diskToken !== this.loadedDiskGeneration) {
1452
+ this.indexGeneration++;
1453
+ }
1454
+ }
1455
+ while (this.loadedIndexGeneration !== this.indexGeneration) {
1456
+ if (this.indexBuild) {
1457
+ await this.indexBuild;
1458
+ continue;
1459
+ }
1460
+ const generation = this.indexGeneration;
1461
+ const build = (async () => {
1462
+ const diskToken = await readContentIndexGeneration(this.root);
1463
+ const fresh = new ContentIdIndex(this.root);
1464
+ await fresh.buildFromFilenames("content");
1465
+ return { diskToken, fresh };
1466
+ })();
1467
+ this.indexBuild = build;
1468
+ let result;
1469
+ try {
1470
+ result = await build;
1471
+ } finally {
1472
+ this.indexBuild = null;
1473
+ }
1474
+ this._idIndex = result.fresh;
1475
+ this.loadedIndexGeneration = generation;
1476
+ this.loadedDiskGeneration = result.diskToken;
1477
+ this.lastDiskProbeMs = Date.now();
1240
1478
  }
1241
1479
  return this._idIndex;
1242
1480
  }
1481
+ /**
1482
+ * Throttle for the on-disk marker probe. Stamps the clock synchronously
1483
+ * before the caller's readFile so concurrent idIndex() callers don't
1484
+ * double-probe (and double-rebuild) on the same token change.
1485
+ */
1486
+ shouldProbeDiskGeneration() {
1487
+ const now = Date.now();
1488
+ if (now - this.lastDiskProbeMs < this.indexFreshnessIntervalMs)
1489
+ return false;
1490
+ this.lastDiskProbeMs = now;
1491
+ return true;
1492
+ }
1493
+ /**
1494
+ * After one of this store's own successful mutations: publish the change to
1495
+ * other processes (bump the on-disk marker) and adopt the written token,
1496
+ * since the in-memory index was already updated incrementally — avoiding a
1497
+ * pointless self-rescan on the next probe. Adoption is skipped unless the
1498
+ * index is quiescent and `updatedIndex` is still the live instance: if a
1499
+ * rebuild raced with the mutation, its scan may predate our file change, so
1500
+ * we leave the recorded token older and let the next probe observe our bump
1501
+ * and trigger the healing rebuild.
1502
+ */
1503
+ async recordOwnMutation(updatedIndex) {
1504
+ const token = await bumpContentIndexGeneration(this.root);
1505
+ if (token !== null && this.indexBuild === null && this.loadedIndexGeneration === this.indexGeneration && this._idIndex === updatedIndex) {
1506
+ this.loadedDiskGeneration = token;
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Backstop for the residual staleness windows of the cross-process marker
1511
+ * (NFS attribute caching, probe throttle, self-adoption — see
1512
+ * content-index-generation.ts): force one rebuild in response to a
1513
+ * suspicious lookup (an ID miss, or an index hit whose file is gone), but
1514
+ * throttle how often a caller can force one. Time-boxed so genuinely
1515
+ * dangling IDs cost at most one rescan per window. Returns true if a
1516
+ * refresh was performed by THIS call; callers that lose the throttle race
1517
+ * still benefit — idIndex() dedupes concurrent builds, so a caller that won
1518
+ * the race rebuilds the index for everyone, and callers should still retry
1519
+ * their lookup against the live index regardless of this return value.
1520
+ */
1521
+ async refreshIndexForSuspiciousLookup() {
1522
+ const now = Date.now();
1523
+ if (now - this.lastForcedRefreshMs < FORCED_REFRESH_MIN_INTERVAL_MS)
1524
+ return false;
1525
+ this.lastForcedRefreshMs = now;
1526
+ this.invalidateIndex();
1527
+ await this.idIndex();
1528
+ return true;
1529
+ }
1530
+ /**
1531
+ * Find the file in `dir` whose filename embeds `id`.
1532
+ * Returns its root-relative path, or null if no such file (or no such dir).
1533
+ */
1534
+ async findEntryPathById(dir, id) {
1535
+ let entries;
1536
+ try {
1537
+ entries = await fs6.readdir(dir, { withFileTypes: true });
1538
+ } catch (err) {
1539
+ if (isNodeError(err) && err.code === "ENOENT")
1540
+ return null;
1541
+ throw err;
1542
+ }
1543
+ for (const entry of entries) {
1544
+ if (entry.isDirectory())
1545
+ continue;
1546
+ if (extractIdFromFilename(entry.name) === id) {
1547
+ return path7.relative(this.root, path7.join(dir, entry.name));
1548
+ }
1549
+ }
1550
+ return null;
1551
+ }
1243
1552
  /**
1244
1553
  * Get all schema items for iteration.
1245
1554
  * Used internally by ReferenceResolver for path matching.
@@ -1247,11 +1556,11 @@ var ContentStore = class {
1247
1556
  getSchemaItems() {
1248
1557
  return this.schemaIndex.values();
1249
1558
  }
1250
- assertSchemaItem(path15) {
1251
- const normalized = normalizeFilesystemPath(path15);
1559
+ assertSchemaItem(path17) {
1560
+ const normalized = normalizeFilesystemPath(path17);
1252
1561
  const item = this.schemaIndex.get(normalized);
1253
1562
  if (!item) {
1254
- throw new ContentStoreError(`Unknown schema item: ${path15}`, "NO_SCHEMA_ITEM");
1563
+ throw new ContentStoreError(`Unknown schema item: ${path17}`, "NO_SCHEMA_ITEM");
1255
1564
  }
1256
1565
  return item;
1257
1566
  }
@@ -1278,7 +1587,7 @@ var ContentStore = class {
1278
1587
  * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
1279
1588
  */
1280
1589
  async buildPaths(schemaItem, slug, options = {}) {
1281
- const rootWithSep = this.root.endsWith(path5.sep) ? this.root : `${this.root}${path5.sep}`;
1590
+ const rootWithSep = this.root.endsWith(path7.sep) ? this.root : `${this.root}${path7.sep}`;
1282
1591
  if (schemaItem.type === "entry-type") {
1283
1592
  const parentPath = schemaItem.parentPath || "";
1284
1593
  const parentCollection = this.schemaIndex.get(parentPath);
@@ -1311,7 +1620,7 @@ var ContentStore = class {
1311
1620
  const entryTypeName = entryTypeConfig?.name || "entry";
1312
1621
  let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
1313
1622
  if (!collectionRoot) {
1314
- collectionRoot = path5.resolve(this.root, schemaItem.logicalPath);
1623
+ collectionRoot = path7.resolve(this.root, schemaItem.logicalPath);
1315
1624
  }
1316
1625
  if (!collectionRoot.startsWith(rootWithSep)) {
1317
1626
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
@@ -1320,7 +1629,7 @@ var ContentStore = class {
1320
1629
  let existingFilename;
1321
1630
  let existingEntryType;
1322
1631
  if (!id) {
1323
- const entries = await fs4.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1632
+ const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1324
1633
  const existingFile = entries.find((entry) => {
1325
1634
  if (entry.isDirectory())
1326
1635
  return false;
@@ -1344,14 +1653,14 @@ var ContentStore = class {
1344
1653
  }
1345
1654
  filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
1346
1655
  }
1347
- const resolved = path5.resolve(collectionRoot, filename);
1348
- const collectionRootWithSep = collectionRoot.endsWith(path5.sep) ? collectionRoot : `${collectionRoot}${path5.sep}`;
1656
+ const resolved = path7.resolve(collectionRoot, filename);
1657
+ const collectionRootWithSep = collectionRoot.endsWith(path7.sep) ? collectionRoot : `${collectionRoot}${path7.sep}`;
1349
1658
  if (!resolved.startsWith(collectionRootWithSep)) {
1350
1659
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
1351
1660
  }
1352
1661
  return {
1353
1662
  absolutePath: resolved,
1354
- relativePath: path5.relative(this.root, resolved),
1663
+ relativePath: path7.relative(this.root, resolved),
1355
1664
  id,
1356
1665
  entryTypeName: finalEntryTypeName
1357
1666
  };
@@ -1386,8 +1695,8 @@ var ContentStore = class {
1386
1695
  async read(collectionPath, slug = "", options = {}) {
1387
1696
  const schemaItem = this.assertSchemaItem(collectionPath);
1388
1697
  const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
1389
- const stat = await fs4.stat(absolutePath);
1390
- const raw = await fs4.readFile(absolutePath, "utf8");
1698
+ const stat = await fs6.stat(absolutePath);
1699
+ const raw = await fs6.readFile(absolutePath, "utf8");
1391
1700
  let doc;
1392
1701
  let format;
1393
1702
  let fields;
@@ -1445,7 +1754,7 @@ var ContentStore = class {
1445
1754
  return doc;
1446
1755
  }
1447
1756
  async write(collectionPath, slug = "", input, entryTypeName, existingId) {
1448
- const idIndex = await this.idIndex();
1757
+ await this.idIndex();
1449
1758
  const schemaItem = this.assertSchemaItem(collectionPath);
1450
1759
  let expectedFormat;
1451
1760
  let fields = [];
@@ -1473,10 +1782,10 @@ var ContentStore = class {
1473
1782
  existingId
1474
1783
  });
1475
1784
  return withLock(absolutePath, async () => {
1476
- await fs4.mkdir(path5.dirname(absolutePath), { recursive: true });
1785
+ await fs6.mkdir(path7.dirname(absolutePath), { recursive: true });
1477
1786
  if (input.expectedVersion !== void 0) {
1478
1787
  try {
1479
- const existing = await fs4.stat(absolutePath);
1788
+ const existing = await fs6.stat(absolutePath);
1480
1789
  if (existing.mtimeMs !== input.expectedVersion) {
1481
1790
  throw new ContentConflictError();
1482
1791
  }
@@ -1489,6 +1798,13 @@ var ContentStore = class {
1489
1798
  }
1490
1799
  }
1491
1800
  }
1801
+ if (existingId && !await filePathExists(absolutePath)) {
1802
+ const actualRelPath = await this.findEntryPathById(path7.dirname(absolutePath), existingId);
1803
+ const indexedRelPath = (await this.idIndex()).findById(existingId)?.relativePath ?? null;
1804
+ if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
1805
+ throw new ContentConflictError();
1806
+ }
1807
+ }
1492
1808
  let content;
1493
1809
  if (input.format === "json") {
1494
1810
  content = `${JSON.stringify(input.data ?? {}, null, 2)}
@@ -1499,19 +1815,17 @@ var ContentStore = class {
1499
1815
  content = matter.stringify(input.body, input.data ?? {});
1500
1816
  }
1501
1817
  await atomicWriteFile(absolutePath, content);
1818
+ const liveIndex = this._idIndex;
1819
+ let staleOldAbsPath = null;
1502
1820
  if (id) {
1503
- const existing = idIndex.findById(id);
1821
+ const existing = liveIndex.findById(id);
1504
1822
  if (existing) {
1505
1823
  if (existing.relativePath !== relativePath) {
1506
- const oldAbsPath = path5.join(this.root, existing.relativePath);
1507
- await fs4.unlink(oldAbsPath).catch((err) => {
1508
- if (!isNodeError(err) || err.code !== "ENOENT")
1509
- throw err;
1510
- });
1511
- idIndex.updatePath(existing.id, relativePath);
1824
+ staleOldAbsPath = path7.join(this.root, existing.relativePath);
1825
+ liveIndex.updatePath(existing.id, relativePath);
1512
1826
  }
1513
1827
  } else {
1514
- idIndex.add({
1828
+ liveIndex.add({
1515
1829
  type: "entry",
1516
1830
  relativePath,
1517
1831
  collection: collectionPath,
@@ -1519,7 +1833,14 @@ var ContentStore = class {
1519
1833
  });
1520
1834
  }
1521
1835
  }
1522
- const afterStat = await fs4.stat(absolutePath);
1836
+ if (staleOldAbsPath) {
1837
+ await fs6.unlink(staleOldAbsPath).catch((err) => {
1838
+ if (!isNodeError(err) || err.code !== "ENOENT")
1839
+ throw err;
1840
+ });
1841
+ }
1842
+ await this.recordOwnMutation(liveIndex);
1843
+ const afterStat = await fs6.stat(absolutePath);
1523
1844
  const base = {
1524
1845
  collection: schemaItem.logicalPath,
1525
1846
  collectionName: schemaItem.name,
@@ -1545,13 +1866,34 @@ var ContentStore = class {
1545
1866
  /**
1546
1867
  * Read an entry by its ID (UUID).
1547
1868
  * Returns null if the ID doesn't exist or points to a collection.
1869
+ *
1870
+ * Suspicious results (ID missing from the index, or an index hit whose file
1871
+ * is gone) trigger one forced index refresh and a retry — self-healing for
1872
+ * mutations by other processes inside the marker's residual windows.
1548
1873
  */
1549
1874
  async readById(id) {
1875
+ const first = await this.readByIdOnce(id);
1876
+ if (first !== STALE_LOOKUP)
1877
+ return first;
1878
+ if (!await this.refreshIndexForSuspiciousLookup())
1879
+ return null;
1880
+ const second = await this.readByIdOnce(id);
1881
+ return second === STALE_LOOKUP ? null : second;
1882
+ }
1883
+ async readByIdOnce(id) {
1550
1884
  const idIndex = await this.idIndex();
1551
1885
  const location = idIndex.findById(id);
1552
- if (!location || location.type !== "entry")
1886
+ if (!location)
1887
+ return STALE_LOOKUP;
1888
+ if (location.type !== "entry")
1553
1889
  return null;
1554
- return this.read(location.collection, location.slug);
1890
+ try {
1891
+ return await this.read(location.collection, location.slug);
1892
+ } catch (err) {
1893
+ if (isNodeError(err) && err.code === "ENOENT")
1894
+ return STALE_LOOKUP;
1895
+ throw err;
1896
+ }
1555
1897
  }
1556
1898
  /**
1557
1899
  * Get the ID for an entry given its collection and slug.
@@ -1562,19 +1904,72 @@ var ContentStore = class {
1562
1904
  const { relativePath } = await this.buildPaths(this.assertCollection(collectionPath), slug);
1563
1905
  return idIndex.findByPath(relativePath);
1564
1906
  }
1907
+ /**
1908
+ * Check whether a document already exists on disk for this collection+slug.
1909
+ * Used by the write boundary to distinguish creates from edits (create
1910
+ * scaffolds and maxItems enforcement).
1911
+ */
1912
+ async documentExists(collectionPath, slug = "") {
1913
+ const schemaItem = this.assertSchemaItem(collectionPath);
1914
+ const { absolutePath } = await this.buildPaths(schemaItem, slug);
1915
+ try {
1916
+ await fs6.access(absolutePath);
1917
+ return true;
1918
+ } catch (err) {
1919
+ if (isNodeError(err) && err.code === "ENOENT")
1920
+ return false;
1921
+ throw err;
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Resolve the actual on-disk entry type of an existing collection entry, by
1926
+ * inspecting its filename directly. Entry filenames embed their type
1927
+ * (`{type}.{slug}.{id}.{ext}`) and it is immutable after creation — see the
1928
+ * existing-file lookup in buildPaths(), which this mirrors. Returns
1929
+ * undefined if no entry exists yet at this slug.
1930
+ *
1931
+ * Used at the API write boundary (api/content.ts) to validate an existing
1932
+ * entry's payload against ITS real schema rather than a caller-supplied (or
1933
+ * omitted/spoofed) `entryType` param — ContentStore.write() preserves the
1934
+ * on-disk type regardless of what's requested, so validation must agree
1935
+ * with what will actually be written (post-review M2).
1936
+ *
1937
+ * Cheap: one buildPaths() resolution plus a stat, no file content is read —
1938
+ * same cost class as documentExists().
1939
+ */
1940
+ async getExistingEntryType(collectionPath, slug = "") {
1941
+ const schemaItem = this.assertCollection(collectionPath);
1942
+ const { absolutePath, entryTypeName } = await this.buildPaths(schemaItem, slug);
1943
+ return await filePathExists(absolutePath) ? entryTypeName : void 0;
1944
+ }
1945
+ /**
1946
+ * Count existing entries of a given entry type in a collection, by filename
1947
+ * (entry filenames embed their type: `{type}.{slug}.{id}.{ext}`). Used to
1948
+ * enforce `EntryTypeConfig.maxItems` server-side at the create boundary.
1949
+ */
1950
+ async countEntriesOfType(collectionPath, entryTypeName) {
1951
+ this.assertCollection(collectionPath);
1952
+ const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
1953
+ if (!collectionRoot)
1954
+ return 0;
1955
+ const entries = await fs6.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1956
+ return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
1957
+ }
1565
1958
  /**
1566
1959
  * Delete an entry and remove it from the index.
1567
1960
  */
1568
1961
  async delete(collectionPath, slug) {
1569
- const idIndex = await this.idIndex();
1962
+ await this.idIndex();
1570
1963
  const collection = this.assertCollection(collectionPath);
1571
1964
  const { absolutePath, relativePath } = await this.buildPaths(collection, slug);
1572
1965
  return withLock(absolutePath, async () => {
1573
- const id = idIndex.findByPath(relativePath);
1574
- await fs4.unlink(absolutePath);
1966
+ await fs6.unlink(absolutePath);
1967
+ const liveIndex = this._idIndex;
1968
+ const id = liveIndex.findByPath(relativePath);
1575
1969
  if (id) {
1576
- idIndex.remove(id);
1970
+ liveIndex.remove(id);
1577
1971
  }
1972
+ await this.recordOwnMutation(liveIndex);
1578
1973
  });
1579
1974
  }
1580
1975
  /**
@@ -1588,7 +1983,7 @@ var ContentStore = class {
1588
1983
  * @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
1589
1984
  */
1590
1985
  async renameEntry(collectionPath, currentSlug, newSlug) {
1591
- const idIndex = await this.idIndex();
1986
+ await this.idIndex();
1592
1987
  const collection = this.assertCollection(collectionPath);
1593
1988
  validateSlug(newSlug);
1594
1989
  const safeNewSlug = newSlug.replace(/^\/+/, "");
@@ -1601,11 +1996,11 @@ var ContentStore = class {
1601
1996
  const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
1602
1997
  return withLock(currentPath, async () => {
1603
1998
  try {
1604
- await fs4.access(currentPath);
1999
+ await fs6.access(currentPath);
1605
2000
  } catch {
1606
2001
  throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
1607
2002
  }
1608
- const currentFilename = path5.basename(currentPath);
2003
+ const currentFilename = path7.basename(currentPath);
1609
2004
  const parts = currentFilename.split(".");
1610
2005
  if (parts.length < 4) {
1611
2006
  throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
@@ -1614,10 +2009,10 @@ var ContentStore = class {
1614
2009
  const contentId = parts[parts.length - 2];
1615
2010
  const ext = `.${parts[parts.length - 1]}`;
1616
2011
  const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
1617
- const parentDir = path5.dirname(currentPath);
1618
- const newPath = path5.join(parentDir, newFilename);
2012
+ const parentDir = path7.dirname(currentPath);
2013
+ const newPath = path7.join(parentDir, newFilename);
1619
2014
  try {
1620
- const entries = await fs4.readdir(parentDir, { withFileTypes: true });
2015
+ const entries = await fs6.readdir(parentDir, { withFileTypes: true });
1621
2016
  for (const entry of entries) {
1622
2017
  if (entry.isDirectory())
1623
2018
  continue;
@@ -1631,19 +2026,21 @@ var ContentStore = class {
1631
2026
  throw err;
1632
2027
  }
1633
2028
  try {
1634
- await fs4.link(currentPath, newPath);
2029
+ await fs6.link(currentPath, newPath);
1635
2030
  } catch (err) {
1636
2031
  if (isNodeError(err) && err.code === "EEXIST") {
1637
2032
  throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
1638
2033
  }
1639
2034
  throw err;
1640
2035
  }
1641
- await fs4.unlink(currentPath);
1642
- const newRelativePath = path5.relative(this.root, newPath);
1643
- const entryId = idIndex.findByPath(currentRelPath);
2036
+ await fs6.unlink(currentPath);
2037
+ const newRelativePath = path7.relative(this.root, newPath);
2038
+ const liveIndex = this._idIndex;
2039
+ const entryId = liveIndex.findByPath(currentRelPath);
1644
2040
  if (entryId) {
1645
- idIndex.updatePath(entryId, newRelativePath);
2041
+ liveIndex.updatePath(entryId, newRelativePath);
1646
2042
  }
2043
+ await this.recordOwnMutation(liveIndex);
1647
2044
  return { newPath: `${collectionPath}/${safeNewSlug}` };
1648
2045
  });
1649
2046
  }
@@ -1737,11 +2134,32 @@ var ContentStore = class {
1737
2134
  * Resolve a single reference ID to full entry data.
1738
2135
  * Returns null if the reference is invalid or missing.
1739
2136
  * Includes id, slug, and collection fields for debugging.
2137
+ *
2138
+ * Suspicious results (ID missing from the index, or an index hit whose file
2139
+ * is gone) trigger one forced index refresh and a retry — self-healing for
2140
+ * mutations by other processes inside the marker's residual windows. The
2141
+ * forced refresh itself is throttled (see refreshIndexForSuspiciousLookup),
2142
+ * but the retry against the live index always runs regardless of whether
2143
+ * this call won the throttle race: when a `list: true` reference array is
2144
+ * resolved via Promise.all, every miss shares the same stale snapshot, and
2145
+ * idIndex() dedupes concurrent builds — so a sibling call that wins the
2146
+ * throttle and rebuilds heals every other miss in the same batch, not just
2147
+ * the first.
1740
2148
  */
1741
2149
  async resolveSingleReference(id, idIndex) {
2150
+ const first = await this.resolveSingleReferenceOnce(id, idIndex);
2151
+ if (first !== STALE_LOOKUP)
2152
+ return first;
2153
+ await this.refreshIndexForSuspiciousLookup();
2154
+ const second = await this.resolveSingleReferenceOnce(id, await this.idIndex());
2155
+ return second === STALE_LOOKUP ? null : second;
2156
+ }
2157
+ async resolveSingleReferenceOnce(id, idIndex) {
1742
2158
  try {
1743
2159
  const location = idIndex.findById(id);
1744
- if (!location || location.type !== "entry" || !location.collection || !location.slug) {
2160
+ if (!location)
2161
+ return STALE_LOOKUP;
2162
+ if (location.type !== "entry" || !location.collection || !location.slug) {
1745
2163
  return null;
1746
2164
  }
1747
2165
  const doc = await this.read(location.collection, location.slug, {
@@ -1754,6 +2172,8 @@ var ContentStore = class {
1754
2172
  ...doc.data
1755
2173
  };
1756
2174
  } catch (error) {
2175
+ if (isNodeError(error) && error.code === "ENOENT")
2176
+ return STALE_LOOKUP;
1757
2177
  console.error(`Failed to resolve reference ${id}:`, error);
1758
2178
  return null;
1759
2179
  }
@@ -1761,11 +2181,11 @@ var ContentStore = class {
1761
2181
  };
1762
2182
 
1763
2183
  // dist/branch-schema-cache.js
1764
- import fs6 from "node:fs/promises";
1765
- import path6 from "node:path";
2184
+ import fs8 from "node:fs/promises";
2185
+ import path8 from "node:path";
1766
2186
 
1767
2187
  // dist/schema/meta-loader.js
1768
- import { promises as fs5 } from "fs";
2188
+ import { promises as fs7 } from "fs";
1769
2189
  import { join as join2 } from "pathe";
1770
2190
  import { z as z6 } from "zod";
1771
2191
  import chokidar from "chokidar";
@@ -1801,7 +2221,7 @@ function stripEmbeddedIdFromName(name) {
1801
2221
  async function scanForCollectionMeta(baseDir, relativePath = "") {
1802
2222
  const collections = [];
1803
2223
  try {
1804
- const entries = await fs5.readdir(baseDir, { withFileTypes: true });
2224
+ const entries = await fs7.readdir(baseDir, { withFileTypes: true });
1805
2225
  for (const entry of entries) {
1806
2226
  if (!entry.isDirectory())
1807
2227
  continue;
@@ -1812,8 +2232,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
1812
2232
  const absolutePath = join2(baseDir, folderName);
1813
2233
  const metaPath = join2(absolutePath, ".collection.json");
1814
2234
  try {
1815
- await fs5.access(metaPath);
1816
- const content = await fs5.readFile(metaPath, "utf-8");
2235
+ await fs7.access(metaPath);
2236
+ const content = await fs7.readFile(metaPath, "utf-8");
1817
2237
  const parsed = JSON.parse(content);
1818
2238
  const meta = collectionMetaSchema.parse(parsed);
1819
2239
  collections.push({
@@ -1845,7 +2265,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1845
2265
  let root = null;
1846
2266
  const rootMetaPath = join2(contentRoot, ".collection.json");
1847
2267
  try {
1848
- await fs5.access(rootMetaPath);
2268
+ await fs7.access(rootMetaPath);
1849
2269
  } catch (err) {
1850
2270
  if (err.code === "ENOENT") {
1851
2271
  } else {
@@ -1853,7 +2273,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1853
2273
  }
1854
2274
  }
1855
2275
  try {
1856
- const content = await fs5.readFile(rootMetaPath, "utf-8");
2276
+ const content = await fs7.readFile(rootMetaPath, "utf-8");
1857
2277
  const parsed = JSON.parse(content);
1858
2278
  root = rootCollectionMetaSchema.parse(parsed);
1859
2279
  } catch (err) {
@@ -1971,16 +2391,16 @@ var MTIME_CHECK_DEBOUNCE_MS = 1e3;
1971
2391
  async function isStaleByMtime(dir, cachedAt) {
1972
2392
  let entries;
1973
2393
  try {
1974
- entries = await fs6.readdir(dir, { recursive: true, encoding: "utf-8" });
2394
+ entries = await fs8.readdir(dir, { recursive: true, encoding: "utf-8" });
1975
2395
  } catch {
1976
2396
  return true;
1977
2397
  }
1978
2398
  for (const entry of entries) {
1979
2399
  if (!entry.endsWith(".collection.json"))
1980
2400
  continue;
1981
- const full = path6.join(dir, entry);
2401
+ const full = path8.join(dir, entry);
1982
2402
  try {
1983
- const stat = await fs6.stat(full);
2403
+ const stat = await fs8.stat(full);
1984
2404
  if (stat.mtimeMs > cachedAt.getTime())
1985
2405
  return true;
1986
2406
  } catch {
@@ -2004,7 +2424,7 @@ var BranchSchemaCache = class {
2004
2424
  * the project root, regardless of which entrypoint produced the cwd branchRoot.
2005
2425
  */
2006
2426
  skipDiskCache(branchRoot) {
2007
- return isBuildMode() || path6.resolve(branchRoot) === path6.resolve(process.cwd());
2427
+ return isBuildMode() || path8.resolve(branchRoot) === path8.resolve(process.cwd());
2008
2428
  }
2009
2429
  /**
2010
2430
  * Get schema for a branch (loads from cache or resolves fresh).
@@ -2021,17 +2441,17 @@ var BranchSchemaCache = class {
2021
2441
  * Load schema from cache or resolve fresh if cache is missing or stale.
2022
2442
  */
2023
2443
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
2024
- const contentRoot = path6.join(branchRoot, contentRootName);
2444
+ const contentRoot = path8.join(branchRoot, contentRootName);
2025
2445
  const skipDiskCache = this.skipDiskCache(branchRoot);
2026
2446
  if (!skipDiskCache) {
2027
- const cacheDir = path6.join(branchRoot, ".canopy-meta");
2028
- const cachePath = path6.join(cacheDir, "schema-cache.json");
2029
- const stalePath = path6.join(cacheDir, "schema-cache.stale");
2447
+ const cacheDir = path8.join(branchRoot, ".canopy-meta");
2448
+ const cachePath = path8.join(cacheDir, "schema-cache.json");
2449
+ const stalePath = path8.join(cacheDir, "schema-cache.stale");
2030
2450
  let cacheData = null;
2031
2451
  try {
2032
- const staleExists = await fs6.access(stalePath).then(() => true).catch(() => false);
2452
+ const staleExists = await fs8.access(stalePath).then(() => true).catch(() => false);
2033
2453
  if (!staleExists) {
2034
- const cacheContent = await fs6.readFile(cachePath, "utf-8");
2454
+ const cacheContent = await fs8.readFile(cachePath, "utf-8");
2035
2455
  cacheData = JSON.parse(cacheContent);
2036
2456
  }
2037
2457
  } catch {
@@ -2056,21 +2476,21 @@ var BranchSchemaCache = class {
2056
2476
  }
2057
2477
  const flatSchema = flattenSchema(result.schema, contentRootName);
2058
2478
  if (!skipDiskCache) {
2059
- const cacheDir = path6.join(branchRoot, ".canopy-meta");
2060
- const cachePath = path6.join(cacheDir, "schema-cache.json");
2061
- const stalePath = path6.join(cacheDir, "schema-cache.stale");
2062
- await fs6.mkdir(cacheDir, { recursive: true });
2479
+ const cacheDir = path8.join(branchRoot, ".canopy-meta");
2480
+ const cachePath = path8.join(cacheDir, "schema-cache.json");
2481
+ const stalePath = path8.join(cacheDir, "schema-cache.stale");
2482
+ await fs8.mkdir(cacheDir, { recursive: true });
2063
2483
  const newCache = {
2064
2484
  version: SCHEMA_CACHE_VERSION,
2065
2485
  schema: result.schema,
2066
2486
  flatSchema,
2067
2487
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
2068
2488
  };
2069
- const tmpPath = path6.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
2070
- await fs6.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
2071
- await fs6.rename(tmpPath, cachePath);
2489
+ const tmpPath = path8.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
2490
+ await fs8.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
2491
+ await fs8.rename(tmpPath, cachePath);
2072
2492
  try {
2073
- await fs6.unlink(stalePath);
2493
+ await fs8.unlink(stalePath);
2074
2494
  } catch {
2075
2495
  }
2076
2496
  }
@@ -2084,15 +2504,16 @@ var BranchSchemaCache = class {
2084
2504
  async invalidate(branchRoot) {
2085
2505
  if (this.skipDiskCache(branchRoot))
2086
2506
  return;
2087
- const cacheDir = path6.join(branchRoot, ".canopy-meta");
2088
- const stalePath = path6.join(cacheDir, "schema-cache.stale");
2089
- await fs6.mkdir(cacheDir, { recursive: true });
2090
- await fs6.writeFile(stalePath, "", "utf-8");
2507
+ const cacheDir = path8.join(branchRoot, ".canopy-meta");
2508
+ const stalePath = path8.join(cacheDir, "schema-cache.stale");
2509
+ await fs8.mkdir(cacheDir, { recursive: true });
2510
+ await fs8.writeFile(stalePath, "", "utf-8");
2091
2511
  }
2092
2512
  };
2093
2513
 
2094
2514
  // dist/ai/generate.js
2095
- import path7 from "node:path";
2515
+ import path9 from "node:path";
2516
+ import fs9 from "node:fs/promises";
2096
2517
  import { minimatch } from "minimatch";
2097
2518
 
2098
2519
  // dist/ai/strip-mdx.js
@@ -2279,6 +2700,10 @@ function entryToMarkdown(entry, config) {
2279
2700
  } else {
2280
2701
  parts.push(...renderJsonEntry(entry, config, skipFields));
2281
2702
  }
2703
+ if (entry.appendedSections) {
2704
+ parts.push(entry.appendedSections.trim());
2705
+ parts.push("");
2706
+ }
2282
2707
  return parts.join("\n");
2283
2708
  }
2284
2709
  function renderMarkdownEntry(entry, config, skipFields) {
@@ -2388,6 +2813,9 @@ ${String(value)}`;
2388
2813
  function renderListField(field, values, depth, label, heading, descriptionLine, entry, config) {
2389
2814
  if (values.length === 0)
2390
2815
  return "";
2816
+ if (field.type === "object" && isFlatObjectList(field)) {
2817
+ return renderObjectListTable(field, values, label, heading, descriptionLine);
2818
+ }
2391
2819
  const isComplex = field.type === "object" || field.type === "block";
2392
2820
  if (isComplex) {
2393
2821
  const items2 = values.map((item, i) => {
@@ -2418,6 +2846,59 @@ ${items2.join("\n\n")}`;
2418
2846
 
2419
2847
  ${items}`;
2420
2848
  }
2849
+ var FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
2850
+ "string",
2851
+ "number",
2852
+ "datetime",
2853
+ "boolean",
2854
+ "select",
2855
+ "reference",
2856
+ "image"
2857
+ ]);
2858
+ function isFlatObjectList(field) {
2859
+ if (field.type !== "object")
2860
+ return false;
2861
+ const subFields = field.fields;
2862
+ if (!subFields || subFields.length === 0)
2863
+ return false;
2864
+ return subFields.every((f) => !("list" in f && f.list) && FLAT_CELL_TYPES.has(f.type));
2865
+ }
2866
+ function renderObjectListTable(field, values, label, heading, descriptionLine) {
2867
+ const columns = field.fields;
2868
+ const headerRow = `| ${columns.map((f) => escapeTableCell(f.label || f.name)).join(" | ")} |`;
2869
+ const separatorRow = `| ${columns.map(() => "---").join(" | ")} |`;
2870
+ const dataRows = values.map((item) => {
2871
+ const record = typeof item === "object" && item !== null ? item : {};
2872
+ const cells = columns.map((f) => {
2873
+ const v = record[f.name];
2874
+ if (v === void 0 || v === null)
2875
+ return "";
2876
+ return escapeTableCell(formatCellValue(f, v));
2877
+ });
2878
+ return `| ${cells.join(" | ")} |`;
2879
+ });
2880
+ const table = [headerRow, separatorRow, ...dataRows].join("\n");
2881
+ return `${heading} ${label}${descriptionLine}
2882
+
2883
+ ${table}`;
2884
+ }
2885
+ function formatCellValue(field, value) {
2886
+ switch (field.type) {
2887
+ case "boolean":
2888
+ return value ? "Yes" : "No";
2889
+ case "reference":
2890
+ return formatReference(value);
2891
+ case "select":
2892
+ return Array.isArray(value) ? value.map((v) => resolveSelectLabel(field, v)).join(", ") : resolveSelectLabel(field, value);
2893
+ case "image":
2894
+ return `![](${String(value)})`;
2895
+ default:
2896
+ return String(value);
2897
+ }
2898
+ }
2899
+ function escapeTableCell(value) {
2900
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n+/g, " ").trim();
2901
+ }
2421
2902
  function renderSelectField(field, value, heading, label, descriptionLine) {
2422
2903
  if (Array.isArray(value)) {
2423
2904
  return `${heading} ${label}${descriptionLine}
@@ -2538,97 +3019,6 @@ function yamlValue(value) {
2538
3019
  return value;
2539
3020
  }
2540
3021
 
2541
- // dist/utils/debug.js
2542
- var LOG_LEVELS = {
2543
- DEBUG: 0,
2544
- INFO: 1,
2545
- WARN: 2,
2546
- ERROR: 3
2547
- };
2548
- var DebugLogger = class {
2549
- constructor(options = {}) {
2550
- this.timers = /* @__PURE__ */ new Map();
2551
- this.options = options;
2552
- }
2553
- shouldLog(level) {
2554
- const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
2555
- if (!enabled)
2556
- return false;
2557
- const minLevel = this.options.minLevel ?? "DEBUG";
2558
- return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
2559
- }
2560
- formatMessage(level, category, message) {
2561
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2562
- const prefix = this.options.prefix ?? "CanopyCMS";
2563
- return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
2564
- }
2565
- debug(category, message, data) {
2566
- if (this.shouldLog("DEBUG")) {
2567
- console.log(this.formatMessage("DEBUG", category, message), data ?? "");
2568
- }
2569
- }
2570
- info(category, message, data) {
2571
- if (this.shouldLog("INFO")) {
2572
- console.log(this.formatMessage("INFO", category, message), data ?? "");
2573
- }
2574
- }
2575
- warn(category, message, data) {
2576
- if (this.shouldLog("WARN")) {
2577
- console.warn(this.formatMessage("WARN", category, message), data ?? "");
2578
- }
2579
- }
2580
- error(category, message, data) {
2581
- const msg = this.formatMessage("ERROR", category, message);
2582
- if (this.shouldLog("ERROR")) {
2583
- console.error(msg, data ?? "");
2584
- }
2585
- const throwOnError = this.options.throwOnError ?? false;
2586
- if (throwOnError) {
2587
- const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
2588
- throw new Error(errorMsg);
2589
- }
2590
- }
2591
- /**
2592
- * Start timing an operation
2593
- */
2594
- time(label) {
2595
- this.timers.set(label, Date.now());
2596
- }
2597
- /**
2598
- * End timing an operation and log the duration
2599
- */
2600
- timeEnd(category, label) {
2601
- const start = this.timers.get(label);
2602
- if (start === void 0) {
2603
- this.warn(category, `Timer '${label}' does not exist`);
2604
- return;
2605
- }
2606
- const duration = Date.now() - start;
2607
- this.timers.delete(label);
2608
- this.debug(category, `${label} completed`, { durationMs: duration });
2609
- return duration;
2610
- }
2611
- /**
2612
- * Wrap an async function with automatic timing
2613
- */
2614
- async timed(category, label, fn) {
2615
- this.time(label);
2616
- try {
2617
- return await fn();
2618
- } finally {
2619
- this.timeEnd(category, label);
2620
- }
2621
- }
2622
- };
2623
- function createDebugLogger(options) {
2624
- return new DebugLogger(options);
2625
- }
2626
- var testLogger = createDebugLogger({
2627
- enabled: process.env.E2E_DEBUG === "true",
2628
- prefix: "E2E",
2629
- throwOnError: false
2630
- });
2631
-
2632
3022
  // dist/utils/entry-url.js
2633
3023
  init_normalize();
2634
3024
  function computeEntryUrl(collection, slug, contentRoot) {
@@ -2643,12 +3033,12 @@ function computeEntryUrl(collection, slug, contentRoot) {
2643
3033
  if (slug && slug !== "index") {
2644
3034
  segments.push(slug);
2645
3035
  }
2646
- const path15 = segments.length > 0 ? `/${segments.join("/")}` : "/";
2647
- return path15.toLowerCase();
3036
+ const path17 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3037
+ return path17.toLowerCase();
2648
3038
  }
2649
3039
 
2650
3040
  // dist/entry-link-resolver.js
2651
- var log = createDebugLogger({ prefix: "EntryLinks" });
3041
+ var log2 = createDebugLogger({ prefix: "EntryLinks" });
2652
3042
  var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
2653
3043
  var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
2654
3044
  function resolveEntryUrl(location, contentRoot) {
@@ -2662,7 +3052,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
2662
3052
  return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
2663
3053
  const location = idIndex.findById(id);
2664
3054
  if (!location || location.type !== "entry" || !location.collection || !location.slug) {
2665
- log.warn("resolve", `Entry link target not found: entry:${id}`);
3055
+ log2.warn("resolve", `Entry link target not found: entry:${id}`);
2666
3056
  return anchor ?? "#";
2667
3057
  }
2668
3058
  let url;
@@ -2819,7 +3209,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
2819
3209
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
2820
3210
  const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
2821
3211
  for (const listEntry of directEntries) {
2822
- const entryTypeName = extractEntryTypeFromFilename(path7.basename(listEntry.relativePath));
3212
+ const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
2823
3213
  if (!entryTypeName)
2824
3214
  continue;
2825
3215
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -2837,6 +3227,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
2837
3227
  }
2838
3228
  if (config?.exclude?.where?.(aiEntry))
2839
3229
  continue;
3230
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
2840
3231
  entries.push(aiEntry);
2841
3232
  const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
2842
3233
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -2887,7 +3278,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
2887
3278
  const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
2888
3279
  const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
2889
3280
  for (const listEntry of directEntries) {
2890
- const entryTypeName = extractEntryTypeFromFilename(path7.basename(listEntry.relativePath));
3281
+ const entryTypeName = extractEntryTypeFromFilename(path9.basename(listEntry.relativePath));
2891
3282
  if (!entryTypeName)
2892
3283
  continue;
2893
3284
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -2905,6 +3296,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
2905
3296
  }
2906
3297
  if (config?.exclude?.where?.(aiEntry))
2907
3298
  continue;
3299
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path9.basename(listEntry.relativePath)), config);
2908
3300
  entries.push(aiEntry);
2909
3301
  const entryFilePath = `${listEntry.slug}.md`;
2910
3302
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -2951,6 +3343,43 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
2951
3343
  fields: entryTypeConfig.schema
2952
3344
  };
2953
3345
  }
3346
+ function makeReadSibling(dir) {
3347
+ const resolvedDir = path9.resolve(dir);
3348
+ return async (name) => {
3349
+ if (!name || name.includes("/") || name.includes("\\") || path9.isAbsolute(name) || hasTraversalSequence(name)) {
3350
+ return null;
3351
+ }
3352
+ const abs = path9.resolve(resolvedDir, name);
3353
+ if (path9.dirname(abs) !== resolvedDir)
3354
+ return null;
3355
+ try {
3356
+ const stats = await fs9.lstat(abs);
3357
+ if (!stats.isFile())
3358
+ return null;
3359
+ return await fs9.readFile(abs, "utf8");
3360
+ } catch (err) {
3361
+ if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
3362
+ return null;
3363
+ throw err;
3364
+ }
3365
+ };
3366
+ }
3367
+ async function runEntryTransform(entry, absolutePath, contentId, config) {
3368
+ const transform = config?.entryTransforms?.[entry.entryType];
3369
+ if (!transform)
3370
+ return;
3371
+ const ctx = {
3372
+ contentId: contentId ?? "",
3373
+ readSibling: makeReadSibling(path9.dirname(absolutePath))
3374
+ };
3375
+ try {
3376
+ const appended = await transform(entry, ctx);
3377
+ if (appended)
3378
+ entry.appendedSections = appended;
3379
+ } catch (err) {
3380
+ console.warn(`AI content: entry transform for "${entry.slug}" (${entry.entryType}) failed:`, getErrorMessage(err));
3381
+ }
3382
+ }
2954
3383
  function matchesBundleFilter(entry, filter, contentRoot) {
2955
3384
  if (filter.collections) {
2956
3385
  const matches = filter.collections.some((pattern) => {
@@ -2978,33 +3407,33 @@ function matchesBundleFilter(entry, filter, contentRoot) {
2978
3407
  }
2979
3408
 
2980
3409
  // dist/branch-workspace.js
2981
- import path12 from "node:path";
3410
+ import path14 from "node:path";
2982
3411
 
2983
3412
  // dist/branch-metadata.js
2984
- import { randomUUID } from "node:crypto";
2985
- import fs8 from "node:fs/promises";
2986
- import path9 from "node:path";
3413
+ import { randomUUID as randomUUID2 } from "node:crypto";
3414
+ import fs11 from "node:fs/promises";
3415
+ import path11 from "node:path";
2987
3416
 
2988
3417
  // dist/branch-registry.js
2989
- import fs7 from "node:fs/promises";
2990
- import path8 from "node:path";
3418
+ import fs10 from "node:fs/promises";
3419
+ import path10 from "node:path";
2991
3420
  var REGISTRY_FILE = "branches.json";
2992
3421
  var REGISTRY_STALE_FILE = "branches.stale.json";
2993
3422
  var REGISTRY_TEMP_FILE = "branches.tmp.json";
2994
3423
  var REGISTRY_VERSION = 1;
2995
3424
  var BranchRegistry = class {
2996
3425
  constructor(root) {
2997
- this.root = path8.resolve(root);
2998
- this.registryPath = path8.join(this.root, REGISTRY_FILE);
2999
- this.stalePath = path8.join(this.root, REGISTRY_STALE_FILE);
3000
- this.tempPath = path8.join(this.root, REGISTRY_TEMP_FILE);
3426
+ this.root = path10.resolve(root);
3427
+ this.registryPath = path10.join(this.root, REGISTRY_FILE);
3428
+ this.stalePath = path10.join(this.root, REGISTRY_STALE_FILE);
3429
+ this.tempPath = path10.join(this.root, REGISTRY_TEMP_FILE);
3001
3430
  }
3002
3431
  /**
3003
3432
  * Returns all branches. Uses cache if fresh, regenerates if stale.
3004
3433
  */
3005
3434
  async list() {
3006
3435
  try {
3007
- const raw = await fs7.readFile(this.registryPath, "utf8");
3436
+ const raw = await fs10.readFile(this.registryPath, "utf8");
3008
3437
  const parsed = JSON.parse(raw);
3009
3438
  if (!parsed.version || !Array.isArray(parsed.branches)) {
3010
3439
  return await this.regenerate();
@@ -3030,7 +3459,7 @@ var BranchRegistry = class {
3030
3459
  */
3031
3460
  async invalidate() {
3032
3461
  try {
3033
- await fs7.rename(this.registryPath, this.stalePath);
3462
+ await fs10.rename(this.registryPath, this.stalePath);
3034
3463
  } catch (err) {
3035
3464
  if (!isNotFoundError(err)) {
3036
3465
  throw err;
@@ -3044,20 +3473,20 @@ var BranchRegistry = class {
3044
3473
  async regenerate() {
3045
3474
  const branches = await this.scanBranchDirectories();
3046
3475
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
3047
- await fs7.mkdir(this.root, { recursive: true });
3476
+ await fs10.mkdir(this.root, { recursive: true });
3048
3477
  const snapshot = {
3049
3478
  version: REGISTRY_VERSION,
3050
3479
  branches
3051
3480
  };
3052
- await fs7.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
3481
+ await fs10.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
3053
3482
  try {
3054
- await fs7.rename(uniqueTempPath, this.registryPath);
3483
+ await fs10.rename(uniqueTempPath, this.registryPath);
3055
3484
  } catch (err) {
3056
- await fs7.unlink(uniqueTempPath).catch(() => {
3485
+ await fs10.unlink(uniqueTempPath).catch(() => {
3057
3486
  });
3058
3487
  throw err;
3059
3488
  }
3060
- await fs7.unlink(this.stalePath).catch(() => {
3489
+ await fs10.unlink(this.stalePath).catch(() => {
3061
3490
  });
3062
3491
  return branches;
3063
3492
  }
@@ -3067,12 +3496,12 @@ var BranchRegistry = class {
3067
3496
  async scanBranchDirectories() {
3068
3497
  const branches = [];
3069
3498
  try {
3070
- const entries = await fs7.readdir(this.root, { withFileTypes: true });
3499
+ const entries = await fs10.readdir(this.root, { withFileTypes: true });
3071
3500
  for (const entry of entries) {
3072
3501
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
3073
3502
  continue;
3074
3503
  }
3075
- const branchRoot = path8.join(this.root, entry.name);
3504
+ const branchRoot = path10.join(this.root, entry.name);
3076
3505
  const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
3077
3506
  if (meta) {
3078
3507
  branches.push({
@@ -3104,8 +3533,8 @@ var BranchMetadataConflictError = class extends Error {
3104
3533
  };
3105
3534
  var BranchMetadataFileManager = class _BranchMetadataFileManager {
3106
3535
  constructor(branchRoot, baseRoot) {
3107
- this.branchRoot = path9.resolve(branchRoot);
3108
- this.filePath = path9.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
3536
+ this.branchRoot = path11.resolve(branchRoot);
3537
+ this.filePath = path11.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
3109
3538
  this.baseRoot = baseRoot;
3110
3539
  }
3111
3540
  /**
@@ -3113,9 +3542,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3113
3542
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
3114
3543
  */
3115
3544
  static async loadOnly(branchRoot) {
3116
- const filePath = path9.join(path9.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
3545
+ const filePath = path11.join(path11.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
3117
3546
  try {
3118
- const raw = await fs8.readFile(filePath, "utf8");
3547
+ const raw = await fs11.readFile(filePath, "utf8");
3119
3548
  return JSON.parse(raw);
3120
3549
  } catch (err) {
3121
3550
  if (isNotFoundError(err)) {
@@ -3133,7 +3562,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3133
3562
  }
3134
3563
  async load() {
3135
3564
  try {
3136
- const raw = await fs8.readFile(this.filePath, "utf8");
3565
+ const raw = await fs11.readFile(this.filePath, "utf8");
3137
3566
  const parsed = JSON.parse(raw);
3138
3567
  const version = parsed.version ?? 0;
3139
3568
  return { meta: parsed, version };
@@ -3150,38 +3579,38 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3150
3579
  */
3151
3580
  async write(meta, expectedVersion) {
3152
3581
  const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
3153
- const writeId = randomUUID();
3582
+ const writeId = randomUUID2();
3154
3583
  const payload = {
3155
3584
  ...meta,
3156
3585
  schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
3157
3586
  version: newVersion,
3158
3587
  writeId
3159
3588
  };
3160
- await fs8.mkdir(path9.dirname(this.filePath), { recursive: true });
3589
+ await fs11.mkdir(path11.dirname(this.filePath), { recursive: true });
3161
3590
  const content = JSON.stringify(payload, null, 2) + "\n";
3162
3591
  if (expectedVersion === null) {
3163
3592
  const tempPath2 = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3164
- await fs8.writeFile(tempPath2, content, "utf-8");
3593
+ await fs11.writeFile(tempPath2, content, "utf-8");
3165
3594
  try {
3166
- await fs8.link(tempPath2, this.filePath);
3595
+ await fs11.link(tempPath2, this.filePath);
3167
3596
  } catch (err) {
3168
- await fs8.unlink(tempPath2).catch(() => {
3597
+ await fs11.unlink(tempPath2).catch(() => {
3169
3598
  });
3170
3599
  if (isFileExistsError(err)) {
3171
3600
  throw new BranchMetadataConflictError();
3172
3601
  }
3173
3602
  throw err;
3174
3603
  }
3175
- await fs8.unlink(tempPath2).catch(() => {
3604
+ await fs11.unlink(tempPath2).catch(() => {
3176
3605
  });
3177
3606
  return { version: newVersion, writeId };
3178
3607
  }
3179
3608
  const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3180
- await fs8.writeFile(tempPath, content, "utf-8");
3609
+ await fs11.writeFile(tempPath, content, "utf-8");
3181
3610
  try {
3182
3611
  let currentVersion = null;
3183
3612
  try {
3184
- const current = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
3613
+ const current = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
3185
3614
  currentVersion = current.version ?? 0;
3186
3615
  } catch {
3187
3616
  currentVersion = null;
@@ -3189,13 +3618,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3189
3618
  if (currentVersion !== expectedVersion) {
3190
3619
  throw new BranchMetadataConflictError();
3191
3620
  }
3192
- await fs8.rename(tempPath, this.filePath);
3193
- const afterWrite = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
3621
+ await fs11.rename(tempPath, this.filePath);
3622
+ const afterWrite = JSON.parse(await fs11.readFile(this.filePath, "utf-8"));
3194
3623
  if (afterWrite.writeId !== writeId) {
3195
3624
  throw new BranchMetadataConflictError();
3196
3625
  }
3197
3626
  } catch (err) {
3198
- await fs8.unlink(tempPath).catch(() => {
3627
+ await fs11.unlink(tempPath).catch(() => {
3199
3628
  });
3200
3629
  throw err;
3201
3630
  }
@@ -3258,8 +3687,8 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
3258
3687
  * Invalidates the registry cache so next list() call regenerates from branch.json files.
3259
3688
  */
3260
3689
  async invalidateRegistry() {
3261
- const registry = new BranchRegistry(this.baseRoot);
3262
- await registry.invalidate();
3690
+ const registry2 = new BranchRegistry(this.baseRoot);
3691
+ await registry2.invalidate();
3263
3692
  }
3264
3693
  };
3265
3694
  var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
@@ -3286,8 +3715,8 @@ var loadBranchContext = async (options) => {
3286
3715
  init_operating_mode();
3287
3716
 
3288
3717
  // dist/git-manager.js
3289
- import fs10 from "node:fs/promises";
3290
- import path11 from "node:path";
3718
+ import fs13 from "node:fs/promises";
3719
+ import path13 from "node:path";
3291
3720
  import { simpleGit as simpleGit2 } from "simple-git";
3292
3721
 
3293
3722
  // dist/utils/git.js
@@ -3311,21 +3740,41 @@ async function resolveBaseBranch(options) {
3311
3740
  }
3312
3741
 
3313
3742
  // dist/utils/provisioning-lock.js
3314
- import fs9 from "node:fs/promises";
3315
- import path10 from "node:path";
3743
+ import fs12 from "node:fs/promises";
3744
+ import path12 from "node:path";
3316
3745
  import lockfile from "proper-lockfile";
3317
3746
  async function acquireProvisioningLock(lockTargetDir, lockName) {
3318
- await fs9.mkdir(lockTargetDir, { recursive: true });
3747
+ await fs12.mkdir(lockTargetDir, { recursive: true });
3319
3748
  return lockfile.lock(lockTargetDir, {
3320
- lockfilePath: path10.join(lockTargetDir, lockName),
3749
+ lockfilePath: path12.join(lockTargetDir, lockName),
3321
3750
  retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
3322
3751
  stale: 3e4
3323
3752
  });
3324
3753
  }
3325
3754
 
3326
3755
  // dist/git-manager.js
3327
- var log2 = createDebugLogger({ prefix: "GitManager" });
3756
+ var log3 = createDebugLogger({ prefix: "GitManager" });
3328
3757
  var remoteInitLocks = /* @__PURE__ */ new Map();
3758
+ async function ensureGitExcludePattern(repoPath, pattern) {
3759
+ const excludePath = path13.join(repoPath, ".git", "info", "exclude");
3760
+ await fs13.mkdir(path13.dirname(excludePath), { recursive: true });
3761
+ let content = "";
3762
+ try {
3763
+ content = await fs13.readFile(excludePath, "utf-8");
3764
+ } catch (err) {
3765
+ if (!isNotFoundError(err))
3766
+ throw err;
3767
+ }
3768
+ const lines = content.split("\n");
3769
+ if (lines.some((line) => line.trim() === pattern)) {
3770
+ log3.debug("git", "Pattern already in .git/info/exclude", { pattern });
3771
+ return;
3772
+ }
3773
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
3774
+ const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
3775
+ await fs13.writeFile(excludePath, newContent, "utf-8");
3776
+ log3.debug("git", "Added pattern to .git/info/exclude", { pattern });
3777
+ }
3329
3778
  var GitConflictError = class extends Error {
3330
3779
  constructor(conflictedFiles) {
3331
3780
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
@@ -3335,21 +3784,22 @@ var GitConflictError = class extends Error {
3335
3784
  };
3336
3785
  var GitManager = class _GitManager {
3337
3786
  constructor(options, gitOptions) {
3338
- this.repoPath = path11.resolve(options.repoPath);
3787
+ this.repoPath = path13.resolve(options.repoPath);
3339
3788
  this.baseBranch = options.baseBranch ?? "main";
3340
3789
  this.remote = options.remote ?? "origin";
3790
+ this.skipIndexMarker = options.skipIndexMarker ?? false;
3341
3791
  this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
3342
- this.git.env("GIT_CEILING_DIRECTORIES", path11.dirname(this.repoPath));
3792
+ this.git.env("GIT_CEILING_DIRECTORIES", path13.dirname(this.repoPath));
3343
3793
  }
3344
3794
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
3345
- log2.debug("git", "Cloning repository", {
3795
+ log3.debug("git", "Cloning repository", {
3346
3796
  remoteUrl,
3347
3797
  targetPath,
3348
3798
  baseBranch
3349
3799
  });
3350
3800
  const git = simpleGit2();
3351
3801
  await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
3352
- log2.debug("git", "Clone complete");
3802
+ log3.debug("git", "Clone complete");
3353
3803
  }
3354
3804
  /**
3355
3805
  * Initializes a local bare git repository to simulate a remote for dev mode.
@@ -3369,30 +3819,30 @@ var GitManager = class _GitManager {
3369
3819
  static async ensureLocalSimulatedRemote(options) {
3370
3820
  const existingLock = remoteInitLocks.get(options.remotePath);
3371
3821
  if (existingLock) {
3372
- log2.debug("git", "Waiting for existing remote initialization", {
3822
+ log3.debug("git", "Waiting for existing remote initialization", {
3373
3823
  remotePath: options.remotePath
3374
3824
  });
3375
3825
  await existingLock;
3376
3826
  }
3377
- const lockPromise = log2.timed("git", "ensureLocalSimulatedRemote", async () => {
3827
+ const lockPromise = log3.timed("git", "ensureLocalSimulatedRemote", async () => {
3378
3828
  let releaseLock;
3379
3829
  try {
3380
- log2.debug("git", "Initializing local simulated remote", {
3830
+ log3.debug("git", "Initializing local simulated remote", {
3381
3831
  remotePath: options.remotePath,
3382
3832
  baseBranch: options.baseBranch
3383
3833
  });
3384
- const remoteParent = path11.dirname(options.remotePath);
3834
+ const remoteParent = path13.dirname(options.remotePath);
3385
3835
  releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
3386
3836
  let remoteExists = false;
3387
3837
  try {
3388
- const stat = await fs10.stat(options.remotePath);
3838
+ const stat = await fs13.stat(options.remotePath);
3389
3839
  remoteExists = stat.isDirectory();
3390
3840
  } catch (err) {
3391
3841
  if (!isNotFoundError(err))
3392
3842
  throw err;
3393
3843
  }
3394
3844
  if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
3395
- log2.debug("git", "Remote already has base branch, skipping");
3845
+ log3.debug("git", "Remote already has base branch, skipping");
3396
3846
  return;
3397
3847
  }
3398
3848
  let gitRoot = options.sourcePath;
@@ -3411,8 +3861,8 @@ var GitManager = class _GitManager {
3411
3861
  }
3412
3862
  let hasCommits = false;
3413
3863
  try {
3414
- const log4 = await sourceGit.log(["-1"]);
3415
- hasCommits = log4.total > 0;
3864
+ const log5 = await sourceGit.log(["-1"]);
3865
+ hasCommits = log5.total > 0;
3416
3866
  } catch {
3417
3867
  hasCommits = false;
3418
3868
  }
@@ -3424,12 +3874,12 @@ var GitManager = class _GitManager {
3424
3874
  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.`);
3425
3875
  }
3426
3876
  if (remoteExists) {
3427
- log2.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
3877
+ log3.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
3428
3878
  remotePath: options.remotePath,
3429
3879
  baseBranch: options.baseBranch
3430
3880
  });
3431
3881
  } else {
3432
- log2.debug("git", "Creating bare remote repository");
3882
+ log3.debug("git", "Creating bare remote repository");
3433
3883
  await simpleGit2().raw([
3434
3884
  "init",
3435
3885
  "--bare",
@@ -3443,13 +3893,13 @@ var GitManager = class _GitManager {
3443
3893
  baseBranch: options.baseBranch,
3444
3894
  subdirectory: options.subdirectory
3445
3895
  });
3446
- log2.debug("git", "Remote initialization complete");
3896
+ log3.debug("git", "Remote initialization complete");
3447
3897
  } finally {
3448
3898
  if (releaseLock) {
3449
3899
  try {
3450
3900
  await releaseLock();
3451
3901
  } catch (err) {
3452
- log2.debug("git", "Failed to release remote-init lock", { err });
3902
+ log3.debug("git", "Failed to release remote-init lock", { err });
3453
3903
  }
3454
3904
  }
3455
3905
  remoteInitLocks.delete(options.remotePath);
@@ -3548,7 +3998,7 @@ var GitManager = class _GitManager {
3548
3998
  */
3549
3999
  static async validateGitRepoExists(repoPath) {
3550
4000
  try {
3551
- const stat = await fs10.stat(path11.join(repoPath, ".git"));
4001
+ const stat = await fs13.stat(path13.join(repoPath, ".git"));
3552
4002
  if (!stat.isDirectory()) {
3553
4003
  throw new Error(`Expected git repo at ${repoPath}`);
3554
4004
  }
@@ -3586,9 +4036,9 @@ var GitManager = class _GitManager {
3586
4036
  return process.env[config.envVarName];
3587
4037
  if (config.autoDetectRemotePath) {
3588
4038
  try {
3589
- const stat = await fs10.stat(config.autoDetectRemotePath);
4039
+ const stat = await fs13.stat(config.autoDetectRemotePath);
3590
4040
  if (stat.isDirectory()) {
3591
- log2.debug("git", "Auto-detected local remote", {
4041
+ log3.debug("git", "Auto-detected local remote", {
3592
4042
  path: config.autoDetectRemotePath
3593
4043
  });
3594
4044
  return config.autoDetectRemotePath;
@@ -3599,8 +4049,8 @@ var GitManager = class _GitManager {
3599
4049
  if (config.shouldAutoInitLocal) {
3600
4050
  const gitRoot = await this.findGitRoot();
3601
4051
  const sourceRoot = options.sourceRoot;
3602
- const sourcePath = sourceRoot ? path11.resolve(gitRoot, sourceRoot) : gitRoot;
3603
- const localRemotePath = path11.join(sourcePath, config.defaultRemotePath);
4052
+ const sourcePath = sourceRoot ? path13.resolve(gitRoot, sourceRoot) : gitRoot;
4053
+ const localRemotePath = path13.join(sourcePath, config.defaultRemotePath);
3604
4054
  await this.ensureLocalSimulatedRemote({
3605
4055
  remotePath: localRemotePath,
3606
4056
  sourcePath: gitRoot,
@@ -3626,24 +4076,24 @@ var GitManager = class _GitManager {
3626
4076
  const baseBranch = await resolveBaseBranch({
3627
4077
  defaultBaseBranch: options.baseBranch,
3628
4078
  mode: options.mode,
3629
- detectFrom: options.sourceRoot ? path11.resolve(process.cwd(), options.sourceRoot) : process.cwd()
4079
+ detectFrom: options.sourceRoot ? path13.resolve(process.cwd(), options.sourceRoot) : process.cwd()
3630
4080
  });
3631
4081
  const remoteName = options.remoteName ?? "origin";
3632
4082
  let repoExists = false;
3633
4083
  try {
3634
4084
  const checkGit = simpleGit2({ baseDir: options.workspacePath });
3635
- checkGit.env("GIT_CEILING_DIRECTORIES", path11.dirname(options.workspacePath));
4085
+ checkGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
3636
4086
  await checkGit.raw(["rev-parse", "--git-dir"]);
3637
4087
  repoExists = true;
3638
4088
  } catch {
3639
- const gitPath = path11.join(options.workspacePath, ".git");
4089
+ const gitPath = path13.join(options.workspacePath, ".git");
3640
4090
  try {
3641
- const stat = await fs10.stat(gitPath);
4091
+ const stat = await fs13.stat(gitPath);
3642
4092
  if (stat.isDirectory()) {
3643
- log2.debug("git", "Removing corrupt .git directory", {
4093
+ log3.debug("git", "Removing corrupt .git directory", {
3644
4094
  workspacePath: options.workspacePath
3645
4095
  });
3646
- await fs10.rm(gitPath, { recursive: true });
4096
+ await fs13.rm(gitPath, { recursive: true });
3647
4097
  }
3648
4098
  } catch (cleanupErr) {
3649
4099
  if (!isNotFoundError(cleanupErr))
@@ -3669,7 +4119,7 @@ var GitManager = class _GitManager {
3669
4119
  }
3670
4120
  justCloned = true;
3671
4121
  const freshGit = simpleGit2({ baseDir: options.workspacePath });
3672
- freshGit.env("GIT_CEILING_DIRECTORIES", path11.dirname(options.workspacePath));
4122
+ freshGit.env("GIT_CEILING_DIRECTORIES", path13.dirname(options.workspacePath));
3673
4123
  await freshGit.addConfig("canopycms.managed", "true");
3674
4124
  await freshGit.addConfig("user.name", options.gitBotAuthorName);
3675
4125
  await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
@@ -3677,12 +4127,13 @@ var GitManager = class _GitManager {
3677
4127
  const git = new _GitManager({
3678
4128
  repoPath: options.workspacePath,
3679
4129
  baseBranch,
3680
- remote: remoteName
4130
+ remote: remoteName,
4131
+ skipIndexMarker: options.branchType === "orphan"
3681
4132
  });
3682
4133
  await git.git.addConfig("canopycms.managed", "true");
3683
4134
  await git.git.addConfig("user.name", options.gitBotAuthorName);
3684
4135
  await git.git.addConfig("user.email", options.gitBotAuthorEmail);
3685
- log2.debug("git", "Marked workspace as CanopyCMS-managed", {
4136
+ log3.debug("git", "Marked workspace as CanopyCMS-managed", {
3686
4137
  workspacePath: options.workspacePath
3687
4138
  });
3688
4139
  if (!justCloned) {
@@ -3717,7 +4168,33 @@ var GitManager = class _GitManager {
3717
4168
  tracking: s.tracking
3718
4169
  };
3719
4170
  }
4171
+ /**
4172
+ * Mark ContentStore ID indexes rooted at (or under) this repo as stale.
4173
+ * Called after operations that mutate the working tree (checkout/merge/rebase) so
4174
+ * ID→path lookups don't keep resolving to pre-mutation paths. Invoked in `finally`
4175
+ * blocks because even failed merges/rebases may have touched the tree before
4176
+ * aborting; over-invalidating is safe.
4177
+ *
4178
+ * Covers both scopes: in-process stores via the registry, and stores in OTHER
4179
+ * processes sharing the filesystem (worker vs Lambda on EFS) via the on-disk
4180
+ * generation marker — unless this manager targets a settings workspace
4181
+ * (skipIndexMarker), where only the free in-process invalidation runs.
4182
+ */
4183
+ async invalidateContentIndexes() {
4184
+ if (this.skipIndexMarker) {
4185
+ invalidateContentIndexesForRoot(this.repoPath);
4186
+ return;
4187
+ }
4188
+ await invalidateContentIndexesDurable(this.repoPath);
4189
+ }
3720
4190
  async checkoutBranch(branch) {
4191
+ try {
4192
+ await this.checkoutBranchInner(branch);
4193
+ } finally {
4194
+ await this.invalidateContentIndexes();
4195
+ }
4196
+ }
4197
+ async checkoutBranchInner(branch) {
3721
4198
  const branches = await this.git.branch();
3722
4199
  if (branches.all.includes(branch)) {
3723
4200
  await this.git.checkout(branch);
@@ -3741,6 +4218,13 @@ var GitManager = class _GitManager {
3741
4218
  }
3742
4219
  }
3743
4220
  async pullBase() {
4221
+ try {
4222
+ await this.pullBaseInner();
4223
+ } finally {
4224
+ await this.invalidateContentIndexes();
4225
+ }
4226
+ }
4227
+ async pullBaseInner() {
3744
4228
  await this.git.fetch(this.remote, this.baseBranch);
3745
4229
  try {
3746
4230
  await this.git.merge([`${this.remote}/${this.baseBranch}`]);
@@ -3761,6 +4245,13 @@ var GitManager = class _GitManager {
3761
4245
  }
3762
4246
  }
3763
4247
  async pullCurrentBranch() {
4248
+ try {
4249
+ await this.pullCurrentBranchInner();
4250
+ } finally {
4251
+ await this.invalidateContentIndexes();
4252
+ }
4253
+ }
4254
+ async pullCurrentBranchInner() {
3764
4255
  const branches = await this.git.branch();
3765
4256
  const currentBranch = branches.current;
3766
4257
  await this.git.fetch(this.remote, currentBranch);
@@ -3783,6 +4274,13 @@ var GitManager = class _GitManager {
3783
4274
  }
3784
4275
  }
3785
4276
  async rebaseOntoBase() {
4277
+ try {
4278
+ await this.rebaseOntoBaseInner();
4279
+ } finally {
4280
+ await this.invalidateContentIndexes();
4281
+ }
4282
+ }
4283
+ async rebaseOntoBaseInner() {
3786
4284
  await this.git.fetch(this.remote, this.baseBranch);
3787
4285
  try {
3788
4286
  await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
@@ -3811,7 +4309,13 @@ var GitManager = class _GitManager {
3811
4309
  }
3812
4310
  async push(branch) {
3813
4311
  const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3814
- await this.git.push(this.remote, `${target}:${target}`, ["--set-upstream"]);
4312
+ await this.git.raw([
4313
+ "push",
4314
+ "--set-upstream",
4315
+ "--end-of-options",
4316
+ this.remote,
4317
+ `${target}:${target}`
4318
+ ]);
3815
4319
  }
3816
4320
  async ensureAuthor(author) {
3817
4321
  const config = await this.git.listConfig();
@@ -3865,7 +4369,7 @@ var GitManager = class _GitManager {
3865
4369
  */
3866
4370
  async forcePush(branch) {
3867
4371
  const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3868
- await this.git.push(this.remote, target, ["--force-with-lease"]);
4372
+ await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
3869
4373
  }
3870
4374
  /**
3871
4375
  * Get remote URL for current repo
@@ -3885,24 +4389,7 @@ var GitManager = class _GitManager {
3885
4389
  * This is idempotent - if the pattern already exists, it won't be added again.
3886
4390
  */
3887
4391
  async ensureGitExclude(pattern) {
3888
- const excludePath = path11.join(this.repoPath, ".git", "info", "exclude");
3889
- await fs10.mkdir(path11.dirname(excludePath), { recursive: true });
3890
- let content = "";
3891
- try {
3892
- content = await fs10.readFile(excludePath, "utf-8");
3893
- } catch (err) {
3894
- if (!isNotFoundError(err))
3895
- throw err;
3896
- }
3897
- const lines = content.split("\n");
3898
- if (lines.some((line) => line.trim() === pattern)) {
3899
- log2.debug("git", "Pattern already in .git/info/exclude", { pattern });
3900
- return;
3901
- }
3902
- const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
3903
- const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
3904
- await fs10.writeFile(excludePath, newContent, "utf-8");
3905
- log2.debug("git", "Added pattern to .git/info/exclude", { pattern });
4392
+ await ensureGitExcludePattern(this.repoPath, pattern);
3906
4393
  }
3907
4394
  /**
3908
4395
  * Create an orphan branch for settings (permissions/groups).
@@ -3910,16 +4397,24 @@ var GitManager = class _GitManager {
3910
4397
  * Orphan branches have no shared history with other branches - they start fresh.
3911
4398
  * This is perfect for deployment-specific settings that shouldn't pollute content history.
3912
4399
  *
3913
- * The branch contains only settings files in .canopy-meta/ (groups.json, permissions.json).
4400
+ * The branch contains only settings files committed by explicit path
4401
+ * (e.g. permissions.json, groups.json at the workspace root).
3914
4402
  *
3915
4403
  * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
3916
4404
  * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
3917
4405
  */
3918
4406
  async createOrphanSettingsBranch(branchName, initialFiles) {
3919
- log2.debug("git", "Creating orphan settings branch", { branchName });
4407
+ try {
4408
+ await this.createOrphanSettingsBranchInner(branchName, initialFiles);
4409
+ } finally {
4410
+ await this.invalidateContentIndexes();
4411
+ }
4412
+ }
4413
+ async createOrphanSettingsBranchInner(branchName, initialFiles) {
4414
+ log3.debug("git", "Creating orphan settings branch", { branchName });
3920
4415
  const branches = await this.git.branch();
3921
4416
  if (branches.all.includes(branchName)) {
3922
- log2.debug("git", "Orphan branch already exists", { branchName });
4417
+ log3.debug("git", "Orphan branch already exists", { branchName });
3923
4418
  await this.git.checkout(branchName);
3924
4419
  return;
3925
4420
  }
@@ -3929,25 +4424,25 @@ var GitManager = class _GitManager {
3929
4424
  } catch {
3930
4425
  }
3931
4426
  for (const [filePath, content] of Object.entries(initialFiles)) {
3932
- const absolutePath = path11.join(this.repoPath, filePath);
3933
- await fs10.mkdir(path11.dirname(absolutePath), { recursive: true });
3934
- await fs10.writeFile(absolutePath, content, "utf-8");
4427
+ const absolutePath = path13.join(this.repoPath, filePath);
4428
+ await fs13.mkdir(path13.dirname(absolutePath), { recursive: true });
4429
+ await fs13.writeFile(absolutePath, content, "utf-8");
3935
4430
  await this.git.add(filePath);
3936
4431
  }
3937
4432
  await this.git.commit("Initialize settings branch", ["--allow-empty"]);
3938
- log2.debug("git", "Orphan settings branch created", { branchName });
4433
+ log3.debug("git", "Orphan settings branch created", { branchName });
3939
4434
  }
3940
4435
  };
3941
4436
 
3942
4437
  // dist/branch-workspace.js
3943
- var log3 = createDebugLogger({ prefix: "BranchWorkspace" });
4438
+ var log4 = createDebugLogger({ prefix: "BranchWorkspace" });
3944
4439
  var workspaceInitLocks = /* @__PURE__ */ new Map();
3945
4440
  var BranchWorkspaceManager = class {
3946
4441
  constructor(config) {
3947
4442
  this.config = config;
3948
4443
  }
3949
4444
  async ensureGitWorkspace(options) {
3950
- return log3.timed("workspace", "ensureGitWorkspace", async () => {
4445
+ return log4.timed("workspace", "ensureGitWorkspace", async () => {
3951
4446
  const existingLock = workspaceInitLocks.get(options.branchRoot);
3952
4447
  if (existingLock) {
3953
4448
  await existingLock;
@@ -3956,11 +4451,11 @@ var BranchWorkspaceManager = class {
3956
4451
  const lockPromise = (async () => {
3957
4452
  let releaseLock;
3958
4453
  try {
3959
- log3.debug("workspace", "Ensuring git workspace", {
4454
+ log4.debug("workspace", "Ensuring git workspace", {
3960
4455
  branchName: options.branchName,
3961
4456
  mode: options.mode
3962
4457
  });
3963
- releaseLock = await acquireProvisioningLock(path12.dirname(options.branchRoot), `.${path12.basename(options.branchRoot)}.init.lock`);
4458
+ releaseLock = await acquireProvisioningLock(path14.dirname(options.branchRoot), `.${path14.basename(options.branchRoot)}.init.lock`);
3964
4459
  await GitManager.initializeWorkspace({
3965
4460
  workspacePath: options.branchRoot,
3966
4461
  branchName: options.branchName,
@@ -3980,7 +4475,7 @@ var BranchWorkspaceManager = class {
3980
4475
  try {
3981
4476
  await releaseLock();
3982
4477
  } catch (err) {
3983
- log3.debug("workspace", "Failed to release workspace-init lock", { err });
4478
+ log4.debug("workspace", "Failed to release workspace-init lock", { err });
3984
4479
  }
3985
4480
  }
3986
4481
  workspaceInitLocks.delete(options.branchRoot);
@@ -4000,7 +4495,7 @@ var BranchWorkspaceManager = class {
4000
4495
  const baseBranch = await resolveBaseBranch({
4001
4496
  defaultBaseBranch: this.config.defaultBaseBranch,
4002
4497
  mode,
4003
- detectFrom: this.config.sourceRoot ? path12.resolve(process.cwd(), this.config.sourceRoot) : void 0
4498
+ detectFrom: this.config.sourceRoot ? path14.resolve(process.cwd(), this.config.sourceRoot) : void 0
4004
4499
  });
4005
4500
  await this.ensureGitWorkspace({
4006
4501
  branchRoot,
@@ -4104,15 +4599,15 @@ async function generateAIContentFiles(options) {
4104
4599
  config: aiConfig,
4105
4600
  entryLinkUrl: config.entryLinkUrl
4106
4601
  });
4107
- const absoluteOutputDir = path13.resolve(outputDir) + path13.sep;
4602
+ const absoluteOutputDir = path15.resolve(outputDir) + path15.sep;
4108
4603
  let fileCount = 0;
4109
4604
  for (const [filePath, content] of result.files) {
4110
- const absolutePath = path13.resolve(path13.join(absoluteOutputDir, filePath));
4605
+ const absolutePath = path15.resolve(path15.join(absoluteOutputDir, filePath));
4111
4606
  if (!absolutePath.startsWith(absoluteOutputDir)) {
4112
4607
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
4113
4608
  }
4114
- await fs11.mkdir(path13.dirname(absolutePath), { recursive: true });
4115
- await fs11.writeFile(absolutePath, content, "utf-8");
4609
+ await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
4610
+ await fs14.writeFile(absolutePath, content, "utf-8");
4116
4611
  fileCount++;
4117
4612
  }
4118
4613
  return { fileCount, outputDir: absoluteOutputDir };
@@ -4123,7 +4618,7 @@ var jiti = createJiti(import.meta.url);
4123
4618
  async function generateAIContentCLI(options) {
4124
4619
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
4125
4620
  console.log("\nCanopyCMS generate-ai-content\n");
4126
- const canopyConfigPath = path14.join(projectDir, "canopycms.config.ts");
4621
+ const canopyConfigPath = path16.join(projectDir, "canopycms.config.ts");
4127
4622
  let canopyConfigModule;
4128
4623
  try {
4129
4624
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -4134,7 +4629,7 @@ async function generateAIContentCLI(options) {
4134
4629
  }
4135
4630
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
4136
4631
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
4137
- const schemasPath = path14.join(projectDir, appDir, "schemas.ts");
4632
+ const schemasPath = path16.join(projectDir, appDir, "schemas.ts");
4138
4633
  let entrySchemaRegistry = {};
4139
4634
  try {
4140
4635
  const schemasModule = await jiti.import(schemasPath);
@@ -4145,7 +4640,7 @@ async function generateAIContentCLI(options) {
4145
4640
  let aiConfig;
4146
4641
  if (configPath) {
4147
4642
  try {
4148
- const aiConfigModule = await jiti.import(path14.resolve(configPath));
4643
+ const aiConfigModule = await jiti.import(path16.resolve(configPath));
4149
4644
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
4150
4645
  } catch (err) {
4151
4646
  console.error(`Could not load AI config from ${configPath}`);
@@ -4162,7 +4657,7 @@ async function generateAIContentCLI(options) {
4162
4657
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
4163
4658
  process.exit(1);
4164
4659
  }
4165
- const resolvedOutput = path14.resolve(projectDir, outputDir);
4660
+ const resolvedOutput = path16.resolve(projectDir, outputDir);
4166
4661
  console.log(` Output: ${resolvedOutput}`);
4167
4662
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
4168
4663
  const result = await generateAIContentFiles({