canopycms 0.0.61 → 0.0.63

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 (237) hide show
  1. package/dist/ai/index.d.ts +9 -0
  2. package/dist/ai/index.d.ts.map +1 -1
  3. package/dist/ai/index.js +9 -0
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/json-to-markdown.js +1 -2
  6. package/dist/ai/json-to-markdown.js.map +1 -1
  7. package/dist/ai/to-plain-text.d.ts +57 -0
  8. package/dist/ai/to-plain-text.d.ts.map +1 -0
  9. package/dist/ai/to-plain-text.js +255 -0
  10. package/dist/ai/to-plain-text.js.map +1 -0
  11. package/dist/ai/transform-components.d.ts.map +1 -1
  12. package/dist/ai/transform-components.js +70 -5
  13. package/dist/ai/transform-components.js.map +1 -1
  14. package/dist/api/__test__/mock-client.d.ts +5 -1
  15. package/dist/api/__test__/mock-client.d.ts.map +1 -1
  16. package/dist/api/__test__/mock-client.js +9 -2
  17. package/dist/api/__test__/mock-client.js.map +1 -1
  18. package/dist/api/admin-branch-health.d.ts +36 -1
  19. package/dist/api/admin-branch-health.d.ts.map +1 -1
  20. package/dist/api/admin-branch-health.js +176 -2
  21. package/dist/api/admin-branch-health.js.map +1 -1
  22. package/dist/api/admin.d.ts +8 -1
  23. package/dist/api/admin.d.ts.map +1 -1
  24. package/dist/api/admin.js.map +1 -1
  25. package/dist/api/branch.d.ts.map +1 -1
  26. package/dist/api/branch.js +16 -2
  27. package/dist/api/branch.js.map +1 -1
  28. package/dist/api/client.d.ts +5 -1
  29. package/dist/api/client.d.ts.map +1 -1
  30. package/dist/api/client.js +6 -0
  31. package/dist/api/client.js.map +1 -1
  32. package/dist/api/content.d.ts +11 -5
  33. package/dist/api/content.d.ts.map +1 -1
  34. package/dist/api/content.js +79 -6
  35. package/dist/api/content.js.map +1 -1
  36. package/dist/api/entries.d.ts +10 -0
  37. package/dist/api/entries.d.ts.map +1 -1
  38. package/dist/api/entries.js +40 -15
  39. package/dist/api/entries.js.map +1 -1
  40. package/dist/api/schema.d.ts +4 -4
  41. package/dist/api/schema.d.ts.map +1 -1
  42. package/dist/api/schema.js +13 -13
  43. package/dist/api/schema.js.map +1 -1
  44. package/dist/assets/transform.d.ts.map +1 -1
  45. package/dist/assets/transform.js +19 -6
  46. package/dist/assets/transform.js.map +1 -1
  47. package/dist/authorization/branch.d.ts +55 -3
  48. package/dist/authorization/branch.d.ts.map +1 -1
  49. package/dist/authorization/branch.js +55 -9
  50. package/dist/authorization/branch.js.map +1 -1
  51. package/dist/authorization/types.d.ts +1 -1
  52. package/dist/authorization/types.d.ts.map +1 -1
  53. package/dist/branch-health.d.ts +13 -0
  54. package/dist/branch-health.d.ts.map +1 -1
  55. package/dist/branch-health.js +23 -0
  56. package/dist/branch-health.js.map +1 -1
  57. package/dist/build-canopy.d.ts +49 -0
  58. package/dist/build-canopy.d.ts.map +1 -0
  59. package/dist/build-canopy.js +51 -0
  60. package/dist/build-canopy.js.map +1 -0
  61. package/dist/cli/cli.js +935 -562
  62. package/dist/cli/generate-ai-content.js +837 -480
  63. package/dist/cli/init.js +25 -7
  64. package/dist/cli/template-files/Dockerfile.cms.template +19 -1
  65. package/dist/cli/template-files/canopycms.config.ts.template +25 -1
  66. package/dist/cli/template-files/cms-stack.ts.template +7 -1
  67. package/dist/client.d.ts +1 -0
  68. package/dist/client.d.ts.map +1 -1
  69. package/dist/client.js.map +1 -1
  70. package/dist/config/schemas/field.d.ts +15 -15
  71. package/dist/config/types.d.ts +2 -2
  72. package/dist/config/types.d.ts.map +1 -1
  73. package/dist/config/types.js +0 -1
  74. package/dist/config/types.js.map +1 -1
  75. package/dist/config/validation.d.ts +9 -0
  76. package/dist/config/validation.d.ts.map +1 -1
  77. package/dist/config/validation.js +14 -0
  78. package/dist/config/validation.js.map +1 -1
  79. package/dist/content-id-index.d.ts +97 -5
  80. package/dist/content-id-index.d.ts.map +1 -1
  81. package/dist/content-id-index.js +170 -18
  82. package/dist/content-id-index.js.map +1 -1
  83. package/dist/content-listing.d.ts +110 -5
  84. package/dist/content-listing.d.ts.map +1 -1
  85. package/dist/content-listing.js +164 -23
  86. package/dist/content-listing.js.map +1 -1
  87. package/dist/content-reader.d.ts +41 -4
  88. package/dist/content-reader.d.ts.map +1 -1
  89. package/dist/content-reader.js +17 -4
  90. package/dist/content-reader.js.map +1 -1
  91. package/dist/content-store.d.ts +91 -8
  92. package/dist/content-store.d.ts.map +1 -1
  93. package/dist/content-store.js +443 -258
  94. package/dist/content-store.js.map +1 -1
  95. package/dist/content-tree.d.ts +31 -2
  96. package/dist/content-tree.d.ts.map +1 -1
  97. package/dist/content-tree.js +22 -5
  98. package/dist/content-tree.js.map +1 -1
  99. package/dist/context.d.ts +30 -10
  100. package/dist/context.d.ts.map +1 -1
  101. package/dist/context.js +55 -4
  102. package/dist/context.js.map +1 -1
  103. package/dist/editor/BranchManager.d.ts.map +1 -1
  104. package/dist/editor/BranchManager.js +18 -6
  105. package/dist/editor/BranchManager.js.map +1 -1
  106. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -2
  108. package/dist/editor/CanopyEditor.js.map +1 -1
  109. package/dist/editor/CanopyEditorPage.d.ts +9 -1
  110. package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
  111. package/dist/editor/CanopyEditorPage.js +9 -2
  112. package/dist/editor/CanopyEditorPage.js.map +1 -1
  113. package/dist/editor/Editor.d.ts +9 -1
  114. package/dist/editor/Editor.d.ts.map +1 -1
  115. package/dist/editor/Editor.js +58 -24
  116. package/dist/editor/Editor.js.map +1 -1
  117. package/dist/editor/FormRenderer.d.ts.map +1 -1
  118. package/dist/editor/FormRenderer.js +12 -0
  119. package/dist/editor/FormRenderer.js.map +1 -1
  120. package/dist/editor/admin/SystemHealthPanel.js +6 -2
  121. package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
  122. package/dist/editor/components/EntryCreateModal.d.ts +9 -1
  123. package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
  124. package/dist/editor/components/EntryCreateModal.js +4 -1
  125. package/dist/editor/components/EntryCreateModal.js.map +1 -1
  126. package/dist/editor/fields/DateTimeField.d.ts +46 -0
  127. package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
  128. package/dist/editor/fields/DateTimeField.js +66 -0
  129. package/dist/editor/fields/DateTimeField.js.map +1 -0
  130. package/dist/editor/fields/NumberField.d.ts +34 -0
  131. package/dist/editor/fields/NumberField.d.ts.map +1 -0
  132. package/dist/editor/fields/NumberField.js +53 -0
  133. package/dist/editor/fields/NumberField.js.map +1 -0
  134. package/dist/editor/fields/NumberListField.d.ts +25 -0
  135. package/dist/editor/fields/NumberListField.d.ts.map +1 -0
  136. package/dist/editor/fields/NumberListField.js +53 -0
  137. package/dist/editor/fields/NumberListField.js.map +1 -0
  138. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  139. package/dist/editor/hooks/useBranchManager.js +115 -26
  140. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  141. package/dist/editor/hooks/useDraftManager.d.ts +8 -0
  142. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  143. package/dist/editor/hooks/useDraftManager.js +261 -21
  144. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  145. package/dist/editor/hooks/useEntryManager.d.ts +21 -0
  146. package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
  147. package/dist/editor/hooks/useEntryManager.js +25 -2
  148. package/dist/editor/hooks/useEntryManager.js.map +1 -1
  149. package/dist/editor/theme.d.ts +8 -8
  150. package/dist/entry-schema.d.ts +228 -8
  151. package/dist/entry-schema.d.ts.map +1 -1
  152. package/dist/entry-schema.js +124 -0
  153. package/dist/entry-schema.js.map +1 -1
  154. package/dist/git-manager.d.ts +51 -7
  155. package/dist/git-manager.d.ts.map +1 -1
  156. package/dist/git-manager.js +105 -14
  157. package/dist/git-manager.js.map +1 -1
  158. package/dist/http/handler.d.ts.map +1 -1
  159. package/dist/http/handler.js +70 -27
  160. package/dist/http/handler.js.map +1 -1
  161. package/dist/http/router.d.ts.map +1 -1
  162. package/dist/http/router.js +40 -3
  163. package/dist/http/router.js.map +1 -1
  164. package/dist/index.d.ts +1 -0
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +7 -0
  167. package/dist/index.js.map +1 -1
  168. package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
  169. package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
  170. package/dist/operating-mode/deployment-name-fixtures.js +55 -0
  171. package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
  172. package/dist/operating-mode/deployment-name.d.ts +9 -0
  173. package/dist/operating-mode/deployment-name.d.ts.map +1 -1
  174. package/dist/operating-mode/deployment-name.js +9 -1
  175. package/dist/operating-mode/deployment-name.js.map +1 -1
  176. package/dist/operating-mode/index.d.ts +2 -1
  177. package/dist/operating-mode/index.d.ts.map +1 -1
  178. package/dist/operating-mode/index.js +4 -1
  179. package/dist/operating-mode/index.js.map +1 -1
  180. package/dist/operating-mode/mode-env.d.ts +58 -0
  181. package/dist/operating-mode/mode-env.d.ts.map +1 -0
  182. package/dist/operating-mode/mode-env.js +97 -0
  183. package/dist/operating-mode/mode-env.js.map +1 -0
  184. package/dist/resolve-canopy-user.d.ts +34 -0
  185. package/dist/resolve-canopy-user.d.ts.map +1 -0
  186. package/dist/resolve-canopy-user.js +55 -0
  187. package/dist/resolve-canopy-user.js.map +1 -0
  188. package/dist/schema/schema-store.d.ts +53 -2
  189. package/dist/schema/schema-store.d.ts.map +1 -1
  190. package/dist/schema/schema-store.js +92 -21
  191. package/dist/schema/schema-store.js.map +1 -1
  192. package/dist/server.d.ts +60 -0
  193. package/dist/server.d.ts.map +1 -1
  194. package/dist/server.js +58 -0
  195. package/dist/server.js.map +1 -1
  196. package/dist/services.d.ts +1 -2
  197. package/dist/services.d.ts.map +1 -1
  198. package/dist/services.js +42 -12
  199. package/dist/services.js.map +1 -1
  200. package/dist/settings-workspace.d.ts +40 -0
  201. package/dist/settings-workspace.d.ts.map +1 -1
  202. package/dist/settings-workspace.js +139 -151
  203. package/dist/settings-workspace.js.map +1 -1
  204. package/dist/static/index.d.ts +65 -5
  205. package/dist/static/index.d.ts.map +1 -1
  206. package/dist/static/index.js +65 -12
  207. package/dist/static/index.js.map +1 -1
  208. package/dist/static/seo.d.ts +150 -0
  209. package/dist/static/seo.d.ts.map +1 -0
  210. package/dist/static/seo.js +189 -0
  211. package/dist/static/seo.js.map +1 -0
  212. package/dist/types.d.ts +10 -0
  213. package/dist/types.d.ts.map +1 -1
  214. package/dist/utils/content-write-lock.d.ts +127 -0
  215. package/dist/utils/content-write-lock.d.ts.map +1 -0
  216. package/dist/utils/content-write-lock.js +170 -0
  217. package/dist/utils/content-write-lock.js.map +1 -0
  218. package/dist/utils/error.d.ts +1 -1
  219. package/dist/utils/error.js +1 -1
  220. package/dist/utils/git.d.ts +16 -0
  221. package/dist/utils/git.d.ts.map +1 -1
  222. package/dist/utils/git.js +22 -0
  223. package/dist/utils/git.js.map +1 -1
  224. package/dist/utils/sanitize-href.d.ts +74 -3
  225. package/dist/utils/sanitize-href.d.ts.map +1 -1
  226. package/dist/utils/sanitize-href.js +125 -6
  227. package/dist/utils/sanitize-href.js.map +1 -1
  228. package/dist/validation/entry-link-validator.d.ts +1 -1
  229. package/dist/validation/entry-link-validator.js +3 -3
  230. package/dist/validation/entry-link-validator.js.map +1 -1
  231. package/dist/validation/entry-validator.js +1 -1
  232. package/dist/validation/entry-validator.js.map +1 -1
  233. package/dist/worker/cms-worker.d.ts +13 -0
  234. package/dist/worker/cms-worker.d.ts.map +1 -1
  235. package/dist/worker/cms-worker.js +326 -264
  236. package/dist/worker/cms-worker.js.map +1 -1
  237. package/package.json +2 -2
package/dist/cli/cli.js CHANGED
@@ -112,6 +112,9 @@ import path7 from "node:path";
112
112
  import * as p from "@clack/prompts";
113
113
  import { createJiti } from "jiti";
114
114
  import path8 from "node:path";
115
+ function canopyLogWarn(...args) {
116
+ active.warn(...args);
117
+ }
115
118
  function getErrorMessage2(err) {
116
119
  if (err instanceof Error) {
117
120
  return err.message;
@@ -740,9 +743,6 @@ async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger) {
740
743
  });
741
744
  }
742
745
  }
743
- function canopyLogWarn(...args) {
744
- active.warn(...args);
745
- }
746
746
  function resolveDeploymentName(config, modeDefault) {
747
747
  const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
748
748
  const configValue = config.deploymentName;
@@ -1047,7 +1047,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1047
1047
  }
1048
1048
  console.log("\nDone");
1049
1049
  }
1050
- var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, active, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
1050
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, active, init_logger, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_mode_env, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
1051
1051
  var init_init = __esm({
1052
1052
  "dist/cli/init.js"() {
1053
1053
  "use strict";
@@ -1060,6 +1060,12 @@ var init_init = __esm({
1060
1060
  for (var name in all)
1061
1061
  __defProp2(target, name, { get: all[name], enumerable: true });
1062
1062
  };
1063
+ init_logger = __esm2({
1064
+ "dist/utils/logger.js"() {
1065
+ "use strict";
1066
+ active = console;
1067
+ }
1068
+ });
1063
1069
  init_types = __esm2({
1064
1070
  "dist/config/types.js"() {
1065
1071
  "use strict";
@@ -1068,7 +1074,6 @@ var init_init = __esm({
1068
1074
  "number",
1069
1075
  "boolean",
1070
1076
  "datetime",
1071
- "rich-text",
1072
1077
  "markdown",
1073
1078
  "mdx",
1074
1079
  "code"
@@ -1353,11 +1358,18 @@ var init_init = __esm({
1353
1358
  init_types2();
1354
1359
  }
1355
1360
  });
1361
+ init_mode_env = __esm2({
1362
+ "dist/operating-mode/mode-env.js"() {
1363
+ "use strict";
1364
+ init_logger();
1365
+ }
1366
+ });
1356
1367
  init_validation = __esm2({
1357
1368
  "dist/config/validation.js"() {
1358
1369
  "use strict";
1359
1370
  init_config();
1360
1371
  init_flatten();
1372
+ init_mode_env();
1361
1373
  }
1362
1374
  });
1363
1375
  init_helpers = __esm2({
@@ -1700,7 +1712,7 @@ var init_init = __esm({
1700
1712
  return true;
1701
1713
  }
1702
1714
  };
1703
- active = console;
1715
+ init_logger();
1704
1716
  warned = false;
1705
1717
  VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1706
1718
  isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
@@ -1814,6 +1826,7 @@ var init_init = __esm({
1814
1826
  }
1815
1827
  };
1816
1828
  strategyCache = /* @__PURE__ */ new Map();
1829
+ init_mode_env();
1817
1830
  init_fs2();
1818
1831
  init_error2();
1819
1832
  }
@@ -1831,62 +1844,63 @@ import { isAbsolute as isAbsolute2 } from "pathe";
1831
1844
  import { z as z42 } from "zod";
1832
1845
  import { z as z52 } from "zod";
1833
1846
  import { z as z6 } from "zod";
1834
- import path62 from "node:path";
1835
- import path18 from "node:path";
1847
+ import path82 from "node:path";
1848
+ import path19 from "node:path";
1836
1849
  import { createJiti as createJiti2 } from "jiti";
1837
1850
  import fs16 from "node:fs/promises";
1838
- import path17 from "node:path";
1839
- import fs8 from "node:fs/promises";
1840
- import path82 from "node:path";
1851
+ import path18 from "node:path";
1852
+ import fs9 from "node:fs/promises";
1853
+ import path10 from "node:path";
1841
1854
  import matter from "gray-matter";
1842
1855
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
1843
1856
  import fs7 from "node:fs/promises";
1844
1857
  import path9 from "node:path";
1858
+ import path32 from "node:path";
1845
1859
  import fs23 from "node:fs/promises";
1846
1860
  import path22 from "node:path";
1847
- import { generate } from "short-uuid";
1848
- import path32 from "node:path";
1861
+ import lockfile from "proper-lockfile";
1849
1862
  import fs32 from "node:fs/promises";
1850
1863
  import path42 from "node:path";
1851
- import { randomUUID } from "node:crypto";
1852
- import fs52 from "node:fs/promises";
1864
+ import { generate } from "short-uuid";
1853
1865
  import path52 from "node:path";
1854
- import { promises as fs42 } from "fs";
1866
+ import fs42 from "node:fs/promises";
1867
+ import path62 from "node:path";
1868
+ import { randomUUID } from "node:crypto";
1869
+ import fs62 from "node:fs/promises";
1870
+ import path72 from "node:path";
1871
+ import { promises as fs52 } from "fs";
1855
1872
  import { join as join3 } from "pathe";
1856
1873
  import { z as z7 } from "zod";
1857
1874
  import chokidar from "chokidar";
1858
- import fs62 from "node:fs/promises";
1859
- import { sep, resolve, relative } from "node:path";
1860
1875
  import fs72 from "node:fs/promises";
1861
- import path72 from "node:path";
1876
+ import { sep, resolve, relative } from "node:path";
1877
+ import fs8 from "node:fs/promises";
1862
1878
  import path92 from "node:path";
1863
- import fs9 from "node:fs/promises";
1864
- import { minimatch } from "minimatch";
1865
- import path15 from "node:path";
1866
- import fs12 from "node:fs/promises";
1867
- import path12 from "node:path";
1879
+ import path11 from "node:path";
1868
1880
  import fs10 from "node:fs/promises";
1869
- import path10 from "node:path";
1881
+ import { minimatch } from "minimatch";
1882
+ import path16 from "node:path";
1883
+ import fs13 from "node:fs/promises";
1884
+ import path14 from "node:path";
1870
1885
  import fs11 from "node:fs/promises";
1871
- import path11 from "node:path";
1886
+ import path12 from "node:path";
1887
+ import fs12 from "node:fs/promises";
1888
+ import path13 from "node:path";
1872
1889
  import { randomUUID as randomUUID2 } from "node:crypto";
1873
- import lockfile from "proper-lockfile";
1890
+ import lockfile2 from "proper-lockfile";
1874
1891
  import fs14 from "node:fs/promises";
1875
- import path14 from "node:path";
1892
+ import path15 from "node:path";
1876
1893
  import { simpleGit as simpleGit2 } from "simple-git";
1877
1894
  import { simpleGit as simpleGit3 } from "simple-git";
1878
- import fs13 from "node:fs/promises";
1879
- import path13 from "node:path";
1880
- import lockfile2 from "proper-lockfile";
1881
1895
  import fs15 from "node:fs/promises";
1882
- import path16 from "node:path";
1896
+ import path17 from "node:path";
1883
1897
  import matter2 from "gray-matter";
1884
1898
  import { parse as yamlParse2 } from "yaml";
1885
- function normalizeFilesystemPath(path192) {
1886
- return path192.split(/[\\/]+/).filter(Boolean).join("/");
1899
+ function normalizeFilesystemPath(path202) {
1900
+ return path202.split(/[\\/]+/).filter(Boolean).join("/");
1887
1901
  }
1888
- function hasTraversalSequence(path192) {
1889
- const normalized = normalizeFilesystemPath(path192);
1902
+ function hasTraversalSequence(path202) {
1903
+ const normalized = normalizeFilesystemPath(path202);
1890
1904
  return normalized.includes("..");
1891
1905
  }
1892
1906
  function createLogicalPath(...segments) {
@@ -1896,14 +1910,14 @@ function createLogicalPath(...segments) {
1896
1910
  }
1897
1911
  return normalized;
1898
1912
  }
1899
- function trimSlashes(path192) {
1913
+ function trimSlashes(path202) {
1900
1914
  let start = 0;
1901
- let end = path192.length;
1902
- while (start < end && path192[start] === "/")
1915
+ let end = path202.length;
1916
+ while (start < end && path202[start] === "/")
1903
1917
  start++;
1904
- while (end > start && path192[end - 1] === "/")
1918
+ while (end > start && path202[end - 1] === "/")
1905
1919
  end--;
1906
- return path192.slice(start, end);
1920
+ return path202.slice(start, end);
1907
1921
  }
1908
1922
  function canopyLogWarn2(...args) {
1909
1923
  active2.warn(...args);
@@ -1911,6 +1925,28 @@ function canopyLogWarn2(...args) {
1911
1925
  function canopyLogError(...args) {
1912
1926
  active2.error(...args);
1913
1927
  }
1928
+ function readModeEnv() {
1929
+ if (typeof process === "undefined" || typeof process.env === "undefined")
1930
+ return void 0;
1931
+ const inBrowser = typeof window !== "undefined";
1932
+ const name = inBrowser ? BROWSER_MODE_ENV_VAR : SERVER_MODE_ENV_VAR;
1933
+ const raw = inBrowser ? process.env.NEXT_PUBLIC_CANOPY_MODE : process.env.CANOPY_MODE;
1934
+ const value = raw?.trim();
1935
+ return value ? { name, value } : void 0;
1936
+ }
1937
+ function resolveOperatingMode(configMode) {
1938
+ const env = readModeEnv();
1939
+ if (!env)
1940
+ return configMode;
1941
+ if (env.value !== "prod" && env.value !== "dev") {
1942
+ throw new Error(`CanopyCMS: invalid ${env.name}=${JSON.stringify(env.value)}. It must be exactly "prod" or "dev". Refusing to fall back to the configured mode (${JSON.stringify(configMode)}): a typo here would silently deploy dev auth semantics.`);
1943
+ }
1944
+ if (env.value !== configMode && !warned2) {
1945
+ canopyLogWarn2(`CanopyCMS: ${env.name}="${env.value}" overrides config.mode="${configMode}". The environment wins by design \u2014 it is the per-deployment value, while canopycms.config.ts is shared by local dev, the image build and the deployment. See operating-mode/mode-env.ts.`);
1946
+ warned2 = true;
1947
+ }
1948
+ return env.value;
1949
+ }
1914
1950
  function clientOperatingStrategy(mode) {
1915
1951
  const cached = clientStrategyCache.get(mode);
1916
1952
  if (cached)
@@ -1937,9 +1973,9 @@ function clearClientStrategyCache() {
1937
1973
  function resolveDeploymentName2(config, modeDefault) {
1938
1974
  const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
1939
1975
  const configValue = config.deploymentName;
1940
- if (envValue && configValue && envValue !== configValue && !warned2) {
1976
+ if (envValue && configValue && envValue !== configValue && !warned22) {
1941
1977
  canopyLogWarn2(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
1942
- warned2 = true;
1978
+ warned22 = true;
1943
1979
  }
1944
1980
  const resolved = envValue || configValue || modeDefault;
1945
1981
  if (!isValidDeploymentName2(resolved)) {
@@ -2000,6 +2036,70 @@ async function withLock(key, fn) {
2000
2036
  locks.delete(key);
2001
2037
  }
2002
2038
  }
2039
+ function getErrorMessage3(err) {
2040
+ if (err instanceof Error) {
2041
+ return err.message;
2042
+ }
2043
+ if (typeof err === "string") {
2044
+ return err;
2045
+ }
2046
+ return String(err);
2047
+ }
2048
+ function isNodeError3(err) {
2049
+ return err instanceof Error && "code" in err;
2050
+ }
2051
+ function isNotFoundError3(err) {
2052
+ return isNodeError3(err) && err.code === "ENOENT";
2053
+ }
2054
+ function isFileExistsError2(err) {
2055
+ return isNodeError3(err) && err.code === "EEXIST";
2056
+ }
2057
+ async function acquireProvisioningLock(lockTargetDir2, lockName) {
2058
+ await fs23.mkdir(lockTargetDir2, { recursive: true });
2059
+ return lockfile.lock(lockTargetDir2, {
2060
+ lockfilePath: path22.join(lockTargetDir2, lockName),
2061
+ retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
2062
+ stale: 3e4
2063
+ });
2064
+ }
2065
+ async function tryAcquireProvisioningLock(lockTargetDir2, lockName) {
2066
+ await fs23.mkdir(lockTargetDir2, { recursive: true });
2067
+ return lockfile.lock(lockTargetDir2, {
2068
+ lockfilePath: path22.join(lockTargetDir2, lockName),
2069
+ retries: 0,
2070
+ stale: 3e4
2071
+ });
2072
+ }
2073
+ function lockTargetDir(branchRoot) {
2074
+ return path32.join(path32.resolve(branchRoot), META_DIR);
2075
+ }
2076
+ function tryAcquireContentWriteLock(branchRoot) {
2077
+ return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME);
2078
+ }
2079
+ async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
2080
+ const startedAt = Date.now();
2081
+ for (let attempt = 0; ; attempt++) {
2082
+ try {
2083
+ return await tryAcquireContentWriteLock(branchRoot);
2084
+ } catch (err) {
2085
+ if (!isNodeError3(err) || err.code !== "ELOCKED")
2086
+ throw err;
2087
+ const remaining = waitMs - (Date.now() - startedAt);
2088
+ if (remaining <= 0)
2089
+ throw new ContentWriteLockBusyError();
2090
+ const base = Math.min(40 * 2 ** attempt, 250);
2091
+ await sleep(Math.min(base * (0.5 + Math.random()), remaining));
2092
+ }
2093
+ }
2094
+ }
2095
+ async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
2096
+ const release = await acquireContentWriteLock(branchRoot, waitMs);
2097
+ try {
2098
+ return await fn();
2099
+ } finally {
2100
+ await release();
2101
+ }
2102
+ }
2003
2103
  function flattenGroupFields(fields) {
2004
2104
  const result = [];
2005
2105
  for (const field of fields) {
@@ -2025,24 +2125,6 @@ function generateId() {
2025
2125
  const full = generate();
2026
2126
  return full.substring(0, 12);
2027
2127
  }
2028
- function getErrorMessage3(err) {
2029
- if (err instanceof Error) {
2030
- return err.message;
2031
- }
2032
- if (typeof err === "string") {
2033
- return err;
2034
- }
2035
- return String(err);
2036
- }
2037
- function isNodeError3(err) {
2038
- return err instanceof Error && "code" in err;
2039
- }
2040
- function isNotFoundError3(err) {
2041
- return isNodeError3(err) && err.code === "ENOENT";
2042
- }
2043
- function isFileExistsError2(err) {
2044
- return isNodeError3(err) && err.code === "EEXIST";
2045
- }
2046
2128
  function toLogicalCollectionPath(physicalPath) {
2047
2129
  if (physicalPath === ".")
2048
2130
  return EMPTY_LOGICAL_PATH;
@@ -2067,7 +2149,7 @@ function extractIdFromFilename(filename) {
2067
2149
  }
2068
2150
  async function resolveCollectionPath(root, logicalPath) {
2069
2151
  const fs172 = await import("node:fs/promises");
2070
- const path192 = await import("node:path");
2152
+ const path202 = await import("node:path");
2071
2153
  const segments = logicalPath.split("/").filter(Boolean);
2072
2154
  let currentPath = root;
2073
2155
  for (const segment of segments) {
@@ -2080,7 +2162,7 @@ async function resolveCollectionPath(root, logicalPath) {
2080
2162
  return logicalName === segment.toLowerCase();
2081
2163
  });
2082
2164
  if (matchingDir) {
2083
- currentPath = path192.join(currentPath, matchingDir.name);
2165
+ currentPath = path202.join(currentPath, matchingDir.name);
2084
2166
  } else {
2085
2167
  return null;
2086
2168
  }
@@ -2130,7 +2212,7 @@ function extractSlugFromFilename(filename, entryTypeName) {
2130
2212
  return filename.toLowerCase();
2131
2213
  }
2132
2214
  function registerContentIndexForInvalidation(root, target) {
2133
- const rootKey = path32.resolve(root);
2215
+ const rootKey = path52.resolve(root);
2134
2216
  let refs = registry.get(rootKey);
2135
2217
  if (!refs) {
2136
2218
  refs = /* @__PURE__ */ new Set();
@@ -2141,8 +2223,8 @@ function registerContentIndexForInvalidation(root, target) {
2141
2223
  finalization.register(target, { rootKey, ref });
2142
2224
  }
2143
2225
  function invalidateContentIndexesForRoot(root) {
2144
- const rootKey = path32.resolve(root);
2145
- const prefix = rootKey + path32.sep;
2226
+ const rootKey = path52.resolve(root);
2227
+ const prefix = rootKey + path52.sep;
2146
2228
  for (const [storeRoot, refs] of registry) {
2147
2229
  if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
2148
2230
  continue;
@@ -2162,7 +2244,7 @@ function createDebugLogger2(options) {
2162
2244
  return new DebugLogger2(options);
2163
2245
  }
2164
2246
  function resourceGenerationPath(root, resource) {
2165
- return path42.resolve(root, META_DIR, `${resource}.generation`);
2247
+ return path62.resolve(root, META_DIR2, `${resource}.generation`);
2166
2248
  }
2167
2249
  async function bumpResourceGeneration(root, resource, options) {
2168
2250
  const token = randomUUID();
@@ -2180,7 +2262,7 @@ async function bumpResourceGeneration(root, resource, options) {
2180
2262
  }
2181
2263
  async function readResourceGeneration(root, resource) {
2182
2264
  try {
2183
- const token = await fs32.readFile(resourceGenerationPath(root, resource), "utf-8");
2265
+ const token = await fs42.readFile(resourceGenerationPath(root, resource), "utf-8");
2184
2266
  return { ok: true, token };
2185
2267
  } catch (err) {
2186
2268
  if (isNodeError3(err) && err.code === "ENOENT")
@@ -2200,7 +2282,7 @@ function stripEmbeddedIdFromName(name) {
2200
2282
  async function scanForCollectionMeta(baseDir, relativePath = "") {
2201
2283
  const collections = [];
2202
2284
  try {
2203
- const entries = await fs42.readdir(baseDir, { withFileTypes: true });
2285
+ const entries = await fs52.readdir(baseDir, { withFileTypes: true });
2204
2286
  for (const entry of entries) {
2205
2287
  if (!entry.isDirectory())
2206
2288
  continue;
@@ -2211,8 +2293,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
2211
2293
  const absolutePath = join3(baseDir, folderName);
2212
2294
  const metaPath = join3(absolutePath, ".collection.json");
2213
2295
  try {
2214
- await fs42.access(metaPath);
2215
- const content = await fs42.readFile(metaPath, "utf-8");
2296
+ await fs52.access(metaPath);
2297
+ const content = await fs52.readFile(metaPath, "utf-8");
2216
2298
  const parsed = JSON.parse(content);
2217
2299
  const meta = collectionMetaSchema.parse(parsed);
2218
2300
  collections.push({
@@ -2244,7 +2326,7 @@ async function loadCollectionMetaFiles(contentRoot) {
2244
2326
  let root = null;
2245
2327
  const rootMetaPath = join3(contentRoot, ".collection.json");
2246
2328
  try {
2247
- await fs42.access(rootMetaPath);
2329
+ await fs52.access(rootMetaPath);
2248
2330
  } catch (err) {
2249
2331
  if (err.code === "ENOENT") {
2250
2332
  } else {
@@ -2252,7 +2334,7 @@ async function loadCollectionMetaFiles(contentRoot) {
2252
2334
  }
2253
2335
  }
2254
2336
  try {
2255
- const content = await fs42.readFile(rootMetaPath, "utf-8");
2337
+ const content = await fs52.readFile(rootMetaPath, "utf-8");
2256
2338
  const parsed = JSON.parse(content);
2257
2339
  root = rootCollectionMetaSchema.parse(parsed);
2258
2340
  } catch (err) {
@@ -2342,16 +2424,16 @@ function isValidSchema(schema) {
2342
2424
  async function isStaleByMtime(dir, cachedAt) {
2343
2425
  let entries;
2344
2426
  try {
2345
- entries = await fs52.readdir(dir, { recursive: true, encoding: "utf-8" });
2427
+ entries = await fs62.readdir(dir, { recursive: true, encoding: "utf-8" });
2346
2428
  } catch {
2347
2429
  return true;
2348
2430
  }
2349
2431
  for (const entry of entries) {
2350
2432
  if (!entry.endsWith(".collection.json"))
2351
2433
  continue;
2352
- const full = path52.join(dir, entry);
2434
+ const full = path72.join(dir, entry);
2353
2435
  try {
2354
- const stat = await fs52.stat(full);
2436
+ const stat = await fs62.stat(full);
2355
2437
  if (stat.mtimeMs > cachedAt.getTime())
2356
2438
  return true;
2357
2439
  } catch {
@@ -2374,7 +2456,7 @@ async function invalidateBranchContentCaches(root) {
2374
2456
  }
2375
2457
  async function filePathExists3(filePath) {
2376
2458
  try {
2377
- await fs62.stat(filePath);
2459
+ await fs72.stat(filePath);
2378
2460
  return true;
2379
2461
  } catch (err) {
2380
2462
  if (isNotFoundError3(err))
@@ -2411,11 +2493,11 @@ function resolveBranchPath(options) {
2411
2493
  const safeBranch = sanitizeBranchName(options.branchName);
2412
2494
  const strategy = operatingStrategy2(options.mode);
2413
2495
  const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
2414
- const normalizedBase = path72.resolve(baseRoot);
2415
- const baseWithSep = normalizedBase.endsWith(path72.sep) ? normalizedBase : `${normalizedBase}${path72.sep}`;
2496
+ const normalizedBase = path92.resolve(baseRoot);
2497
+ const baseWithSep = normalizedBase.endsWith(path92.sep) ? normalizedBase : `${normalizedBase}${path92.sep}`;
2416
2498
  const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
2417
2499
  const withinBase = (target) => {
2418
- const resolved = path72.resolve(target);
2500
+ const resolved = path92.resolve(target);
2419
2501
  return resolved === normalizedBase || resolved.startsWith(baseWithSep);
2420
2502
  };
2421
2503
  if (!withinBase(branchRoot)) {
@@ -2425,7 +2507,7 @@ function resolveBranchPath(options) {
2425
2507
  }
2426
2508
  async function ensureBranchRoot(options) {
2427
2509
  const result = resolveBranchPath(options);
2428
- await fs72.mkdir(result.branchRoot, { recursive: true });
2510
+ await fs8.mkdir(result.branchRoot, { recursive: true });
2429
2511
  return result;
2430
2512
  }
2431
2513
  async function withLocks(keys, fn) {
@@ -2508,14 +2590,44 @@ function parseComponentProps(attrString) {
2508
2590
  }
2509
2591
  return props;
2510
2592
  }
2593
+ function isFenceCloseLine(line, marker) {
2594
+ return line.startsWith(marker) && /^\s*$/.test(line.slice(marker.length));
2595
+ }
2596
+ function maskFencedCodeBlocks(body, blocks) {
2597
+ const lines = body.split("\n");
2598
+ const lineCount = lines.length;
2599
+ const nextCloseLine = {
2600
+ "```": new Array(lineCount + 1).fill(-1),
2601
+ "~~~": new Array(lineCount + 1).fill(-1)
2602
+ };
2603
+ for (let i2 = lineCount - 1; i2 >= 0; i2--) {
2604
+ nextCloseLine["```"][i2] = isFenceCloseLine(lines[i2], "```") ? i2 : nextCloseLine["```"][i2 + 1];
2605
+ nextCloseLine["~~~"][i2] = isFenceCloseLine(lines[i2], "~~~") ? i2 : nextCloseLine["~~~"][i2 + 1];
2606
+ }
2607
+ const outLines = [];
2608
+ let i = 0;
2609
+ while (i < lineCount) {
2610
+ const line = lines[i];
2611
+ const marker = line.startsWith("```") ? "```" : line.startsWith("~~~") ? "~~~" : null;
2612
+ const searchFrom = i + 2;
2613
+ const closeIndex = marker && searchFrom < lineCount ? nextCloseLine[marker][searchFrom] : -1;
2614
+ if (!marker || closeIndex === -1) {
2615
+ outLines.push(line);
2616
+ i++;
2617
+ continue;
2618
+ }
2619
+ const block = lines.slice(i, closeIndex + 1).join("\n");
2620
+ const idx = blocks.length;
2621
+ blocks.push(block);
2622
+ outLines.push(`${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`);
2623
+ i = closeIndex + 1;
2624
+ }
2625
+ return outLines.join("\n");
2626
+ }
2511
2627
  function maskCodeBlocks(body) {
2512
2628
  const blocks = [];
2513
2629
  const inlines = [];
2514
- let masked = body.replace(/^(```|~~~).*\n[\s\S]*?\n\1\s*$/gm, (block) => {
2515
- const idx = blocks.length;
2516
- blocks.push(block);
2517
- return `${BLOCK_PREFIX}${idx}${BLOCK_SUFFIX}`;
2518
- });
2630
+ let masked = maskFencedCodeBlocks(body, blocks);
2519
2631
  masked = masked.replace(/``[^`]+``|`[^`]+`/g, (span) => {
2520
2632
  const idx = inlines.length;
2521
2633
  inlines.push(span);
@@ -2626,7 +2738,7 @@ function entryToMarkdown(entry, config) {
2626
2738
  }
2627
2739
  function renderMarkdownEntry(entry, config, skipFields) {
2628
2740
  const parts = [];
2629
- const bodyFieldTypes = /* @__PURE__ */ new Set(["rich-text", "markdown", "mdx"]);
2741
+ const bodyFieldTypes = /* @__PURE__ */ new Set(["markdown", "mdx"]);
2630
2742
  const metadataFields = flattenGroupFields(entry.fields).filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
2631
2743
  for (const field of metadataFields) {
2632
2744
  const value = entry.data[field.name];
@@ -2698,7 +2810,6 @@ ${String(value)}`;
2698
2810
  return `${heading} ${label}${descriptionLine}
2699
2811
 
2700
2812
  ${value ? "Yes" : "No"}`;
2701
- case "rich-text":
2702
2813
  case "markdown":
2703
2814
  case "mdx":
2704
2815
  return `${heading} ${label}${descriptionLine}
@@ -2957,8 +3068,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
2957
3068
  if (slug && slug !== "index") {
2958
3069
  segments.push(slug);
2959
3070
  }
2960
- const path192 = segments.length > 0 ? `/${segments.join("/")}` : "/";
2961
- return path192.toLowerCase();
3071
+ const path202 = segments.length > 0 ? `/${segments.join("/")}` : "/";
3072
+ return path202.toLowerCase();
2962
3073
  }
2963
3074
  function resolveEntryUrl(location, contentRoot) {
2964
3075
  return computeEntryUrl(location.collection ?? "", location.slug ?? "", contentRoot);
@@ -3126,7 +3237,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
3126
3237
  const listed = await store.getCollectionEntryPaths(collection.logicalPath);
3127
3238
  const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
3128
3239
  for (const listEntry of directEntries) {
3129
- const entryTypeName = extractEntryTypeFromFilename(path92.basename(listEntry.relativePath));
3240
+ const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
3130
3241
  if (!entryTypeName)
3131
3242
  continue;
3132
3243
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -3144,7 +3255,7 @@ async function processCollection(store, collection, flatSchema, contentRoot, con
3144
3255
  }
3145
3256
  if (config?.exclude?.where?.(aiEntry))
3146
3257
  continue;
3147
- await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path92.basename(listEntry.relativePath)), config);
3258
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
3148
3259
  entries.push(aiEntry);
3149
3260
  const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
3150
3261
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -3195,7 +3306,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
3195
3306
  const listed = await store.getCollectionEntryPaths(rootCollection.logicalPath);
3196
3307
  const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
3197
3308
  for (const listEntry of directEntries) {
3198
- const entryTypeName = extractEntryTypeFromFilename(path92.basename(listEntry.relativePath));
3309
+ const entryTypeName = extractEntryTypeFromFilename(path11.basename(listEntry.relativePath));
3199
3310
  if (!entryTypeName)
3200
3311
  continue;
3201
3312
  if (config?.exclude?.entryTypes?.includes(entryTypeName))
@@ -3213,7 +3324,7 @@ async function processRootEntries(store, rootCollection, contentRoot, config, id
3213
3324
  }
3214
3325
  if (config?.exclude?.where?.(aiEntry))
3215
3326
  continue;
3216
- await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path92.basename(listEntry.relativePath)), config);
3327
+ await runEntryTransform(aiEntry, doc.absolutePath, extractIdFromFilename(path11.basename(listEntry.relativePath)), config);
3217
3328
  entries.push(aiEntry);
3218
3329
  const entryFilePath = `${listEntry.slug}.md`;
3219
3330
  const entryMarkdown = entryToMarkdown(aiEntry, config);
@@ -3261,19 +3372,19 @@ function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollection
3261
3372
  };
3262
3373
  }
3263
3374
  function makeReadSibling(dir) {
3264
- const resolvedDir = path92.resolve(dir);
3375
+ const resolvedDir = path11.resolve(dir);
3265
3376
  return async (name) => {
3266
- if (!name || name.includes("/") || name.includes("\\") || path92.isAbsolute(name) || hasTraversalSequence(name)) {
3377
+ if (!name || name.includes("/") || name.includes("\\") || path11.isAbsolute(name) || hasTraversalSequence(name)) {
3267
3378
  return null;
3268
3379
  }
3269
- const abs = path92.resolve(resolvedDir, name);
3270
- if (path92.dirname(abs) !== resolvedDir)
3380
+ const abs = path11.resolve(resolvedDir, name);
3381
+ if (path11.dirname(abs) !== resolvedDir)
3271
3382
  return null;
3272
3383
  try {
3273
- const stats = await fs9.lstat(abs);
3384
+ const stats = await fs10.lstat(abs);
3274
3385
  if (!stats.isFile())
3275
3386
  return null;
3276
- return await fs9.readFile(abs, "utf8");
3387
+ return await fs10.readFile(abs, "utf8");
3277
3388
  } catch (err) {
3278
3389
  if (isNodeError3(err) && (err.code === "ENOENT" || err.code === "ENOTDIR"))
3279
3390
  return null;
@@ -3287,7 +3398,7 @@ async function runEntryTransform(entry, absolutePath, contentId, config) {
3287
3398
  return;
3288
3399
  const ctx = {
3289
3400
  contentId: contentId ?? "",
3290
- readSibling: makeReadSibling(path92.dirname(absolutePath))
3401
+ readSibling: makeReadSibling(path11.dirname(absolutePath))
3291
3402
  };
3292
3403
  try {
3293
3404
  const appended = await transform(entry, ctx);
@@ -3328,29 +3439,29 @@ async function writeOccJsonFile(filePath, payload, options) {
3328
3439
  const writeId = randomUUID2();
3329
3440
  const data = { ...payload, version: newVersion, writeId };
3330
3441
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
3331
- const dir = path11.dirname(filePath);
3332
- await fs11.mkdir(dir, { recursive: true });
3442
+ const dir = path13.dirname(filePath);
3443
+ await fs12.mkdir(dir, { recursive: true });
3333
3444
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3334
- await fs11.writeFile(tempPath, content, "utf-8");
3445
+ await fs12.writeFile(tempPath, content, "utf-8");
3335
3446
  if (expectedVersion === null) {
3336
3447
  try {
3337
- await fs11.link(tempPath, filePath);
3448
+ await fs12.link(tempPath, filePath);
3338
3449
  } catch (err) {
3339
- await fs11.unlink(tempPath).catch(() => {
3450
+ await fs12.unlink(tempPath).catch(() => {
3340
3451
  });
3341
3452
  if (isFileExistsError2(err)) {
3342
3453
  throw new OccWriteConflictError();
3343
3454
  }
3344
3455
  throw err;
3345
3456
  }
3346
- await fs11.unlink(tempPath).catch(() => {
3457
+ await fs12.unlink(tempPath).catch(() => {
3347
3458
  });
3348
3459
  return { version: newVersion, writeId };
3349
3460
  }
3350
3461
  try {
3351
3462
  let currentVersion = null;
3352
3463
  try {
3353
- const current = JSON.parse(await fs11.readFile(filePath, "utf-8"));
3464
+ const current = JSON.parse(await fs12.readFile(filePath, "utf-8"));
3354
3465
  currentVersion = current.version ?? 0;
3355
3466
  } catch {
3356
3467
  currentVersion = null;
@@ -3358,16 +3469,16 @@ async function writeOccJsonFile(filePath, payload, options) {
3358
3469
  if (currentVersion !== expectedVersion) {
3359
3470
  throw new OccWriteConflictError();
3360
3471
  }
3361
- await fs11.rename(tempPath, filePath);
3472
+ await fs12.rename(tempPath, filePath);
3362
3473
  if (settleMs > 0) {
3363
3474
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
3364
3475
  }
3365
- const afterWrite = JSON.parse(await fs11.readFile(filePath, "utf-8"));
3476
+ const afterWrite = JSON.parse(await fs12.readFile(filePath, "utf-8"));
3366
3477
  if (afterWrite.writeId !== writeId) {
3367
3478
  throw new OccWriteConflictError();
3368
3479
  }
3369
3480
  } catch (err) {
3370
- await fs11.unlink(tempPath).catch(() => {
3481
+ await fs12.unlink(tempPath).catch(() => {
3371
3482
  });
3372
3483
  throw err;
3373
3484
  }
@@ -3393,13 +3504,13 @@ async function withOccRetry(operation, options) {
3393
3504
  throw new Error("Unreachable");
3394
3505
  }
3395
3506
  async function withOccFileLock(filePath, fn) {
3396
- const dir = path11.dirname(filePath);
3397
- await fs11.mkdir(dir, { recursive: true });
3507
+ const dir = path13.dirname(filePath);
3508
+ await fs12.mkdir(dir, { recursive: true });
3398
3509
  const MAX_ATTEMPTS = 14;
3399
3510
  let release = null;
3400
3511
  for (let attempt = 1; release === null; attempt++) {
3401
3512
  try {
3402
- release = await lockfile.lock(filePath, {
3513
+ release = await lockfile2.lock(filePath, {
3403
3514
  lockfilePath: `${filePath}.lock`,
3404
3515
  realpath: false,
3405
3516
  stale: 1e4,
@@ -3449,6 +3560,9 @@ function isNetworkRemoteUrl(url) {
3449
3560
  return false;
3450
3561
  return COLON_BEFORE_SLASH_PATTERN.test(url);
3451
3562
  }
3563
+ function isMissingRemoteRefFailure(message) {
3564
+ return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
3565
+ }
3452
3566
  async function detectHeadBranch(repoRoot, fallback = "main") {
3453
3567
  try {
3454
3568
  const git = simpleGit3({ baseDir: repoRoot });
@@ -3466,14 +3580,6 @@ async function resolveBaseBranch(options) {
3466
3580
  }
3467
3581
  return "main";
3468
3582
  }
3469
- async function acquireProvisioningLock(lockTargetDir, lockName) {
3470
- await fs13.mkdir(lockTargetDir, { recursive: true });
3471
- return lockfile2.lock(lockTargetDir, {
3472
- lockfilePath: path13.join(lockTargetDir, lockName),
3473
- retries: { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true },
3474
- stale: 3e4
3475
- });
3476
- }
3477
3583
  function gitChildEnv(overrides) {
3478
3584
  const env = {};
3479
3585
  for (const [key, value] of Object.entries(process.env)) {
@@ -3483,8 +3589,8 @@ function gitChildEnv(overrides) {
3483
3589
  return { ...env, ...FORCE_C_LOCALE, ...overrides };
3484
3590
  }
3485
3591
  async function ensureGitExcludePattern(repoPath, pattern) {
3486
- const excludePath = path14.join(repoPath, ".git", "info", "exclude");
3487
- await fs14.mkdir(path14.dirname(excludePath), { recursive: true });
3592
+ const excludePath = path15.join(repoPath, ".git", "info", "exclude");
3593
+ await fs14.mkdir(path15.dirname(excludePath), { recursive: true });
3488
3594
  let content = "";
3489
3595
  try {
3490
3596
  content = await fs14.readFile(excludePath, "utf-8");
@@ -3555,16 +3661,25 @@ async function resolveBranchRoot(config) {
3555
3661
  });
3556
3662
  return context.branchRoot;
3557
3663
  }
3558
- async function listEntries(branchRoot, flatSchema, contentRootName, options) {
3664
+ async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
3559
3665
  const rootPath = options?.rootPath ?? contentRootName;
3560
3666
  const extract = options?.extract;
3561
3667
  const filter = options?.filter;
3562
3668
  const customSort = options?.sort;
3563
3669
  const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
3670
+ const shouldInclude = visibility?.shouldInclude;
3671
+ const skippedFiles = [];
3564
3672
  const collectionResults = await Promise.all(collections.map(async (collection) => {
3565
- const entries = await listCollectionEntries(branchRoot, collection);
3566
- return entries.map((entry) => ({ entry, collection }));
3673
+ const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
3674
+ const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.physicalPath)) : entries;
3675
+ return visible.map((entry) => ({ entry, collection }));
3567
3676
  }));
3677
+ if (isBuildMode() && skippedFiles.length > 0) {
3678
+ const lines = skippedFiles.map(({ filename, collectionPath }) => ` - ${collectionPath}/${filename}`);
3679
+ throw new Error(`CanopyCMS static build: found ${skippedFiles.length} file(s) that look like malformed content entries inside a content collection:
3680
+ ${lines.join("\n")}
3681
+ Expected {type}.{slug}.{id}.{ext} with a known entry type (a name declared in that collection's entries config) and a valid 12-char Base58 ID. This usually means a schema rename without a matching file rename, an entry type declared in one collection but not another, or a stray file placed directly in a collection directory. If this is meant to be a colocated sibling artifact (e.g. an entry's contentId-prefixed data file read via an entryTransforms readSibling call), keep its name to three or fewer dot-separated segments (id.suffix.ext) so it is never mistaken for an entry attempt. Otherwise rename or move the file, or fix the schema, then rebuild.`);
3682
+ }
3568
3683
  const contentPrefix = contentRootName ? `${contentRootName}/` : "";
3569
3684
  const items = [];
3570
3685
  for (const results of collectionResults) {
@@ -3591,7 +3706,8 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options) {
3591
3706
  entryType: entry.entryType,
3592
3707
  format: entry.format,
3593
3708
  data,
3594
- schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
3709
+ schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema,
3710
+ updatedAt: entry.updatedAt
3595
3711
  };
3596
3712
  if (filter && !filter(item))
3597
3713
  continue;
@@ -3644,30 +3760,30 @@ function isValidImageCropValue(value) {
3644
3760
  }
3645
3761
  return isValidCropRect(x, y, w, h);
3646
3762
  }
3647
- function validateImageValue(field, value, path192) {
3763
+ function validateImageValue(field, value, path202) {
3648
3764
  if (!isPlainRecord2(value)) {
3649
- return [{ fieldPath: path192, message: "Expected an image object with { src, alt }" }];
3765
+ return [{ fieldPath: path202, message: "Expected an image object with { src, alt }" }];
3650
3766
  }
3651
3767
  const errors = [];
3652
3768
  if (typeof value.src !== "string" || value.src.trim() === "") {
3653
- errors.push({ fieldPath: `${path192}.src`, message: "Image src is required" });
3769
+ errors.push({ fieldPath: `${path202}.src`, message: "Image src is required" });
3654
3770
  }
3655
3771
  if (typeof value.alt !== "string") {
3656
- errors.push({ fieldPath: `${path192}.alt`, message: "Image alt text is required" });
3772
+ errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
3657
3773
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
3658
- errors.push({ fieldPath: `${path192}.alt`, message: "Image alt text is required" });
3774
+ errors.push({ fieldPath: `${path202}.alt`, message: "Image alt text is required" });
3659
3775
  }
3660
3776
  if (value.width !== void 0 && !isPositiveInt(value.width)) {
3661
- errors.push({ fieldPath: `${path192}.width`, message: "Image width must be a positive integer" });
3777
+ errors.push({ fieldPath: `${path202}.width`, message: "Image width must be a positive integer" });
3662
3778
  }
3663
3779
  if (value.height !== void 0 && !isPositiveInt(value.height)) {
3664
3780
  errors.push({
3665
- fieldPath: `${path192}.height`,
3781
+ fieldPath: `${path202}.height`,
3666
3782
  message: "Image height must be a positive integer"
3667
3783
  });
3668
3784
  }
3669
3785
  if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
3670
- errors.push({ fieldPath: `${path192}.crop`, message: "Invalid image crop rect" });
3786
+ errors.push({ fieldPath: `${path202}.crop`, message: "Invalid image crop rect" });
3671
3787
  }
3672
3788
  return errors;
3673
3789
  }
@@ -3678,44 +3794,44 @@ function referenceValueId(value) {
3678
3794
  return value.id;
3679
3795
  return void 0;
3680
3796
  }
3681
- function validateScalar(field, value, path192) {
3797
+ function validateScalar(field, value, path202) {
3682
3798
  switch (field.type) {
3683
3799
  case "number":
3684
3800
  if (typeof value !== "number" || !Number.isFinite(value)) {
3685
- return [{ fieldPath: path192, message: "Expected a number" }];
3801
+ return [{ fieldPath: path202, message: "Expected a number" }];
3686
3802
  }
3687
3803
  return [];
3688
3804
  case "boolean":
3689
3805
  if (typeof value !== "boolean") {
3690
- return [{ fieldPath: path192, message: "Expected true or false" }];
3806
+ return [{ fieldPath: path202, message: "Expected true or false" }];
3691
3807
  }
3692
3808
  return [];
3693
3809
  case "datetime":
3694
3810
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
3695
- return [{ fieldPath: path192, message: "Expected a valid date/time string" }];
3811
+ return [{ fieldPath: path202, message: "Expected a valid date/time string" }];
3696
3812
  }
3697
3813
  return [];
3698
3814
  case "select": {
3699
3815
  if (typeof value !== "string") {
3700
- return [{ fieldPath: path192, message: "Expected a selection" }];
3816
+ return [{ fieldPath: path202, message: "Expected a selection" }];
3701
3817
  }
3702
3818
  const options = selectOptionValues(field);
3703
3819
  if (value !== "" && !options.includes(value)) {
3704
- return [{ fieldPath: path192, message: `Must be one of: ${options.join(", ")}` }];
3820
+ return [{ fieldPath: path202, message: `Must be one of: ${options.join(", ")}` }];
3705
3821
  }
3706
3822
  return [];
3707
3823
  }
3708
3824
  case "reference":
3709
3825
  if (referenceValueId(value) === void 0) {
3710
- return [{ fieldPath: path192, message: "Expected a reference id" }];
3826
+ return [{ fieldPath: path202, message: "Expected a reference id" }];
3711
3827
  }
3712
3828
  return [];
3713
3829
  case "image":
3714
- return validateImageValue(field, value, path192);
3830
+ return validateImageValue(field, value, path202);
3715
3831
  default:
3716
3832
  if (STRING_FIELD_TYPES.has(field.type)) {
3717
3833
  if (typeof value !== "string") {
3718
- return [{ fieldPath: path192, message: "Expected text" }];
3834
+ return [{ fieldPath: path202, message: "Expected text" }];
3719
3835
  }
3720
3836
  }
3721
3837
  return [];
@@ -3738,12 +3854,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
3738
3854
  errors.push(...validateEntryData(field.fields, data, pathPrefix));
3739
3855
  continue;
3740
3856
  }
3741
- const path192 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
3857
+ const path202 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
3742
3858
  const value = data[field.name];
3743
3859
  const isRequired = "required" in field && field.required === true;
3744
3860
  if (isRequired && isEmptyForRequired(field, value)) {
3745
3861
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
3746
- errors.push({ fieldPath: path192, message });
3862
+ errors.push({ fieldPath: path202, message });
3747
3863
  continue;
3748
3864
  }
3749
3865
  if (value === void 0 || value === null)
@@ -3751,11 +3867,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
3751
3867
  if (field.type === "block") {
3752
3868
  const blockField = field;
3753
3869
  if (!Array.isArray(value)) {
3754
- errors.push({ fieldPath: path192, message: "Expected a list of blocks" });
3870
+ errors.push({ fieldPath: path202, message: "Expected a list of blocks" });
3755
3871
  continue;
3756
3872
  }
3757
3873
  value.forEach((item, index) => {
3758
- const itemPath = `${path192}[${index}]`;
3874
+ const itemPath = `${path202}[${index}]`;
3759
3875
  if (!isPlainRecord2(item)) {
3760
3876
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
3761
3877
  return;
@@ -3777,11 +3893,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
3777
3893
  const objectField = field;
3778
3894
  if (objectField.list) {
3779
3895
  if (!Array.isArray(value)) {
3780
- errors.push({ fieldPath: path192, message: "Expected a list of items" });
3896
+ errors.push({ fieldPath: path202, message: "Expected a list of items" });
3781
3897
  continue;
3782
3898
  }
3783
3899
  value.forEach((item, index) => {
3784
- const itemPath = `${path192}[${index}]`;
3900
+ const itemPath = `${path202}[${index}]`;
3785
3901
  if (!isPlainRecord2(item)) {
3786
3902
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
3787
3903
  return;
@@ -3791,27 +3907,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
3791
3907
  }
3792
3908
  });
3793
3909
  } else if (!isPlainRecord2(value)) {
3794
- errors.push({ fieldPath: path192, message: "Expected an object" });
3910
+ errors.push({ fieldPath: path202, message: "Expected an object" });
3795
3911
  } else if (objectField.fields) {
3796
- errors.push(...validateEntryData(objectField.fields, value, path192));
3912
+ errors.push(...validateEntryData(objectField.fields, value, path202));
3797
3913
  }
3798
3914
  continue;
3799
3915
  }
3800
3916
  if ("list" in field && field.list) {
3801
3917
  if (!Array.isArray(value)) {
3802
- errors.push({ fieldPath: path192, message: "Expected a list of values" });
3918
+ errors.push({ fieldPath: path202, message: "Expected a list of values" });
3803
3919
  continue;
3804
3920
  }
3805
3921
  value.forEach((item, index) => {
3806
- errors.push(...validateScalar(field, item, `${path192}[${index}]`));
3922
+ errors.push(...validateScalar(field, item, `${path202}[${index}]`));
3807
3923
  });
3808
3924
  continue;
3809
3925
  }
3810
3926
  if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
3811
- errors.push({ fieldPath: path192, message: "Expected a single value, not a list" });
3927
+ errors.push({ fieldPath: path202, message: "Expected a single value, not a list" });
3812
3928
  continue;
3813
3929
  }
3814
- errors.push(...validateScalar(field, value, path192));
3930
+ errors.push(...validateScalar(field, value, path202));
3815
3931
  }
3816
3932
  return errors;
3817
3933
  }
@@ -3877,14 +3993,14 @@ async function generateAIContentFiles(options) {
3877
3993
  config: aiConfig2,
3878
3994
  entryLinkUrl: config.entryLinkUrl
3879
3995
  });
3880
- const absoluteOutputDir = path17.resolve(outputDir) + path17.sep;
3996
+ const absoluteOutputDir = path18.resolve(outputDir) + path18.sep;
3881
3997
  let fileCount = 0;
3882
3998
  for (const [filePath, content] of result.files) {
3883
- const absolutePath = path17.resolve(path17.join(absoluteOutputDir, filePath));
3999
+ const absolutePath = path18.resolve(path18.join(absoluteOutputDir, filePath));
3884
4000
  if (!absolutePath.startsWith(absoluteOutputDir)) {
3885
4001
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
3886
4002
  }
3887
- await fs16.mkdir(path17.dirname(absolutePath), { recursive: true });
4003
+ await fs16.mkdir(path18.dirname(absolutePath), { recursive: true });
3888
4004
  await fs16.writeFile(absolutePath, content, "utf-8");
3889
4005
  fileCount++;
3890
4006
  }
@@ -3893,7 +4009,7 @@ async function generateAIContentFiles(options) {
3893
4009
  async function generateAIContentCLI(options) {
3894
4010
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
3895
4011
  console.log("\nCanopyCMS generate-ai-content\n");
3896
- const canopyConfigPath = path18.join(projectDir, "canopycms.config.ts");
4012
+ const canopyConfigPath = path19.join(projectDir, "canopycms.config.ts");
3897
4013
  let canopyConfigModule;
3898
4014
  try {
3899
4015
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -3904,7 +4020,7 @@ async function generateAIContentCLI(options) {
3904
4020
  }
3905
4021
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
3906
4022
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
3907
- const schemasPath = path18.join(projectDir, appDir, "schemas.ts");
4023
+ const schemasPath = path19.join(projectDir, appDir, "schemas.ts");
3908
4024
  let entrySchemaRegistry = {};
3909
4025
  try {
3910
4026
  const schemasModule = await jiti.import(schemasPath);
@@ -3915,7 +4031,7 @@ async function generateAIContentCLI(options) {
3915
4031
  let aiConfig2;
3916
4032
  if (configPath) {
3917
4033
  try {
3918
- const aiConfigModule = await jiti.import(path18.resolve(configPath));
4034
+ const aiConfigModule = await jiti.import(path19.resolve(configPath));
3919
4035
  aiConfig2 = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
3920
4036
  } catch (err) {
3921
4037
  console.error(`Could not load AI config from ${configPath}`);
@@ -3932,7 +4048,7 @@ async function generateAIContentCLI(options) {
3932
4048
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
3933
4049
  process.exit(1);
3934
4050
  }
3935
- const resolvedOutput = path18.resolve(projectDir, outputDir);
4051
+ const resolvedOutput = path19.resolve(projectDir, outputDir);
3936
4052
  console.log(` Output: ${resolvedOutput}`);
3937
4053
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
3938
4054
  const result = await generateAIContentFiles({
@@ -3946,7 +4062,7 @@ async function generateAIContentCLI(options) {
3946
4062
  console.log(` Output: ${result.outputDir}
3947
4063
  `);
3948
4064
  }
3949
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned2, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
4065
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env2, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned22, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, BranchSyncingError, DuplicateContentIdError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3950
4066
  var init_generate_ai_content = __esm({
3951
4067
  "dist/cli/generate-ai-content.js"() {
3952
4068
  "use strict";
@@ -3964,7 +4080,7 @@ var init_generate_ai_content = __esm({
3964
4080
  "use strict";
3965
4081
  }
3966
4082
  });
3967
- init_logger = __esm3({
4083
+ init_logger2 = __esm3({
3968
4084
  "dist/utils/logger.js"() {
3969
4085
  "use strict";
3970
4086
  active2 = console;
@@ -4083,7 +4199,6 @@ var init_generate_ai_content = __esm({
4083
4199
  "number",
4084
4200
  "boolean",
4085
4201
  "datetime",
4086
- "rich-text",
4087
4202
  "markdown",
4088
4203
  "mdx",
4089
4204
  "code"
@@ -4336,11 +4451,21 @@ var init_generate_ai_content = __esm({
4336
4451
  DEFAULT_PROD_WORKSPACE2 = "/mnt/efs/workspace";
4337
4452
  }
4338
4453
  });
4454
+ init_mode_env2 = __esm3({
4455
+ "dist/operating-mode/mode-env.js"() {
4456
+ "use strict";
4457
+ init_logger2();
4458
+ SERVER_MODE_ENV_VAR = "CANOPY_MODE";
4459
+ BROWSER_MODE_ENV_VAR = "NEXT_PUBLIC_CANOPY_MODE";
4460
+ warned2 = false;
4461
+ }
4462
+ });
4339
4463
  init_validation2 = __esm3({
4340
4464
  "dist/config/validation.js"() {
4341
4465
  "use strict";
4342
4466
  init_config3();
4343
4467
  init_flatten2();
4468
+ init_mode_env2();
4344
4469
  forEachReferenceField = (fields, visit) => {
4345
4470
  if (!Array.isArray(fields))
4346
4471
  return;
@@ -4432,8 +4557,8 @@ var init_generate_ai_content = __esm({
4432
4557
  init_deployment_name = __esm3({
4433
4558
  "dist/operating-mode/deployment-name.js"() {
4434
4559
  "use strict";
4435
- init_logger();
4436
- warned2 = false;
4560
+ init_logger2();
4561
+ warned22 = false;
4437
4562
  VALID_DEPLOYMENT_NAME2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
4438
4563
  isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
4439
4564
  }
@@ -4486,32 +4611,32 @@ var init_generate_ai_content = __esm({
4486
4611
  // - shouldCommit(), shouldPush()
4487
4612
  // Add client-unsafe methods (use Node.js APIs)
4488
4613
  getWorkspaceRoot(_sourceRoot) {
4489
- return path62.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
4614
+ return path82.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
4490
4615
  }
4491
4616
  getContentRoot(contentRoot, sourceRoot) {
4492
- return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4617
+ return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
4493
4618
  }
4494
4619
  getContentBranchesRoot(sourceRoot) {
4495
- return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4620
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4496
4621
  }
4497
4622
  getContentBranchRoot(branchName, sourceRoot) {
4498
- return path62.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4623
+ return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4499
4624
  }
4500
4625
  getGitExcludePattern() {
4501
4626
  return ".canopy-meta/";
4502
4627
  }
4503
4628
  getPermissionsFilePath(root) {
4504
- return path62.join(root, this.getPermissionsFileName());
4629
+ return path82.join(root, this.getPermissionsFileName());
4505
4630
  }
4506
4631
  getGroupsFilePath(root) {
4507
- return path62.join(root, this.getGroupsFileName());
4632
+ return path82.join(root, this.getGroupsFileName());
4508
4633
  }
4509
4634
  getRemoteUrlConfig() {
4510
4635
  return {
4511
4636
  shouldAutoInitLocal: false,
4512
4637
  defaultRemotePath: "",
4513
4638
  envVarName: "CANOPYCMS_REMOTE_URL",
4514
- autoDetectRemotePath: path62.join(this.getWorkspaceRoot(), "remote.git")
4639
+ autoDetectRemotePath: path82.join(this.getWorkspaceRoot(), "remote.git")
4515
4640
  };
4516
4641
  }
4517
4642
  requiresExistingRepo() {
@@ -4523,7 +4648,7 @@ var init_generate_ai_content = __esm({
4523
4648
  return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
4524
4649
  }
4525
4650
  getSettingsRoot(sourceRoot) {
4526
- return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
4651
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
4527
4652
  }
4528
4653
  usesSeparateSettingsBranch() {
4529
4654
  return true;
@@ -4540,25 +4665,25 @@ var init_generate_ai_content = __esm({
4540
4665
  DevStrategy2 = class extends DevClientSafeStrategy2 {
4541
4666
  // Inherits client-safe methods from DevClientSafeStrategy
4542
4667
  getWorkspaceRoot(sourceRoot) {
4543
- return path62.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
4668
+ return path82.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
4544
4669
  }
4545
4670
  getContentRoot(contentRoot, sourceRoot) {
4546
- return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4671
+ return path82.resolve(sourceRoot ?? process.cwd(), contentRoot);
4547
4672
  }
4548
4673
  getContentBranchesRoot(sourceRoot) {
4549
- return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4674
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
4550
4675
  }
4551
4676
  getContentBranchRoot(branchName, sourceRoot) {
4552
- return path62.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4677
+ return path82.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
4553
4678
  }
4554
4679
  getGitExcludePattern() {
4555
4680
  return ".canopy-meta/";
4556
4681
  }
4557
4682
  getPermissionsFilePath(root) {
4558
- return path62.join(root, this.getPermissionsFileName());
4683
+ return path82.join(root, this.getPermissionsFileName());
4559
4684
  }
4560
4685
  getGroupsFilePath(root) {
4561
- return path62.join(root, this.getGroupsFileName());
4686
+ return path82.join(root, this.getGroupsFileName());
4562
4687
  }
4563
4688
  getRemoteUrlConfig() {
4564
4689
  return {
@@ -4576,7 +4701,7 @@ var init_generate_ai_content = __esm({
4576
4701
  return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
4577
4702
  }
4578
4703
  getSettingsRoot(sourceRoot) {
4579
- return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
4704
+ return path82.join(this.getWorkspaceRoot(sourceRoot), "settings");
4580
4705
  }
4581
4706
  usesSeparateSettingsBranch() {
4582
4707
  return true;
@@ -4592,11 +4717,15 @@ var init_generate_ai_content = __esm({
4592
4717
  });
4593
4718
  operating_mode_exports = {};
4594
4719
  __export3(operating_mode_exports, {
4720
+ BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
4721
+ SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
4595
4722
  clearClientStrategyCache: () => clearClientStrategyCache,
4596
4723
  clearStrategyCache: () => clearStrategyCache,
4597
4724
  clientOperatingStrategy: () => clientOperatingStrategy,
4725
+ isValidDeploymentName: () => isValidDeploymentName2,
4598
4726
  operatingStrategy: () => operatingStrategy2,
4599
- resolveDeploymentName: () => resolveDeploymentName2
4727
+ resolveDeploymentName: () => resolveDeploymentName2,
4728
+ resolveOperatingMode: () => resolveOperatingMode
4600
4729
  });
4601
4730
  init_operating_mode = __esm3({
4602
4731
  "dist/operating-mode/index.js"() {
@@ -4604,46 +4733,89 @@ var init_generate_ai_content = __esm({
4604
4733
  init_client_safe_strategy();
4605
4734
  init_client_unsafe_strategy();
4606
4735
  init_deployment_name();
4736
+ init_mode_env2();
4607
4737
  }
4608
4738
  });
4609
4739
  locks = /* @__PURE__ */ new Map();
4740
+ META_DIR = ".canopy-meta";
4741
+ CONTENT_WRITE_LOCK_NAME = "content-write.lock";
4742
+ DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
4743
+ ContentWriteLockBusyError = class extends Error {
4744
+ constructor(message = "This branch is busy (syncing with the base branch, or another save is in flight); the change was not saved. Try again in a moment.") {
4745
+ super(message);
4746
+ this.name = "ContentWriteLockBusyError";
4747
+ }
4748
+ };
4749
+ sleep = (ms) => new Promise((resolve2) => {
4750
+ setTimeout(resolve2, ms);
4751
+ });
4610
4752
  init_normalize2();
4611
4753
  BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
4612
4754
  CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
4613
4755
  PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
4614
4756
  isValidId = isValidContentId;
4757
+ init_logger2();
4615
4758
  EMPTY_LOGICAL_PATH = "";
4616
4759
  ContentIdIndex = class {
4617
4760
  constructor(root) {
4618
4761
  this.idToLocation = /* @__PURE__ */ new Map();
4619
4762
  this.pathToId = /* @__PURE__ */ new Map();
4620
4763
  this.byCollection = /* @__PURE__ */ new Map();
4621
- this.root = path22.resolve(root);
4764
+ this.duplicateIds = /* @__PURE__ */ new Map();
4765
+ this.root = path42.resolve(root);
4622
4766
  }
4623
4767
  /**
4624
- * Build index by scanning filenames recursively.
4625
- * Throws if duplicate IDs found (collision detection).
4768
+ * Build index by scanning filenames recursively. Duplicate embedded IDs
4769
+ * are quarantined, not thrown on -- see the class doc comment's
4770
+ * "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
4626
4771
  */
4627
4772
  async buildFromFilenames(startPath = "") {
4628
4773
  await this.scanDirectory(startPath);
4629
4774
  }
4775
+ /** Insert a location into all three indexes. Caller must ensure no existing entry for this id. */
4776
+ insertLocation(id, location) {
4777
+ this.idToLocation.set(id, location);
4778
+ this.pathToId.set(location.relativePath, id);
4779
+ if (location.type === "entry" && location.collection) {
4780
+ if (!this.byCollection.has(location.collection)) {
4781
+ this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
4782
+ }
4783
+ this.byCollection.get(location.collection).add(id);
4784
+ }
4785
+ }
4786
+ /** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
4787
+ evictLocation(location) {
4788
+ this.idToLocation.delete(location.id);
4789
+ this.pathToId.delete(location.relativePath);
4790
+ if (location.type === "entry" && location.collection) {
4791
+ const idSet = this.byCollection.get(location.collection);
4792
+ if (idSet) {
4793
+ idSet.delete(location.id);
4794
+ if (idSet.size === 0)
4795
+ this.byCollection.delete(location.collection);
4796
+ }
4797
+ }
4798
+ }
4799
+ /** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
4800
+ recordDuplicate(id, droppedPath) {
4801
+ let dropped = this.duplicateIds.get(id);
4802
+ if (!dropped) {
4803
+ dropped = /* @__PURE__ */ new Set();
4804
+ this.duplicateIds.set(id, dropped);
4805
+ }
4806
+ dropped.add(droppedPath);
4807
+ }
4630
4808
  async scanDirectory(relativePath) {
4631
- const absoluteDir = path22.join(this.root, relativePath);
4809
+ const absoluteDir = path42.join(this.root, relativePath);
4632
4810
  try {
4633
- const entries = await fs23.readdir(absoluteDir, { withFileTypes: true });
4811
+ const entries = await fs32.readdir(absoluteDir, { withFileTypes: true });
4634
4812
  for (const entry of entries) {
4635
4813
  if (entry.name.startsWith(".") || entry.name === "_ids_") {
4636
4814
  continue;
4637
4815
  }
4638
- const fullRelativePath = path22.join(relativePath, entry.name);
4816
+ const fullRelativePath = path42.join(relativePath, entry.name);
4639
4817
  const id = extractIdFromFilename(entry.name);
4640
4818
  if (id) {
4641
- if (this.idToLocation.has(id)) {
4642
- const existing = this.idToLocation.get(id);
4643
- throw new Error(`ID collision detected: ${id}
4644
- File 1: ${existing.relativePath}
4645
- File 2: ${fullRelativePath}`);
4646
- }
4647
4819
  const location = {
4648
4820
  id,
4649
4821
  // already ContentId from extractIdFromFilename
@@ -4653,17 +4825,27 @@ var init_generate_ai_content = __esm({
4653
4825
  };
4654
4826
  if (!entry.isDirectory()) {
4655
4827
  const slug = extractSlugFromFilename(entry.name);
4656
- const physicalCollection = path22.dirname(fullRelativePath);
4828
+ const physicalCollection = path42.dirname(fullRelativePath);
4657
4829
  const collectionPath = toLogicalCollectionPath(physicalCollection);
4658
4830
  location.slug = slug;
4659
4831
  location.collection = collectionPath;
4660
- if (!this.byCollection.has(collectionPath)) {
4661
- this.byCollection.set(collectionPath, /* @__PURE__ */ new Set());
4832
+ }
4833
+ const existing = this.idToLocation.get(id);
4834
+ if (existing) {
4835
+ const newWins = fullRelativePath < existing.relativePath;
4836
+ const kept = newWins ? fullRelativePath : existing.relativePath;
4837
+ const dropped = newWins ? existing.relativePath : fullRelativePath;
4838
+ if (newWins) {
4839
+ this.evictLocation(existing);
4840
+ this.recordDuplicate(id, existing.relativePath);
4841
+ this.insertLocation(id, location);
4842
+ } else {
4843
+ this.recordDuplicate(id, fullRelativePath);
4662
4844
  }
4663
- this.byCollection.get(collectionPath).add(id);
4845
+ canopyLogWarn2(`[ContentIdIndex] Duplicate content ID ${id}: "${kept}" and "${dropped}" both embed this ID. Keeping "${kept}" for ID-based lookups (reads, references, listings); "${dropped}" is excluded from those lookups for now but has NOT been deleted -- it is still on disk at that path, and saves addressed to it are refused until this is resolved. An admin can resolve this via the repair-content-duplicates admin action for this branch, which archives "${dropped}" with a dot-prefixed name so future scans stop flagging it. Root: ${this.root}`);
4846
+ } else {
4847
+ this.insertLocation(id, location);
4664
4848
  }
4665
- this.idToLocation.set(id, location);
4666
- this.pathToId.set(fullRelativePath, id);
4667
4849
  }
4668
4850
  if (entry.isDirectory()) {
4669
4851
  await this.scanDirectory(fullRelativePath);
@@ -4675,6 +4857,42 @@ var init_generate_ai_content = __esm({
4675
4857
  }
4676
4858
  }
4677
4859
  }
4860
+ /**
4861
+ * IDs discovered on more than one file during the last buildFromFilenames
4862
+ * scan. Empty unless the content tree currently has a duplicate-ID pair
4863
+ * (see the class doc comment). Consumed by branch-health.ts's admin scan
4864
+ * and by the repair-content-duplicates admin action
4865
+ * (api/admin-branch-health.ts).
4866
+ */
4867
+ getDuplicateIds() {
4868
+ const result = [];
4869
+ for (const id of this.duplicateIds.keys()) {
4870
+ const duplicate = this.getDuplicateFor(id);
4871
+ if (duplicate)
4872
+ result.push(duplicate);
4873
+ }
4874
+ return result;
4875
+ }
4876
+ /**
4877
+ * The quarantine record for ONE id, or null when that id is not
4878
+ * duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
4879
+ * write path, which has to ask this question on every save whose resolved
4880
+ * path disagrees with the index (ContentStore.write()'s duplicate-ID
4881
+ * guard) and must not pay for materializing the whole list.
4882
+ */
4883
+ getDuplicateFor(id) {
4884
+ const dropped = this.duplicateIds.get(id);
4885
+ if (!dropped)
4886
+ return null;
4887
+ const kept = this.idToLocation.get(id);
4888
+ if (!kept)
4889
+ return null;
4890
+ return {
4891
+ id,
4892
+ keptPath: kept.relativePath,
4893
+ droppedPaths: Array.from(dropped).sort()
4894
+ };
4895
+ }
4678
4896
  /**
4679
4897
  * Forward lookup: ID → location (O(1))
4680
4898
  */
@@ -4767,7 +4985,7 @@ var init_generate_ai_content = __esm({
4767
4985
  * must already exist on disk (created by ContentStore).
4768
4986
  */
4769
4987
  add(location) {
4770
- const id = extractIdFromFilename(path22.basename(location.relativePath));
4988
+ const id = extractIdFromFilename(path42.basename(location.relativePath));
4771
4989
  if (!id) {
4772
4990
  throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
4773
4991
  }
@@ -4824,8 +5042,8 @@ var init_generate_ai_content = __esm({
4824
5042
  location.relativePath = newRelativePath;
4825
5043
  if (location.type === "entry") {
4826
5044
  const oldCollection = location.collection;
4827
- location.slug = extractSlugFromFilename(path22.basename(newRelativePath));
4828
- const physicalCollection = path22.dirname(newRelativePath);
5045
+ location.slug = extractSlugFromFilename(path42.basename(newRelativePath));
5046
+ const physicalCollection = path42.dirname(newRelativePath);
4829
5047
  location.collection = toLogicalCollectionPath(physicalCollection);
4830
5048
  if (oldCollection !== location.collection) {
4831
5049
  if (oldCollection) {
@@ -4955,8 +5173,8 @@ var init_generate_ai_content = __esm({
4955
5173
  throwOnError: false
4956
5174
  });
4957
5175
  log2 = createDebugLogger2({ prefix: "ResourceGeneration" });
4958
- META_DIR = ".canopy-meta";
4959
- init_logger();
5176
+ META_DIR2 = ".canopy-meta";
5177
+ init_logger2();
4960
5178
  entryTypeMetaSchema = z7.object({
4961
5179
  name: z7.string().min(1),
4962
5180
  format: z7.enum(["md", "mdx", "json", "yaml"]),
@@ -5085,7 +5303,7 @@ var init_generate_ai_content = __esm({
5085
5303
  * the project root, regardless of which entrypoint produced the cwd branchRoot.
5086
5304
  */
5087
5305
  skipDiskCache(branchRoot) {
5088
- return isBuildMode() || path52.resolve(branchRoot) === path52.resolve(process.cwd());
5306
+ return isBuildMode() || path72.resolve(branchRoot) === path72.resolve(process.cwd());
5089
5307
  }
5090
5308
  /**
5091
5309
  * Get schema for a branch (loads from cache or resolves fresh).
@@ -5111,14 +5329,14 @@ var init_generate_ai_content = __esm({
5111
5329
  * Load schema from cache or resolve fresh if cache is missing or stale.
5112
5330
  */
5113
5331
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
5114
- const contentRoot = path52.join(branchRoot, contentRootName);
5332
+ const contentRoot = path72.join(branchRoot, contentRootName);
5115
5333
  const skipDiskCache = this.skipDiskCache(branchRoot);
5116
5334
  if (!skipDiskCache) {
5117
- const cacheDir = path52.join(branchRoot, ".canopy-meta");
5118
- const cachePath = path52.join(cacheDir, "schema-cache.json");
5335
+ const cacheDir = path72.join(branchRoot, ".canopy-meta");
5336
+ const cachePath = path72.join(cacheDir, "schema-cache.json");
5119
5337
  let cacheData = null;
5120
5338
  try {
5121
- const cacheContent = await fs52.readFile(cachePath, "utf-8");
5339
+ const cacheContent = await fs62.readFile(cachePath, "utf-8");
5122
5340
  cacheData = JSON.parse(cacheContent);
5123
5341
  } catch {
5124
5342
  cacheData = null;
@@ -5155,7 +5373,7 @@ var init_generate_ai_content = __esm({
5155
5373
  */
5156
5374
  async resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, options) {
5157
5375
  const { skipDiskCache } = options;
5158
- const contentRoot = path52.join(branchRoot, contentRootName);
5376
+ const contentRoot = path72.join(branchRoot, contentRootName);
5159
5377
  const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
5160
5378
  const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
5161
5379
  if (!isValidSchema(result.schema)) {
@@ -5168,9 +5386,9 @@ var init_generate_ai_content = __esm({
5168
5386
  }
5169
5387
  const flatSchema = flattenSchema(result.schema, contentRootName);
5170
5388
  if (!skipDiskCache) {
5171
- const cacheDir = path52.join(branchRoot, ".canopy-meta");
5172
- const cachePath = path52.join(cacheDir, "schema-cache.json");
5173
- await fs52.unlink(path52.join(cacheDir, "schema-cache.stale")).catch(() => {
5389
+ const cacheDir = path72.join(branchRoot, ".canopy-meta");
5390
+ const cachePath = path72.join(cacheDir, "schema-cache.json");
5391
+ await fs62.unlink(path72.join(cacheDir, "schema-cache.stale")).catch(() => {
5174
5392
  });
5175
5393
  if (read && read.ok) {
5176
5394
  const newCache = {
@@ -5180,13 +5398,13 @@ var init_generate_ai_content = __esm({
5180
5398
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
5181
5399
  generation: read.token
5182
5400
  };
5183
- await fs52.mkdir(cacheDir, { recursive: true });
5184
- const tmpPath = path52.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
5185
- await fs52.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
5401
+ await fs62.mkdir(cacheDir, { recursive: true });
5402
+ const tmpPath = path72.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
5403
+ await fs62.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
5186
5404
  try {
5187
- await fs52.rename(tmpPath, cachePath);
5405
+ await fs62.rename(tmpPath, cachePath);
5188
5406
  } catch (err) {
5189
- await fs52.unlink(tmpPath).catch(() => {
5407
+ await fs62.unlink(tmpPath).catch(() => {
5190
5408
  });
5191
5409
  throw err;
5192
5410
  }
@@ -5267,11 +5485,35 @@ var init_generate_ai_content = __esm({
5267
5485
  }
5268
5486
  };
5269
5487
  ContentConflictError = class extends Error {
5270
- constructor() {
5271
- super("Content was modified by another editor");
5488
+ constructor(message = "Content was modified by another editor") {
5489
+ super(message);
5272
5490
  this.name = "ContentConflictError";
5273
5491
  }
5274
5492
  };
5493
+ BranchSyncingError = class extends ContentConflictError {
5494
+ constructor(message) {
5495
+ super(message);
5496
+ this.name = "BranchSyncingError";
5497
+ }
5498
+ };
5499
+ DuplicateContentIdError = class extends ContentConflictError {
5500
+ constructor(contentId, paths) {
5501
+ const sorted = Array.from(new Set(paths)).sort();
5502
+ super(
5503
+ // Names the STATE, not an action. The repair-content-duplicates
5504
+ // endpoint exists but nothing in the editor renders it, so telling an
5505
+ // editor "an admin can run X" sent them to an admin who could neither
5506
+ // run X nor see that the branch was affected. Say what is true; the
5507
+ // admin panel's read-only duplicate list (SystemHealthPanel) is the
5508
+ // diagnosis half, and the repair UI is tracked in
5509
+ // .claude/future-tasks/duplicate-content-id-repair-ui.md.
5510
+ `Content ID ${contentId} is on more than one file (${sorted.map((p5) => `"${p5}"`).join(" and ")}), so this save was refused rather than risk overwriting or deleting the wrong one. An administrator needs to resolve the duplicate on the server before this entry can be saved.`
5511
+ );
5512
+ this.name = "DuplicateContentIdError";
5513
+ this.contentId = contentId;
5514
+ this.paths = sorted;
5515
+ }
5516
+ };
5275
5517
  DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1e3;
5276
5518
  FORCED_REFRESH_MIN_INTERVAL_MS = 5e3;
5277
5519
  STALE_LOOKUP = Symbol("stale-index-lookup");
@@ -5283,13 +5525,40 @@ var init_generate_ai_content = __esm({
5283
5525
  this.lastDiskProbeMs = 0;
5284
5526
  this.lastForcedRefreshMs = 0;
5285
5527
  this.indexBuild = null;
5286
- this.root = path82.resolve(root);
5528
+ this.root = path10.resolve(root);
5287
5529
  this.contentRootName = options.contentRootName || "content";
5288
5530
  this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
5531
+ this.contentWriteLockWaitMs = options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
5289
5532
  this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
5290
5533
  this._idIndex = new ContentIdIndex(this.root);
5291
5534
  registerContentIndexForInvalidation(this.root, this);
5292
5535
  }
5536
+ /**
5537
+ * [SYNC-C1] Run a working-tree mutation under the branch's cross-host
5538
+ * content-write lock (utils/content-write-lock.ts), on top of the
5539
+ * in-process locks the callee takes for itself.
5540
+ *
5541
+ * The in-process mutex serializes writers inside ONE process; it says
5542
+ * nothing about the EC2 worker rebasing this same tree on shared EFS, which
5543
+ * destroys an in-flight save (`checkout --theirs` overwrites it and the
5544
+ * rebase then reports success; `rebase --abort` hard-resets it). This is the
5545
+ * layer that actually excludes the two.
5546
+ *
5547
+ * Reads deliberately do NOT take it -- an extra EFS round-trip per read is
5548
+ * not an acceptable cost, and reads cannot be destroyed by a rebase.
5549
+ *
5550
+ * Acquisition order is always content lock -> `withLock`, never the reverse,
5551
+ * so the two cannot deadlock.
5552
+ */
5553
+ async withContentWriteExclusion(fn) {
5554
+ try {
5555
+ return await withContentWriteLock(this.root, fn, this.contentWriteLockWaitMs);
5556
+ } catch (err) {
5557
+ if (err instanceof ContentWriteLockBusyError)
5558
+ throw new BranchSyncingError(err.message);
5559
+ throw err;
5560
+ }
5561
+ }
5293
5562
  /**
5294
5563
  * Mark the ID index stale so the next idIndex() access rebuilds it from disk.
5295
5564
  *
@@ -5393,26 +5662,32 @@ var init_generate_ai_content = __esm({
5393
5662
  return true;
5394
5663
  }
5395
5664
  /**
5396
- * Find the file in `dir` whose filename embeds `id`.
5397
- * Returns its root-relative path, or null if no such file (or no such dir).
5665
+ * Every file in `dir` whose filename embeds `id`, as root-relative paths
5666
+ * (empty when there is no such file, or no such dir).
5667
+ *
5668
+ * [F1] Returns ALL matches, not the first: more than one match is a
5669
+ * duplicate-ID pair, and callers must be able to tell that apart from a
5670
+ * clean single hit rather than silently picking whichever one `readdir()`
5671
+ * happened to yield first.
5398
5672
  */
5399
- async findEntryPathById(dir, id) {
5673
+ async findEntryPathsById(dir, id) {
5400
5674
  let entries;
5401
5675
  try {
5402
- entries = await fs8.readdir(dir, { withFileTypes: true });
5676
+ entries = await fs9.readdir(dir, { withFileTypes: true });
5403
5677
  } catch (err) {
5404
5678
  if (isNodeError3(err) && err.code === "ENOENT")
5405
- return null;
5679
+ return [];
5406
5680
  throw err;
5407
5681
  }
5682
+ const matches = [];
5408
5683
  for (const entry of entries) {
5409
5684
  if (entry.isDirectory())
5410
5685
  continue;
5411
5686
  if (extractIdFromFilename(entry.name) === id) {
5412
- return path82.relative(this.root, path82.join(dir, entry.name));
5687
+ matches.push(path10.relative(this.root, path10.join(dir, entry.name)));
5413
5688
  }
5414
5689
  }
5415
- return null;
5690
+ return matches.sort();
5416
5691
  }
5417
5692
  /**
5418
5693
  * Get all schema items for iteration.
@@ -5421,11 +5696,11 @@ var init_generate_ai_content = __esm({
5421
5696
  getSchemaItems() {
5422
5697
  return this.schemaIndex.values();
5423
5698
  }
5424
- assertSchemaItem(path192) {
5425
- const normalized = normalizeFilesystemPath(path192);
5699
+ assertSchemaItem(path202) {
5700
+ const normalized = normalizeFilesystemPath(path202);
5426
5701
  const item = this.schemaIndex.get(normalized);
5427
5702
  if (!item) {
5428
- throw new ContentStoreError(`Unknown schema item: ${path192}`, "NO_SCHEMA_ITEM");
5703
+ throw new ContentStoreError(`Unknown schema item: ${path202}`, "NO_SCHEMA_ITEM");
5429
5704
  }
5430
5705
  return item;
5431
5706
  }
@@ -5508,7 +5783,7 @@ var init_generate_ai_content = __esm({
5508
5783
  * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
5509
5784
  */
5510
5785
  async buildPaths(schemaItem, slug, options = {}) {
5511
- const rootWithSep = this.root.endsWith(path82.sep) ? this.root : `${this.root}${path82.sep}`;
5786
+ const rootWithSep = this.root.endsWith(path10.sep) ? this.root : `${this.root}${path10.sep}`;
5512
5787
  if (schemaItem.type === "entry-type") {
5513
5788
  const parentPath = schemaItem.parentPath || "";
5514
5789
  const parentCollection = this.schemaIndex.get(parentPath);
@@ -5541,7 +5816,7 @@ var init_generate_ai_content = __esm({
5541
5816
  const entryTypeName = entryTypeConfig?.name || "entry";
5542
5817
  let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
5543
5818
  if (!collectionRoot) {
5544
- collectionRoot = path82.resolve(this.root, schemaItem.logicalPath);
5819
+ collectionRoot = path10.resolve(this.root, schemaItem.logicalPath);
5545
5820
  }
5546
5821
  if (!collectionRoot.startsWith(rootWithSep)) {
5547
5822
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
@@ -5551,7 +5826,7 @@ var init_generate_ai_content = __esm({
5551
5826
  let existingEntryType;
5552
5827
  let foundExisting = false;
5553
5828
  if (!id) {
5554
- const entries = await fs8.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5829
+ const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5555
5830
  const existingFile = entries.find((entry) => {
5556
5831
  if (entry.isDirectory())
5557
5832
  return false;
@@ -5577,14 +5852,14 @@ var init_generate_ai_content = __esm({
5577
5852
  }
5578
5853
  filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
5579
5854
  }
5580
- const resolved = path82.resolve(collectionRoot, filename);
5581
- const collectionRootWithSep = collectionRoot.endsWith(path82.sep) ? collectionRoot : `${collectionRoot}${path82.sep}`;
5855
+ const resolved = path10.resolve(collectionRoot, filename);
5856
+ const collectionRootWithSep = collectionRoot.endsWith(path10.sep) ? collectionRoot : `${collectionRoot}${path10.sep}`;
5582
5857
  if (!resolved.startsWith(collectionRootWithSep)) {
5583
5858
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
5584
5859
  }
5585
5860
  return {
5586
5861
  absolutePath: resolved,
5587
- relativePath: path82.relative(this.root, resolved),
5862
+ relativePath: path10.relative(this.root, resolved),
5588
5863
  id,
5589
5864
  entryTypeName: finalEntryTypeName,
5590
5865
  existed
@@ -5620,8 +5895,8 @@ var init_generate_ai_content = __esm({
5620
5895
  async read(collectionPath, slug = "", options = {}) {
5621
5896
  const schemaItem = this.assertSchemaItem(collectionPath);
5622
5897
  const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
5623
- const stat = await fs8.stat(absolutePath);
5624
- const raw = await fs8.readFile(absolutePath, "utf8");
5898
+ const stat = await fs9.stat(absolutePath);
5899
+ const raw = await fs9.readFile(absolutePath, "utf8");
5625
5900
  let doc;
5626
5901
  let format;
5627
5902
  let fields;
@@ -5704,103 +5979,128 @@ var init_generate_ai_content = __esm({
5704
5979
  }
5705
5980
  const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
5706
5981
  let lockKey = this.entryLockKey(schemaItem, slug, prePass);
5707
- const RETRY_KEY = Symbol("retry-with-new-lock-key");
5708
- for (let attempt = 0; attempt < 10; attempt++) {
5709
- const outcome = await withLock(lockKey, async () => {
5710
- const inLock = await this.buildPaths(schemaItem, slug, {
5711
- entryTypeName,
5712
- existingId
5713
- });
5714
- const currentKey = this.entryLockKey(schemaItem, slug, inLock);
5715
- if (currentKey !== lockKey) {
5716
- lockKey = currentKey;
5717
- return RETRY_KEY;
5718
- }
5719
- const { absolutePath, relativePath, id } = inLock;
5720
- await fs8.mkdir(path82.dirname(absolutePath), { recursive: true });
5721
- if (input.expectedVersion !== void 0) {
5722
- try {
5723
- const existing = await fs8.stat(absolutePath);
5724
- if (existing.mtimeMs !== input.expectedVersion) {
5725
- throw new ContentConflictError();
5982
+ return this.withContentWriteExclusion(async () => {
5983
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
5984
+ for (let attempt = 0; attempt < 10; attempt++) {
5985
+ const outcome = await withLock(lockKey, async () => {
5986
+ const inLock = await this.buildPaths(schemaItem, slug, {
5987
+ entryTypeName,
5988
+ existingId
5989
+ });
5990
+ const currentKey = this.entryLockKey(schemaItem, slug, inLock);
5991
+ if (currentKey !== lockKey) {
5992
+ lockKey = currentKey;
5993
+ return RETRY_KEY;
5994
+ }
5995
+ const { absolutePath, relativePath, id } = inLock;
5996
+ if (id) {
5997
+ const guardIndex = this._idIndex;
5998
+ const indexedPath = guardIndex.findById(id)?.relativePath ?? null;
5999
+ if (indexedPath && indexedPath !== relativePath) {
6000
+ const quarantined = guardIndex.getDuplicateFor(id);
6001
+ const bothOnDisk = await filePathExists3(absolutePath) && await filePathExists3(path10.join(this.root, indexedPath));
6002
+ if (quarantined || bothOnDisk) {
6003
+ throw new DuplicateContentIdError(id, [
6004
+ indexedPath,
6005
+ relativePath,
6006
+ ...quarantined?.droppedPaths ?? [],
6007
+ ...quarantined ? [quarantined.keptPath] : []
6008
+ ]);
6009
+ }
5726
6010
  }
5727
- } catch (err) {
5728
- if (err instanceof ContentConflictError)
5729
- throw err;
5730
- if (isNodeError3(err) && err.code === "ENOENT") {
5731
- } else {
5732
- throw err;
6011
+ }
6012
+ await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
6013
+ if (input.expectedVersion !== void 0) {
6014
+ try {
6015
+ const existing = await fs9.stat(absolutePath);
6016
+ if (input.expectedVersion === null) {
6017
+ throw new ContentConflictError("An entry with this slug already exists");
6018
+ }
6019
+ if (existing.mtimeMs !== input.expectedVersion) {
6020
+ throw new ContentConflictError();
6021
+ }
6022
+ } catch (err) {
6023
+ if (err instanceof ContentConflictError)
6024
+ throw err;
6025
+ if (isNodeError3(err) && err.code === "ENOENT") {
6026
+ } else {
6027
+ throw err;
6028
+ }
5733
6029
  }
5734
6030
  }
5735
- }
5736
- if (existingId && !await filePathExists3(absolutePath)) {
5737
- const actualRelPath = await this.findEntryPathById(path82.dirname(absolutePath), existingId);
5738
- const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
5739
- if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
5740
- throw new ContentConflictError();
6031
+ if (existingId && !await filePathExists3(absolutePath)) {
6032
+ const actualRelPaths = await this.findEntryPathsById(path10.dirname(absolutePath), existingId);
6033
+ if (actualRelPaths.length > 1) {
6034
+ throw new DuplicateContentIdError(existingId, actualRelPaths);
6035
+ }
6036
+ const actualRelPath = actualRelPaths[0] ?? null;
6037
+ const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
6038
+ if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
6039
+ throw new ContentConflictError();
6040
+ }
5741
6041
  }
5742
- }
5743
- let content;
5744
- if (input.format === "json") {
5745
- content = `${JSON.stringify(input.data ?? {}, null, 2)}
6042
+ let content;
6043
+ if (input.format === "json") {
6044
+ content = `${JSON.stringify(input.data ?? {}, null, 2)}
5746
6045
  `;
5747
- } else if (input.format === "yaml") {
5748
- content = yamlStringify(input.data ?? {});
5749
- } else {
5750
- content = matter.stringify(input.body, input.data ?? {});
5751
- }
5752
- await atomicWriteFile2(absolutePath, content);
5753
- const liveIndex = this._idIndex;
5754
- let staleOldAbsPath = null;
5755
- if (id) {
5756
- const existing = liveIndex.findById(id);
5757
- if (existing) {
5758
- if (existing.relativePath !== relativePath) {
5759
- staleOldAbsPath = path82.join(this.root, existing.relativePath);
5760
- liveIndex.updatePath(existing.id, relativePath);
5761
- }
6046
+ } else if (input.format === "yaml") {
6047
+ content = yamlStringify(input.data ?? {});
5762
6048
  } else {
5763
- liveIndex.add({
5764
- type: "entry",
5765
- relativePath,
5766
- collection: collectionPath,
5767
- slug: slug || void 0
6049
+ content = matter.stringify(input.body, input.data ?? {});
6050
+ }
6051
+ await atomicWriteFile2(absolutePath, content);
6052
+ const liveIndex = this._idIndex;
6053
+ let staleOldAbsPath = null;
6054
+ if (id) {
6055
+ const existing = liveIndex.findById(id);
6056
+ if (existing) {
6057
+ if (existing.relativePath !== relativePath) {
6058
+ staleOldAbsPath = path10.join(this.root, existing.relativePath);
6059
+ liveIndex.updatePath(existing.id, relativePath);
6060
+ }
6061
+ } else {
6062
+ liveIndex.add({
6063
+ type: "entry",
6064
+ relativePath,
6065
+ collection: collectionPath,
6066
+ slug: slug || void 0
6067
+ });
6068
+ }
6069
+ }
6070
+ if (staleOldAbsPath) {
6071
+ await fs9.unlink(staleOldAbsPath).catch((err) => {
6072
+ if (!isNodeError3(err) || err.code !== "ENOENT")
6073
+ throw err;
5768
6074
  });
5769
6075
  }
5770
- }
5771
- if (staleOldAbsPath) {
5772
- await fs8.unlink(staleOldAbsPath).catch((err) => {
5773
- if (!isNodeError3(err) || err.code !== "ENOENT")
5774
- throw err;
5775
- });
5776
- }
5777
- await this.recordOwnMutation(liveIndex);
5778
- const afterStat = await fs8.stat(absolutePath);
5779
- const base = {
5780
- collection: schemaItem.logicalPath,
5781
- collectionName: schemaItem.name,
5782
- relativePath,
5783
- absolutePath,
5784
- version: afterStat.mtimeMs
5785
- };
5786
- if (input.format === "json") {
5787
- return { ...base, format: "json", data: input.data ?? {} };
5788
- }
5789
- if (input.format === "yaml") {
5790
- return { ...base, format: "yaml", data: input.data ?? {} };
5791
- }
5792
- return {
5793
- ...base,
5794
- format: input.format,
5795
- data: input.data ?? {},
5796
- body: input.body,
5797
- bodyFieldName: findBodyFieldName(fields)
5798
- };
5799
- });
5800
- if (typeof outcome !== "symbol")
5801
- return outcome;
5802
- }
5803
- throw new ContentConflictError();
6076
+ await this.recordOwnMutation(liveIndex);
6077
+ const afterStat = await fs9.stat(absolutePath);
6078
+ const base = {
6079
+ collection: schemaItem.logicalPath,
6080
+ collectionName: schemaItem.name,
6081
+ relativePath,
6082
+ absolutePath,
6083
+ version: afterStat.mtimeMs
6084
+ };
6085
+ if (input.format === "json") {
6086
+ return { ...base, format: "json", data: input.data ?? {} };
6087
+ }
6088
+ if (input.format === "yaml") {
6089
+ return { ...base, format: "yaml", data: input.data ?? {} };
6090
+ }
6091
+ return {
6092
+ ...base,
6093
+ format: input.format,
6094
+ data: input.data ?? {},
6095
+ body: input.body,
6096
+ bodyFieldName: findBodyFieldName(fields)
6097
+ };
6098
+ });
6099
+ if (typeof outcome !== "symbol")
6100
+ return outcome;
6101
+ }
6102
+ throw new ContentConflictError();
6103
+ });
5804
6104
  }
5805
6105
  /**
5806
6106
  * Read an entry by its ID (UUID).
@@ -5852,7 +6152,7 @@ var init_generate_ai_content = __esm({
5852
6152
  const schemaItem = this.assertSchemaItem(collectionPath);
5853
6153
  const { absolutePath } = await this.buildPaths(schemaItem, slug);
5854
6154
  try {
5855
- await fs8.access(absolutePath);
6155
+ await fs9.access(absolutePath);
5856
6156
  return true;
5857
6157
  } catch (err) {
5858
6158
  if (isNodeError3(err) && err.code === "ENOENT")
@@ -5891,7 +6191,7 @@ var init_generate_ai_content = __esm({
5891
6191
  const collectionRoot = await resolveCollectionPath(this.root, collectionPath);
5892
6192
  if (!collectionRoot)
5893
6193
  return 0;
5894
- const entries = await fs8.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
6194
+ const entries = await fs9.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
5895
6195
  return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
5896
6196
  }
5897
6197
  /**
@@ -5915,34 +6215,36 @@ var init_generate_ai_content = __esm({
5915
6215
  const collection = this.assertCollection(collectionPath);
5916
6216
  const prePass = await this.buildPaths(collection, slug);
5917
6217
  if (!prePass.existed) {
5918
- await fs8.unlink(prePass.absolutePath);
6218
+ await fs9.unlink(prePass.absolutePath);
5919
6219
  return;
5920
6220
  }
5921
6221
  let lockKey = this.entryLockKey(collection, slug, prePass);
5922
6222
  await this.afterPrePassForTesting();
5923
- const RETRY_KEY = Symbol("retry-with-new-lock-key");
5924
- for (let attempt = 0; attempt < 10; attempt++) {
5925
- const outcome = await withLock(lockKey, async () => {
5926
- const inLock = await this.buildPaths(collection, slug);
5927
- const currentKey = this.entryLockKey(collection, slug, inLock);
5928
- if (currentKey !== lockKey) {
5929
- lockKey = currentKey;
5930
- return RETRY_KEY;
5931
- }
5932
- const { absolutePath, relativePath } = inLock;
5933
- await fs8.unlink(absolutePath);
5934
- const liveIndex = this._idIndex;
5935
- const id = liveIndex.findByPath(relativePath);
5936
- if (id) {
5937
- liveIndex.remove(id);
5938
- }
5939
- await this.recordOwnMutation(liveIndex);
5940
- return void 0;
5941
- });
5942
- if (outcome !== RETRY_KEY)
5943
- return;
5944
- }
5945
- throw new ContentConflictError();
6223
+ return this.withContentWriteExclusion(async () => {
6224
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
6225
+ for (let attempt = 0; attempt < 10; attempt++) {
6226
+ const outcome = await withLock(lockKey, async () => {
6227
+ const inLock = await this.buildPaths(collection, slug);
6228
+ const currentKey = this.entryLockKey(collection, slug, inLock);
6229
+ if (currentKey !== lockKey) {
6230
+ lockKey = currentKey;
6231
+ return RETRY_KEY;
6232
+ }
6233
+ const { absolutePath, relativePath } = inLock;
6234
+ await fs9.unlink(absolutePath);
6235
+ const liveIndex = this._idIndex;
6236
+ const id = liveIndex.findByPath(relativePath);
6237
+ if (id) {
6238
+ liveIndex.remove(id);
6239
+ }
6240
+ await this.recordOwnMutation(liveIndex);
6241
+ return void 0;
6242
+ });
6243
+ if (outcome !== RETRY_KEY)
6244
+ return;
6245
+ }
6246
+ throw new ContentConflictError();
6247
+ });
5946
6248
  }
5947
6249
  /**
5948
6250
  * Rename an entry by changing its slug (middle segment of filename).
@@ -5973,70 +6275,72 @@ var init_generate_ai_content = __esm({
5973
6275
  let sourceId = prePass.id;
5974
6276
  let destLockKey = this.createLockKey(collection, safeNewSlug);
5975
6277
  await this.afterPrePassForTesting();
5976
- const RETRY_KEY = Symbol("retry-with-new-lock-key");
5977
- for (let attempt = 0; attempt < 10; attempt++) {
5978
- const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
5979
- const inLock = await this.buildPaths(collection, currentSlug);
5980
- if (!inLock.existed) {
5981
- throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
5982
- }
5983
- const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
5984
- const currentDestKey = this.createLockKey(collection, safeNewSlug);
5985
- if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
5986
- sourceLockKey = currentSourceKey;
6278
+ return this.withContentWriteExclusion(async () => {
6279
+ const RETRY_KEY = Symbol("retry-with-new-lock-key");
6280
+ for (let attempt = 0; attempt < 10; attempt++) {
6281
+ const outcome = await withLocks([sourceLockKey, destLockKey], async () => {
6282
+ const inLock = await this.buildPaths(collection, currentSlug);
6283
+ if (!inLock.existed) {
6284
+ throw new ContentStoreError(`Entry not found: ${currentSlug}`, "NOT_FOUND");
6285
+ }
6286
+ const currentSourceKey = this.entryLockKey(collection, currentSlug, inLock);
6287
+ const currentDestKey = this.createLockKey(collection, safeNewSlug);
6288
+ if (currentSourceKey !== sourceLockKey || inLock.id !== sourceId) {
6289
+ sourceLockKey = currentSourceKey;
6290
+ destLockKey = currentDestKey;
6291
+ sourceId = inLock.id;
6292
+ return RETRY_KEY;
6293
+ }
5987
6294
  destLockKey = currentDestKey;
5988
- sourceId = inLock.id;
5989
- return RETRY_KEY;
5990
- }
5991
- destLockKey = currentDestKey;
5992
- const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
5993
- const currentFilename = path82.basename(currentPath);
5994
- const parts = currentFilename.split(".");
5995
- if (parts.length < 4) {
5996
- throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
5997
- }
5998
- const entryTypeName = parts[0];
5999
- const contentId = parts[parts.length - 2];
6000
- const ext = `.${parts[parts.length - 1]}`;
6001
- const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
6002
- const parentDir = path82.dirname(currentPath);
6003
- const newPath = path82.join(parentDir, newFilename);
6004
- try {
6005
- const entries = await fs8.readdir(parentDir, { withFileTypes: true });
6006
- for (const entry of entries) {
6007
- if (entry.isDirectory())
6008
- continue;
6009
- const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
6010
- if (existingSlug === safeNewSlug) {
6011
- throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6295
+ const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
6296
+ const currentFilename = path10.basename(currentPath);
6297
+ const parts = currentFilename.split(".");
6298
+ if (parts.length < 4) {
6299
+ throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
6300
+ }
6301
+ const entryTypeName = parts[0];
6302
+ const contentId = parts[parts.length - 2];
6303
+ const ext = `.${parts[parts.length - 1]}`;
6304
+ const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
6305
+ const parentDir = path10.dirname(currentPath);
6306
+ const newPath = path10.join(parentDir, newFilename);
6307
+ try {
6308
+ const entries = await fs9.readdir(parentDir, { withFileTypes: true });
6309
+ for (const entry of entries) {
6310
+ if (entry.isDirectory())
6311
+ continue;
6312
+ const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
6313
+ if (existingSlug === safeNewSlug) {
6314
+ throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6315
+ }
6012
6316
  }
6317
+ } catch (err) {
6318
+ if (err instanceof ContentStoreError)
6319
+ throw err;
6013
6320
  }
6014
- } catch (err) {
6015
- if (err instanceof ContentStoreError)
6321
+ try {
6322
+ await fs9.link(currentPath, newPath);
6323
+ } catch (err) {
6324
+ if (isNodeError3(err) && err.code === "EEXIST") {
6325
+ throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6326
+ }
6016
6327
  throw err;
6017
- }
6018
- try {
6019
- await fs8.link(currentPath, newPath);
6020
- } catch (err) {
6021
- if (isNodeError3(err) && err.code === "EEXIST") {
6022
- throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`, "VALIDATION");
6023
6328
  }
6024
- throw err;
6025
- }
6026
- await fs8.unlink(currentPath);
6027
- const newRelativePath = path82.relative(this.root, newPath);
6028
- const liveIndex = this._idIndex;
6029
- const entryId = liveIndex.findByPath(currentRelPath);
6030
- if (entryId) {
6031
- liveIndex.updatePath(entryId, newRelativePath);
6032
- }
6033
- await this.recordOwnMutation(liveIndex);
6034
- return { newPath: `${collectionPath}/${safeNewSlug}` };
6035
- });
6036
- if (typeof outcome !== "symbol")
6037
- return outcome;
6038
- }
6039
- throw new ContentConflictError();
6329
+ await fs9.unlink(currentPath);
6330
+ const newRelativePath = path10.relative(this.root, newPath);
6331
+ const liveIndex = this._idIndex;
6332
+ const entryId = liveIndex.findByPath(currentRelPath);
6333
+ if (entryId) {
6334
+ liveIndex.updatePath(entryId, newRelativePath);
6335
+ }
6336
+ await this.recordOwnMutation(liveIndex);
6337
+ return { newPath: `${collectionPath}/${safeNewSlug}` };
6338
+ });
6339
+ if (typeof outcome !== "symbol")
6340
+ return outcome;
6341
+ }
6342
+ throw new ContentConflictError();
6343
+ });
6040
6344
  }
6041
6345
  /**
6042
6346
  * List all entries in a collection tree (including subcollections).
@@ -6211,7 +6515,7 @@ var init_generate_ai_content = __esm({
6211
6515
  log22 = createDebugLogger2({ prefix: "EntryLinks" });
6212
6516
  BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
6213
6517
  ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
6214
- init_logger();
6518
+ init_logger2();
6215
6519
  log3 = createDebugLogger2({ prefix: "BranchRegistry" });
6216
6520
  REGISTRY_FILE = "branches.json";
6217
6521
  REGISTRY_STALE_FILE = "branches.stale.json";
@@ -6223,10 +6527,10 @@ var init_generate_ai_content = __esm({
6223
6527
  constructor(root) {
6224
6528
  this.regenInFlight = null;
6225
6529
  this.lastForcedRefreshMs = 0;
6226
- this.root = path10.resolve(root);
6227
- this.registryPath = path10.join(this.root, REGISTRY_FILE);
6228
- this.stalePath = path10.join(this.root, REGISTRY_STALE_FILE);
6229
- this.tempPath = path10.join(this.root, REGISTRY_TEMP_FILE);
6530
+ this.root = path12.resolve(root);
6531
+ this.registryPath = path12.join(this.root, REGISTRY_FILE);
6532
+ this.stalePath = path12.join(this.root, REGISTRY_STALE_FILE);
6533
+ this.tempPath = path12.join(this.root, REGISTRY_TEMP_FILE);
6230
6534
  }
6231
6535
  /**
6232
6536
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -6235,7 +6539,7 @@ var init_generate_ai_content = __esm({
6235
6539
  async list() {
6236
6540
  let parsed;
6237
6541
  try {
6238
- const raw = await fs10.readFile(this.registryPath, "utf8");
6542
+ const raw = await fs11.readFile(this.registryPath, "utf8");
6239
6543
  parsed = JSON.parse(raw);
6240
6544
  } catch (err) {
6241
6545
  if (isNotFoundError3(err)) {
@@ -6324,7 +6628,7 @@ var init_generate_ai_content = __esm({
6324
6628
  async performRegenerate() {
6325
6629
  const read = await readResourceGeneration(this.root, RESOURCE2);
6326
6630
  const branches = await this.scanBranchDirectories();
6327
- await fs10.unlink(this.stalePath).catch(() => {
6631
+ await fs11.unlink(this.stalePath).catch(() => {
6328
6632
  });
6329
6633
  if (!read.ok) {
6330
6634
  return branches;
@@ -6335,12 +6639,12 @@ var init_generate_ai_content = __esm({
6335
6639
  generation: read.token
6336
6640
  };
6337
6641
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
6338
- await fs10.mkdir(this.root, { recursive: true });
6339
- await fs10.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
6642
+ await fs11.mkdir(this.root, { recursive: true });
6643
+ await fs11.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
6340
6644
  try {
6341
- await fs10.rename(uniqueTempPath, this.registryPath);
6645
+ await fs11.rename(uniqueTempPath, this.registryPath);
6342
6646
  } catch (err) {
6343
- await fs10.unlink(uniqueTempPath).catch(() => {
6647
+ await fs11.unlink(uniqueTempPath).catch(() => {
6344
6648
  });
6345
6649
  throw err;
6346
6650
  }
@@ -6354,12 +6658,12 @@ var init_generate_ai_content = __esm({
6354
6658
  async scanBranchDirectories() {
6355
6659
  const branches = [];
6356
6660
  try {
6357
- const entries = await fs10.readdir(this.root, { withFileTypes: true });
6661
+ const entries = await fs11.readdir(this.root, { withFileTypes: true });
6358
6662
  for (const entry of entries) {
6359
6663
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
6360
6664
  continue;
6361
6665
  }
6362
- const branchRoot = path10.join(this.root, entry.name);
6666
+ const branchRoot = path12.join(this.root, entry.name);
6363
6667
  let meta;
6364
6668
  try {
6365
6669
  meta = await BranchMetadataFileManager.loadOnly(branchRoot);
@@ -6410,8 +6714,8 @@ var init_generate_ai_content = __esm({
6410
6714
  };
6411
6715
  BranchMetadataFileManager = class _BranchMetadataFileManager {
6412
6716
  constructor(branchRoot, baseRoot, options) {
6413
- this.branchRoot = path12.resolve(branchRoot);
6414
- this.filePath = path12.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6717
+ this.branchRoot = path14.resolve(branchRoot);
6718
+ this.filePath = path14.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6415
6719
  this.baseRoot = baseRoot;
6416
6720
  this.settleMs = options?.settleMs;
6417
6721
  }
@@ -6420,10 +6724,10 @@ var init_generate_ai_content = __esm({
6420
6724
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
6421
6725
  */
6422
6726
  static async loadOnly(branchRoot) {
6423
- const resolvedRoot = path12.resolve(branchRoot);
6424
- const filePath = path12.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6727
+ const resolvedRoot = path14.resolve(branchRoot);
6728
+ const filePath = path14.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
6425
6729
  try {
6426
- const raw = await fs12.readFile(filePath, "utf8");
6730
+ const raw = await fs13.readFile(filePath, "utf8");
6427
6731
  return JSON.parse(raw);
6428
6732
  } catch (err) {
6429
6733
  if (isNotFoundError3(err)) {
@@ -6444,7 +6748,7 @@ var init_generate_ai_content = __esm({
6444
6748
  }
6445
6749
  async load() {
6446
6750
  try {
6447
- const raw = await fs12.readFile(this.filePath, "utf8");
6751
+ const raw = await fs13.readFile(this.filePath, "utf8");
6448
6752
  const parsed = JSON.parse(raw);
6449
6753
  const version = parsed.version ?? 0;
6450
6754
  return { meta: parsed, version };
@@ -6506,7 +6810,7 @@ var init_generate_ai_content = __esm({
6506
6810
  */
6507
6811
  async save(incoming) {
6508
6812
  try {
6509
- await fs12.stat(this.branchRoot);
6813
+ await fs13.stat(this.branchRoot);
6510
6814
  } catch (err) {
6511
6815
  if (isNotFoundError3(err)) {
6512
6816
  throw new BranchMetadataConflictError("Branch no longer exists");
@@ -6595,6 +6899,7 @@ var init_generate_ai_content = __esm({
6595
6899
  LEADING_OPTION_PATTERN = /^-/;
6596
6900
  WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
6597
6901
  COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
6902
+ MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
6598
6903
  log5 = createDebugLogger2({ prefix: "GitManager" });
6599
6904
  GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
6600
6905
  FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
@@ -6607,14 +6912,23 @@ var init_generate_ai_content = __esm({
6607
6912
  this.name = "GitConflictError";
6608
6913
  }
6609
6914
  };
6915
+ GitRemoteRefMissingError = class extends Error {
6916
+ constructor(branch, remote, gitError) {
6917
+ super(`Remote '${remote}' has no ref for branch '${branch}' yet`);
6918
+ this.branch = branch;
6919
+ this.remote = remote;
6920
+ this.gitError = gitError;
6921
+ this.name = "GitRemoteRefMissingError";
6922
+ }
6923
+ };
6610
6924
  GitManager = class _GitManager {
6611
6925
  constructor(options, gitOptions) {
6612
- this.repoPath = path14.resolve(options.repoPath);
6926
+ this.repoPath = path15.resolve(options.repoPath);
6613
6927
  this.baseBranch = options.baseBranch ?? "main";
6614
6928
  this.remote = options.remote ?? "origin";
6615
6929
  this.skipIndexMarker = options.skipIndexMarker ?? false;
6616
6930
  this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
6617
- this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(this.repoPath) }));
6931
+ this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(this.repoPath) }));
6618
6932
  }
6619
6933
  static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
6620
6934
  log5.debug("git", "Cloning repository", {
@@ -6656,7 +6970,7 @@ var init_generate_ai_content = __esm({
6656
6970
  remotePath: options.remotePath,
6657
6971
  baseBranch: options.baseBranch
6658
6972
  });
6659
- const remoteParent = path14.dirname(options.remotePath);
6973
+ const remoteParent = path15.dirname(options.remotePath);
6660
6974
  releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
6661
6975
  let remoteExists = false;
6662
6976
  try {
@@ -6762,8 +7076,8 @@ var init_generate_ai_content = __esm({
6762
7076
  * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
6763
7077
  * task on stderr output, and `--quiet` suppresses exactly that).
6764
7078
  * `--end-of-options` guards the ref-name positionals the same way
6765
- * `push`/`forcePush` below do, since `branch` here can be a sanitized but
6766
- * otherwise caller-influenced string.
7079
+ * `push` below does, since `branch` here can be a sanitized but otherwise
7080
+ * caller-influenced string.
6767
7081
  *
6768
7082
  * A failure here therefore means the remote itself is unreadable and is
6769
7083
  * surfaced, NOT treated as "branch absent" — that would route
@@ -6912,7 +7226,7 @@ var init_generate_ai_content = __esm({
6912
7226
  */
6913
7227
  static async validateGitRepoExists(repoPath) {
6914
7228
  try {
6915
- const stat = await fs14.stat(path14.join(repoPath, ".git"));
7229
+ const stat = await fs14.stat(path15.join(repoPath, ".git"));
6916
7230
  if (!stat.isDirectory()) {
6917
7231
  throw new Error(`Expected git repo at ${repoPath}`);
6918
7232
  }
@@ -7003,8 +7317,8 @@ var init_generate_ai_content = __esm({
7003
7317
  if (config.shouldAutoInitLocal) {
7004
7318
  const gitRoot = await this.findGitRoot();
7005
7319
  const sourceRoot = options.sourceRoot;
7006
- const sourcePath = sourceRoot ? path14.resolve(gitRoot, sourceRoot) : gitRoot;
7007
- const localRemotePath = path14.join(sourcePath, config.defaultRemotePath);
7320
+ const sourcePath = sourceRoot ? path15.resolve(gitRoot, sourceRoot) : gitRoot;
7321
+ const localRemotePath = path15.join(sourcePath, config.defaultRemotePath);
7008
7322
  await this.ensureLocalSimulatedRemote({
7009
7323
  remotePath: localRemotePath,
7010
7324
  sourcePath: gitRoot,
@@ -7032,7 +7346,7 @@ var init_generate_ai_content = __esm({
7032
7346
  static async repoExistsAt(workspacePath) {
7033
7347
  try {
7034
7348
  const checkGit = simpleGit2({ baseDir: workspacePath });
7035
- checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(workspacePath) }));
7349
+ checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(workspacePath) }));
7036
7350
  await checkGit.raw(["rev-parse", "--git-dir"]);
7037
7351
  return true;
7038
7352
  } catch {
@@ -7054,12 +7368,12 @@ var init_generate_ai_content = __esm({
7054
7368
  const baseBranch = await resolveBaseBranch({
7055
7369
  defaultBaseBranch: options.baseBranch,
7056
7370
  mode: options.mode,
7057
- detectFrom: options.sourceRoot ? path14.resolve(process.cwd(), options.sourceRoot) : process.cwd()
7371
+ detectFrom: options.sourceRoot ? path15.resolve(process.cwd(), options.sourceRoot) : process.cwd()
7058
7372
  });
7059
7373
  const remoteName = options.remoteName ?? "origin";
7060
7374
  const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
7061
7375
  if (!repoExists) {
7062
- const gitPath = path14.join(options.workspacePath, ".git");
7376
+ const gitPath = path15.join(options.workspacePath, ".git");
7063
7377
  try {
7064
7378
  const stat = await fs14.stat(gitPath);
7065
7379
  if (stat.isDirectory()) {
@@ -7093,7 +7407,7 @@ var init_generate_ai_content = __esm({
7093
7407
  }
7094
7408
  justCloned = true;
7095
7409
  const freshGit = simpleGit2({ baseDir: options.workspacePath });
7096
- freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(options.workspacePath) }));
7410
+ freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path15.dirname(options.workspacePath) }));
7097
7411
  await freshGit.addConfig("canopycms.managed", "true");
7098
7412
  await freshGit.addConfig("user.name", options.gitBotAuthorName);
7099
7413
  await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
@@ -7236,9 +7550,16 @@ var init_generate_ai_content = __esm({
7236
7550
  async pullCurrentBranchInner() {
7237
7551
  const branches = await this.git.branch();
7238
7552
  const currentBranch = branches.current;
7239
- await this.git.fetch(this.remote, currentBranch);
7240
7553
  try {
7241
- await this.git.merge([`${this.remote}/${currentBranch}`]);
7554
+ await this.git.fetch(this.remote, currentBranch);
7555
+ } catch (err) {
7556
+ if (!isMissingRemoteRefFailure(getErrorMessage3(err)))
7557
+ throw err;
7558
+ throw new GitRemoteRefMissingError(currentBranch, this.remote, err);
7559
+ }
7560
+ const fetchedTip = (await this.git.revparse(["FETCH_HEAD"])).trim();
7561
+ try {
7562
+ await this.git.merge([fetchedTip]);
7242
7563
  } catch (err) {
7243
7564
  try {
7244
7565
  const status = await this.git.status();
@@ -7300,6 +7621,44 @@ var init_generate_ai_content = __esm({
7300
7621
  `${target}:${target}`
7301
7622
  ]);
7302
7623
  }
7624
+ /**
7625
+ * Check whether the local branch has commits the remote mirror doesn't
7626
+ * have -- i.e. whether push() would actually move the remote ref forward.
7627
+ *
7628
+ * Exists so callers (submitBranch) can gate pushing on "is there anything
7629
+ * new to send" rather than on "is the working tree dirty": committing
7630
+ * cleans the tree, so a dirty-tree gate around commit+push skips the push
7631
+ * entirely on a retry after a failed push, even though the just-created
7632
+ * commit never reached the remote (see services.ts submitBranch).
7633
+ *
7634
+ * Workspaces are cloned `--single-branch`, so the remote-tracking ref for
7635
+ * any branch other than the one cloned never exists locally -- same
7636
+ * constraint pullBaseInner works around. This fetches the specific branch
7637
+ * directly (bypassing the configured single-branch refspec, exactly like
7638
+ * pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
7639
+ * the fetch that populated it, rather than trusting `<remote>/<branch>`:
7640
+ * FETCH_HEAD is a shared mutable file that any other fetch in this clone
7641
+ * can repoint before it's read.
7642
+ *
7643
+ * A branch that has never been pushed has no ref on the remote at all --
7644
+ * `git fetch` then fails ("couldn't find remote ref"), which this treats
7645
+ * as "ahead" (needs pushing) rather than an error.
7646
+ */
7647
+ async hasUnpushedCommits(branch) {
7648
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
7649
+ const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
7650
+ let fetchedTip;
7651
+ try {
7652
+ await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
7653
+ fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
7654
+ } catch {
7655
+ return true;
7656
+ }
7657
+ if (fetchedTip === localSha)
7658
+ return false;
7659
+ const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
7660
+ return aheadCount !== "0";
7661
+ }
7303
7662
  async ensureAuthor(author) {
7304
7663
  const config = await this.git.listConfig();
7305
7664
  const isManaged = config.all["canopycms.managed"] === "true";
@@ -7346,14 +7705,6 @@ var init_generate_ai_content = __esm({
7346
7705
  const status = await this.status();
7347
7706
  return status.files.map((f) => f.path);
7348
7707
  }
7349
- /**
7350
- * Force push (use with caution - for PR updates only)
7351
- * Uses --force-with-lease for safer force pushes
7352
- */
7353
- async forcePush(branch) {
7354
- const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
7355
- await this.git.raw(["push", "--force-with-lease", "--end-of-options", this.remote, target]);
7356
- }
7357
7708
  /**
7358
7709
  * Get remote URL for current repo
7359
7710
  */
@@ -7407,8 +7758,8 @@ var init_generate_ai_content = __esm({
7407
7758
  } catch {
7408
7759
  }
7409
7760
  for (const [filePath, content] of Object.entries(initialFiles)) {
7410
- const absolutePath = path14.join(this.repoPath, filePath);
7411
- await fs14.mkdir(path14.dirname(absolutePath), { recursive: true });
7761
+ const absolutePath = path15.join(this.repoPath, filePath);
7762
+ await fs14.mkdir(path15.dirname(absolutePath), { recursive: true });
7412
7763
  await fs14.writeFile(absolutePath, content, "utf-8");
7413
7764
  await this.git.add(filePath);
7414
7765
  }
@@ -7436,7 +7787,7 @@ var init_generate_ai_content = __esm({
7436
7787
  branchName: options.branchName,
7437
7788
  mode: options.mode
7438
7789
  });
7439
- releaseLock = await acquireProvisioningLock(path15.dirname(options.branchRoot), `.${path15.basename(options.branchRoot)}.init.lock`);
7790
+ releaseLock = await acquireProvisioningLock(path16.dirname(options.branchRoot), `.${path16.basename(options.branchRoot)}.init.lock`);
7440
7791
  await GitManager.initializeWorkspace({
7441
7792
  workspacePath: options.branchRoot,
7442
7793
  branchName: options.branchName,
@@ -7477,7 +7828,7 @@ var init_generate_ai_content = __esm({
7477
7828
  const baseBranch = await resolveBaseBranch({
7478
7829
  defaultBaseBranch: this.config.defaultBaseBranch,
7479
7830
  mode,
7480
- detectFrom: this.config.sourceRoot ? path15.resolve(process.cwd(), this.config.sourceRoot) : void 0
7831
+ detectFrom: this.config.sourceRoot ? path16.resolve(process.cwd(), this.config.sourceRoot) : void 0
7481
7832
  });
7482
7833
  await this.ensureGitWorkspace({
7483
7834
  branchRoot,
@@ -7535,29 +7886,38 @@ var init_generate_ai_content = __esm({
7535
7886
  }
7536
7887
  };
7537
7888
  parseTypedFilename = (filename, entryTypes) => {
7889
+ if (filename.startsWith("."))
7890
+ return null;
7538
7891
  const lastDot = filename.lastIndexOf(".");
7539
7892
  if (lastDot === -1)
7540
7893
  return null;
7541
7894
  const nameWithoutExt = filename.slice(0, lastDot);
7542
7895
  const parts = nameWithoutExt.split(".");
7543
- if (parts.length >= 3) {
7544
- const potentialType = parts[0];
7545
- const matchingType = entryTypes.find((e) => e.name === potentialType);
7546
- if (matchingType) {
7547
- const id = parts[parts.length - 1];
7548
- if (!isValidId(id))
7549
- return null;
7550
- const slug = parts.slice(1, -1).join(".").toLowerCase();
7551
- return {
7552
- type: potentialType,
7553
- slug,
7554
- id
7555
- };
7556
- }
7896
+ if (parts.length < 3)
7897
+ return null;
7898
+ const potentialType = parts[0];
7899
+ if (entryTypes && !entryTypes.some((e) => e.name === potentialType)) {
7900
+ return null;
7557
7901
  }
7558
- return null;
7902
+ const id = parts[parts.length - 1];
7903
+ if (!isValidId(id))
7904
+ return null;
7905
+ const slug = parts.slice(1, -1).join(".").toLowerCase();
7906
+ return {
7907
+ type: potentialType,
7908
+ slug,
7909
+ id
7910
+ };
7911
+ };
7912
+ looksLikeMalformedEntry = (filename, entryTypes) => {
7913
+ if (filename.startsWith(".") || filename.startsWith("_"))
7914
+ return false;
7915
+ const segments = filename.split(".");
7916
+ if (segments.length >= 4)
7917
+ return true;
7918
+ return segments.length === 3 && entryTypes.some((e) => e.name === segments[0]);
7559
7919
  };
7560
- listCollectionEntries = async (root, collection) => {
7920
+ listCollectionEntries = async (root, collection, onSkip) => {
7561
7921
  if (collection.type !== "collection" || !collection.entries) {
7562
7922
  return [];
7563
7923
  }
@@ -7585,11 +7945,14 @@ var init_generate_ai_content = __esm({
7585
7945
  }
7586
7946
  const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
7587
7947
  const entries = await Promise.all(files.map(async (file) => {
7588
- const absolutePath = path16.join(collectionRoot, file.name);
7948
+ const absolutePath = path17.join(collectionRoot, file.name);
7589
7949
  const relativePath = normalizePath(root, absolutePath);
7590
7950
  const parsed = parseTypedFilename(file.name, entryTypes);
7591
7951
  if (!parsed) {
7592
7952
  log7.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
7953
+ if (looksLikeMalformedEntry(file.name, entryTypes)) {
7954
+ onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
7955
+ }
7593
7956
  return null;
7594
7957
  }
7595
7958
  const { type: entryTypeName, slug, id: contentId } = parsed;
@@ -7619,7 +7982,7 @@ var init_generate_ai_content = __esm({
7619
7982
  init_config22();
7620
7983
  MAX_INPUT_PIXELS = 4096 * 4096;
7621
7984
  KNOWN_FIELD_TYPES = new Set(fieldTypes2);
7622
- STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "rich-text", "markdown", "mdx", "code"]);
7985
+ STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "markdown", "mdx", "code"]);
7623
7986
  selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
7624
7987
  jiti = createJiti2(import.meta.url);
7625
7988
  }
@@ -7627,9 +7990,9 @@ var init_generate_ai_content = __esm({
7627
7990
 
7628
7991
  // dist/utils/atomic-write.js
7629
7992
  import fs17 from "node:fs/promises";
7630
- import path19 from "node:path";
7993
+ import path20 from "node:path";
7631
7994
  async function atomicWriteFile3(filePath, content) {
7632
- const dir = path19.dirname(filePath);
7995
+ const dir = path20.dirname(filePath);
7633
7996
  await fs17.mkdir(dir, { recursive: true });
7634
7997
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
7635
7998
  await fs17.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
@@ -7757,10 +8120,10 @@ var init_debug2 = __esm({
7757
8120
 
7758
8121
  // dist/resource-generation.js
7759
8122
  import fs18 from "node:fs/promises";
7760
- import path20 from "node:path";
8123
+ import path21 from "node:path";
7761
8124
  import { randomUUID as randomUUID3 } from "node:crypto";
7762
8125
  function resourceGenerationPath2(root, resource) {
7763
- return path20.resolve(root, META_DIR2, `${resource}.generation`);
8126
+ return path21.resolve(root, META_DIR3, `${resource}.generation`);
7764
8127
  }
7765
8128
  async function bumpResourceGeneration2(root, resource, options) {
7766
8129
  const token = randomUUID3();
@@ -7792,7 +8155,7 @@ async function readResourceGeneration2(root, resource) {
7792
8155
  function isGenerationCurrent2(snapshotToken, read) {
7793
8156
  return read.ok && read.token === snapshotToken;
7794
8157
  }
7795
- var log8, META_DIR2;
8158
+ var log8, META_DIR3;
7796
8159
  var init_resource_generation = __esm({
7797
8160
  "dist/resource-generation.js"() {
7798
8161
  "use strict";
@@ -7800,15 +8163,15 @@ var init_resource_generation = __esm({
7800
8163
  init_error();
7801
8164
  init_debug2();
7802
8165
  log8 = createDebugLogger3({ prefix: "ResourceGeneration" });
7803
- META_DIR2 = ".canopy-meta";
8166
+ META_DIR3 = ".canopy-meta";
7804
8167
  }
7805
8168
  });
7806
8169
 
7807
8170
  // dist/content-index-registry.js
7808
- import path21 from "node:path";
8171
+ import path23 from "node:path";
7809
8172
  function invalidateContentIndexesForRoot2(root) {
7810
- const rootKey = path21.resolve(root);
7811
- const prefix = rootKey + path21.sep;
8173
+ const rootKey = path23.resolve(root);
8174
+ const prefix = rootKey + path23.sep;
7812
8175
  for (const [storeRoot, refs] of registry2) {
7813
8176
  if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
7814
8177
  continue;
@@ -7848,7 +8211,7 @@ function canopyLogError2(...args) {
7848
8211
  active3.error(...args);
7849
8212
  }
7850
8213
  var active3;
7851
- var init_logger2 = __esm({
8214
+ var init_logger3 = __esm({
7852
8215
  "dist/utils/logger.js"() {
7853
8216
  "use strict";
7854
8217
  active3 = console;
@@ -7971,6 +8334,7 @@ var init_content_id_index = __esm({
7971
8334
  "use strict";
7972
8335
  init_id();
7973
8336
  init_error();
8337
+ init_logger3();
7974
8338
  }
7975
8339
  });
7976
8340
 
@@ -8054,7 +8418,7 @@ var init_meta_loader = __esm({
8054
8418
  "dist/schema/meta-loader.js"() {
8055
8419
  "use strict";
8056
8420
  init_error();
8057
- init_logger2();
8421
+ init_logger3();
8058
8422
  init_content_id_index();
8059
8423
  entryTypeMetaSchema2 = z8.object({
8060
8424
  name: z8.string().min(1),
@@ -8120,7 +8484,6 @@ var init_types5 = __esm({
8120
8484
  "number",
8121
8485
  "boolean",
8122
8486
  "datetime",
8123
- "rich-text",
8124
8487
  "markdown",
8125
8488
  "mdx",
8126
8489
  "code"
@@ -8391,12 +8754,21 @@ var init_config4 = __esm({
8391
8754
  }
8392
8755
  });
8393
8756
 
8757
+ // dist/operating-mode/mode-env.js
8758
+ var init_mode_env3 = __esm({
8759
+ "dist/operating-mode/mode-env.js"() {
8760
+ "use strict";
8761
+ init_logger3();
8762
+ }
8763
+ });
8764
+
8394
8765
  // dist/config/validation.js
8395
8766
  var init_validation4 = __esm({
8396
8767
  "dist/config/validation.js"() {
8397
8768
  "use strict";
8398
8769
  init_config4();
8399
8770
  init_flatten3();
8771
+ init_mode_env3();
8400
8772
  }
8401
8773
  });
8402
8774
 
@@ -8459,12 +8831,12 @@ var init_content_index_generation = __esm({
8459
8831
 
8460
8832
  // dist/sync-core.js
8461
8833
  import fs20 from "node:fs/promises";
8462
- import path23 from "node:path";
8834
+ import path24 from "node:path";
8463
8835
  import { simpleGit as simpleGit4 } from "simple-git";
8464
8836
  function assertWithinDir(resolved, parent, label) {
8465
- const normalizedResolved = path23.resolve(resolved);
8466
- const normalizedParent = path23.resolve(parent);
8467
- if (!normalizedResolved.startsWith(normalizedParent + path23.sep) && normalizedResolved !== normalizedParent) {
8837
+ const normalizedResolved = path24.resolve(resolved);
8838
+ const normalizedParent = path24.resolve(parent);
8839
+ if (!normalizedResolved.startsWith(normalizedParent + path24.sep) && normalizedResolved !== normalizedParent) {
8468
8840
  throw new Error(`${label} escapes the expected directory: ${resolved}`);
8469
8841
  }
8470
8842
  }
@@ -8503,7 +8875,7 @@ async function listFilesRecursive(dir, prefix = "") {
8503
8875
  continue;
8504
8876
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
8505
8877
  if (entry.isDirectory()) {
8506
- results.push(...await listFilesRecursive(path23.join(dir, entry.name), rel));
8878
+ results.push(...await listFilesRecursive(path24.join(dir, entry.name), rel));
8507
8879
  } else {
8508
8880
  results.push(rel);
8509
8881
  }
@@ -8518,8 +8890,8 @@ async function copyDir(src, dest) {
8518
8890
  continue;
8519
8891
  if (entry.isSymbolicLink())
8520
8892
  continue;
8521
- const srcPath = path23.join(src, entry.name);
8522
- const destPath = path23.join(dest, entry.name);
8893
+ const srcPath = path24.join(src, entry.name);
8894
+ const destPath = path24.join(dest, entry.name);
8523
8895
  if (entry.isDirectory()) {
8524
8896
  await copyDir(srcPath, destPath);
8525
8897
  } else {
@@ -8532,7 +8904,7 @@ async function pushContentToWorkspace(options) {
8532
8904
  if (!await filePathExists(srcContentDir)) {
8533
8905
  return { fileCount: 0 };
8534
8906
  }
8535
- const wsContentDir = path23.join(branchPath, contentRoot);
8907
+ const wsContentDir = path24.join(branchPath, contentRoot);
8536
8908
  assertWithinDir(wsContentDir, branchPath, "content-root");
8537
8909
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
8538
8910
  try {
@@ -8588,11 +8960,11 @@ var init_provisioning_lock = __esm({
8588
8960
 
8589
8961
  // dist/git-manager.js
8590
8962
  import fs21 from "node:fs/promises";
8591
- import path24 from "node:path";
8963
+ import path25 from "node:path";
8592
8964
  import { simpleGit as simpleGit6 } from "simple-git";
8593
8965
  async function ensureGitExcludePattern2(repoPath, pattern) {
8594
- const excludePath = path24.join(repoPath, ".git", "info", "exclude");
8595
- await fs21.mkdir(path24.dirname(excludePath), { recursive: true });
8966
+ const excludePath = path25.join(repoPath, ".git", "info", "exclude");
8967
+ await fs21.mkdir(path25.dirname(excludePath), { recursive: true });
8596
8968
  let content = "";
8597
8969
  try {
8598
8970
  content = await fs21.readFile(excludePath, "utf-8");
@@ -8713,7 +9085,7 @@ var warned3, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
8713
9085
  var init_deployment_name2 = __esm({
8714
9086
  "dist/operating-mode/deployment-name.js"() {
8715
9087
  "use strict";
8716
- init_logger2();
9088
+ init_logger3();
8717
9089
  warned3 = false;
8718
9090
  VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8719
9091
  isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
@@ -8764,7 +9136,7 @@ var init_config5 = __esm({
8764
9136
  });
8765
9137
 
8766
9138
  // dist/operating-mode/client-unsafe-strategy.js
8767
- import path25 from "node:path";
9139
+ import path26 from "node:path";
8768
9140
  function operatingStrategy3(mode) {
8769
9141
  const cached = strategyCache3.get(mode);
8770
9142
  if (cached)
@@ -8799,32 +9171,32 @@ var init_client_unsafe_strategy2 = __esm({
8799
9171
  // - shouldCommit(), shouldPush()
8800
9172
  // Add client-unsafe methods (use Node.js APIs)
8801
9173
  getWorkspaceRoot(_sourceRoot) {
8802
- return path25.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
9174
+ return path26.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
8803
9175
  }
8804
9176
  getContentRoot(contentRoot, sourceRoot) {
8805
- return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
9177
+ return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
8806
9178
  }
8807
9179
  getContentBranchesRoot(sourceRoot) {
8808
- return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9180
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
8809
9181
  }
8810
9182
  getContentBranchRoot(branchName, sourceRoot) {
8811
- return path25.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9183
+ return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
8812
9184
  }
8813
9185
  getGitExcludePattern() {
8814
9186
  return ".canopy-meta/";
8815
9187
  }
8816
9188
  getPermissionsFilePath(root) {
8817
- return path25.join(root, this.getPermissionsFileName());
9189
+ return path26.join(root, this.getPermissionsFileName());
8818
9190
  }
8819
9191
  getGroupsFilePath(root) {
8820
- return path25.join(root, this.getGroupsFileName());
9192
+ return path26.join(root, this.getGroupsFileName());
8821
9193
  }
8822
9194
  getRemoteUrlConfig() {
8823
9195
  return {
8824
9196
  shouldAutoInitLocal: false,
8825
9197
  defaultRemotePath: "",
8826
9198
  envVarName: "CANOPYCMS_REMOTE_URL",
8827
- autoDetectRemotePath: path25.join(this.getWorkspaceRoot(), "remote.git")
9199
+ autoDetectRemotePath: path26.join(this.getWorkspaceRoot(), "remote.git")
8828
9200
  };
8829
9201
  }
8830
9202
  requiresExistingRepo() {
@@ -8836,7 +9208,7 @@ var init_client_unsafe_strategy2 = __esm({
8836
9208
  return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
8837
9209
  }
8838
9210
  getSettingsRoot(sourceRoot) {
8839
- return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
9211
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
8840
9212
  }
8841
9213
  usesSeparateSettingsBranch() {
8842
9214
  return true;
@@ -8853,25 +9225,25 @@ var init_client_unsafe_strategy2 = __esm({
8853
9225
  DevStrategy3 = class extends DevClientSafeStrategy3 {
8854
9226
  // Inherits client-safe methods from DevClientSafeStrategy
8855
9227
  getWorkspaceRoot(sourceRoot) {
8856
- return path25.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
9228
+ return path26.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
8857
9229
  }
8858
9230
  getContentRoot(contentRoot, sourceRoot) {
8859
- return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
9231
+ return path26.resolve(sourceRoot ?? process.cwd(), contentRoot);
8860
9232
  }
8861
9233
  getContentBranchesRoot(sourceRoot) {
8862
- return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
9234
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
8863
9235
  }
8864
9236
  getContentBranchRoot(branchName, sourceRoot) {
8865
- return path25.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
9237
+ return path26.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
8866
9238
  }
8867
9239
  getGitExcludePattern() {
8868
9240
  return ".canopy-meta/";
8869
9241
  }
8870
9242
  getPermissionsFilePath(root) {
8871
- return path25.join(root, this.getPermissionsFileName());
9243
+ return path26.join(root, this.getPermissionsFileName());
8872
9244
  }
8873
9245
  getGroupsFilePath(root) {
8874
- return path25.join(root, this.getGroupsFileName());
9246
+ return path26.join(root, this.getGroupsFileName());
8875
9247
  }
8876
9248
  getRemoteUrlConfig() {
8877
9249
  return {
@@ -8889,7 +9261,7 @@ var init_client_unsafe_strategy2 = __esm({
8889
9261
  return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
8890
9262
  }
8891
9263
  getSettingsRoot(sourceRoot) {
8892
- return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
9264
+ return path26.join(this.getWorkspaceRoot(sourceRoot), "settings");
8893
9265
  }
8894
9266
  usesSeparateSettingsBranch() {
8895
9267
  return true;
@@ -8911,6 +9283,7 @@ var init_operating_mode2 = __esm({
8911
9283
  init_client_safe_strategy2();
8912
9284
  init_client_unsafe_strategy2();
8913
9285
  init_deployment_name2();
9286
+ init_mode_env3();
8914
9287
  }
8915
9288
  });
8916
9289
 
@@ -8921,7 +9294,7 @@ __export(sync_exports, {
8921
9294
  sync: () => sync
8922
9295
  });
8923
9296
  import fs24 from "node:fs/promises";
8924
- import path26 from "node:path";
9297
+ import path27 from "node:path";
8925
9298
  import { simpleGit as simpleGit7 } from "simple-git";
8926
9299
  import * as p2 from "@clack/prompts";
8927
9300
  async function detectCurrentBranch(projectDir) {
@@ -8962,7 +9335,7 @@ async function selectBranch(options, branchesDir) {
8962
9335
  branchName = result;
8963
9336
  }
8964
9337
  }
8965
- const branchPath = path26.join(branchesDir, branchName);
9338
+ const branchPath = path27.join(branchesDir, branchName);
8966
9339
  assertWithinDir(branchPath, branchesDir, "--branch");
8967
9340
  if (!branches.includes(branchName)) {
8968
9341
  if (options.autoCreate) {
@@ -8983,8 +9356,8 @@ async function selectBranch(options, branchesDir) {
8983
9356
  async function syncPush(options) {
8984
9357
  const { projectDir } = options;
8985
9358
  const contentRoot = options.contentRoot || "content";
8986
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
8987
- const srcContentDir = path26.join(projectDir, contentRoot);
9359
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9360
+ const srcContentDir = path27.join(projectDir, contentRoot);
8988
9361
  assertWithinDir(srcContentDir, projectDir, "--content-root");
8989
9362
  if (!await filePathExists(srcContentDir)) {
8990
9363
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -8992,7 +9365,7 @@ async function syncPush(options) {
8992
9365
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
8993
9366
  if (!branchName)
8994
9367
  return { fileCount: 0 };
8995
- const branchPath = path26.join(branchesDir, branchName);
9368
+ const branchPath = path27.join(branchesDir, branchName);
8996
9369
  assertWithinDir(branchPath, branchesDir, "--branch");
8997
9370
  const wsGit = simpleGit7({ baseDir: branchPath });
8998
9371
  const status = await wsGit.status();
@@ -9050,19 +9423,19 @@ async function syncPush(options) {
9050
9423
  async function syncPull(options) {
9051
9424
  const { projectDir } = options;
9052
9425
  const contentRoot = options.contentRoot || "content";
9053
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9426
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9054
9427
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
9055
9428
  if (!branchName)
9056
9429
  return { fileCount: 0 };
9057
- const branchPath = path26.join(branchesDir, branchName);
9430
+ const branchPath = path27.join(branchesDir, branchName);
9058
9431
  assertWithinDir(branchPath, branchesDir, "--branch");
9059
- const branchContentDir = path26.join(branchPath, contentRoot);
9432
+ const branchContentDir = path27.join(branchPath, contentRoot);
9060
9433
  assertWithinDir(branchContentDir, branchPath, "--content-root");
9061
9434
  if (!await filePathExists(branchContentDir)) {
9062
9435
  throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
9063
9436
  }
9064
9437
  p2.log.step(`Pulling content from branch: ${branchName}`);
9065
- const destContentDir = path26.join(projectDir, contentRoot);
9438
+ const destContentDir = path27.join(projectDir, contentRoot);
9066
9439
  assertWithinDir(destContentDir, projectDir, "--content-root");
9067
9440
  const sourceGit = simpleGit7({ baseDir: projectDir });
9068
9441
  const status = await sourceGit.status();
@@ -9138,8 +9511,8 @@ async function syncPull(options) {
9138
9511
  async function syncBoth(options) {
9139
9512
  const { projectDir } = options;
9140
9513
  const contentRoot = options.contentRoot || "content";
9141
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9142
- const srcContentDir = path26.join(projectDir, contentRoot);
9514
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9515
+ const srcContentDir = path27.join(projectDir, contentRoot);
9143
9516
  assertWithinDir(srcContentDir, projectDir, "--content-root");
9144
9517
  if (!await filePathExists(srcContentDir)) {
9145
9518
  throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
@@ -9147,7 +9520,7 @@ async function syncBoth(options) {
9147
9520
  const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
9148
9521
  if (!branchName)
9149
9522
  return { pushed: 0, pulled: 0 };
9150
- const branchPath = path26.join(branchesDir, branchName);
9523
+ const branchPath = path27.join(branchesDir, branchName);
9151
9524
  assertWithinDir(branchPath, branchesDir, "--branch");
9152
9525
  const wsGit = simpleGit7({ baseDir: branchPath });
9153
9526
  const status = await wsGit.status();
@@ -9182,7 +9555,7 @@ async function syncBoth(options) {
9182
9555
  try {
9183
9556
  const incomingBranch = `sync-incoming-${Date.now()}`;
9184
9557
  await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
9185
- const wsContentDir = path26.join(branchPath, contentRoot);
9558
+ const wsContentDir = path27.join(branchPath, contentRoot);
9186
9559
  assertWithinDir(wsContentDir, branchPath, "--content-root");
9187
9560
  const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
9188
9561
  try {
@@ -9246,11 +9619,11 @@ async function syncBoth(options) {
9246
9619
  }
9247
9620
  async function syncAbort(options) {
9248
9621
  const { projectDir } = options;
9249
- const branchesDir = path26.join(projectDir, ".canopy-dev", "content-branches");
9622
+ const branchesDir = path27.join(projectDir, ".canopy-dev", "content-branches");
9250
9623
  const branchName = await selectBranch({ ...options, projectDir }, branchesDir);
9251
9624
  if (!branchName)
9252
9625
  return;
9253
- const branchPath = path26.join(branchesDir, branchName);
9626
+ const branchPath = path27.join(branchesDir, branchName);
9254
9627
  assertWithinDir(branchPath, branchesDir, "--branch");
9255
9628
  const wsGit = simpleGit7({ baseDir: branchPath });
9256
9629
  const status = await wsGit.status();
@@ -9379,7 +9752,7 @@ var init_async_mutex = __esm({
9379
9752
 
9380
9753
  // dist/utils/occ-json-write.js
9381
9754
  import fs25 from "node:fs/promises";
9382
- import path27 from "node:path";
9755
+ import path28 from "node:path";
9383
9756
  import { randomUUID as randomUUID4 } from "node:crypto";
9384
9757
  import lockfile4 from "proper-lockfile";
9385
9758
  async function writeOccJsonFile2(filePath, payload, options) {
@@ -9388,7 +9761,7 @@ async function writeOccJsonFile2(filePath, payload, options) {
9388
9761
  const writeId = randomUUID4();
9389
9762
  const data = { ...payload, version: newVersion, writeId };
9390
9763
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
9391
- const dir = path27.dirname(filePath);
9764
+ const dir = path28.dirname(filePath);
9392
9765
  await fs25.mkdir(dir, { recursive: true });
9393
9766
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
9394
9767
  await fs25.writeFile(tempPath, content, "utf-8");
@@ -9453,7 +9826,7 @@ async function withOccRetry2(operation, options) {
9453
9826
  throw new Error("Unreachable");
9454
9827
  }
9455
9828
  async function withOccFileLock2(filePath, fn) {
9456
- const dir = path27.dirname(filePath);
9829
+ const dir = path28.dirname(filePath);
9457
9830
  await fs25.mkdir(dir, { recursive: true });
9458
9831
  const MAX_ATTEMPTS = 14;
9459
9832
  let release = null;
@@ -9514,7 +9887,7 @@ var init_occ_json_write = __esm({
9514
9887
 
9515
9888
  // dist/branch-registry.js
9516
9889
  import fs26 from "node:fs/promises";
9517
- import path28 from "node:path";
9890
+ import path29 from "node:path";
9518
9891
  var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
9519
9892
  var init_branch_registry = __esm({
9520
9893
  "dist/branch-registry.js"() {
@@ -9522,7 +9895,7 @@ var init_branch_registry = __esm({
9522
9895
  init_branch_metadata();
9523
9896
  init_error();
9524
9897
  init_debug2();
9525
- init_logger2();
9898
+ init_logger3();
9526
9899
  init_resource_generation();
9527
9900
  log12 = createDebugLogger3({ prefix: "BranchRegistry" });
9528
9901
  REGISTRY_FILE2 = "branches.json";
@@ -9535,10 +9908,10 @@ var init_branch_registry = __esm({
9535
9908
  constructor(root) {
9536
9909
  this.regenInFlight = null;
9537
9910
  this.lastForcedRefreshMs = 0;
9538
- this.root = path28.resolve(root);
9539
- this.registryPath = path28.join(this.root, REGISTRY_FILE2);
9540
- this.stalePath = path28.join(this.root, REGISTRY_STALE_FILE2);
9541
- this.tempPath = path28.join(this.root, REGISTRY_TEMP_FILE2);
9911
+ this.root = path29.resolve(root);
9912
+ this.registryPath = path29.join(this.root, REGISTRY_FILE2);
9913
+ this.stalePath = path29.join(this.root, REGISTRY_STALE_FILE2);
9914
+ this.tempPath = path29.join(this.root, REGISTRY_TEMP_FILE2);
9542
9915
  }
9543
9916
  /**
9544
9917
  * Returns all branches. Uses the cached snapshot if its embedded generation
@@ -9671,7 +10044,7 @@ var init_branch_registry = __esm({
9671
10044
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
9672
10045
  continue;
9673
10046
  }
9674
- const branchRoot = path28.join(this.root, entry.name);
10047
+ const branchRoot = path29.join(this.root, entry.name);
9675
10048
  let meta;
9676
10049
  try {
9677
10050
  meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
@@ -9701,7 +10074,7 @@ var init_branch_registry = __esm({
9701
10074
 
9702
10075
  // dist/branch-metadata.js
9703
10076
  import fs27 from "node:fs/promises";
9704
- import path29 from "node:path";
10077
+ import path30 from "node:path";
9705
10078
  var BRANCH_META_DIR2, BRANCH_META_FILE2, CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataCorruptError2, BranchMetadataFileManager2;
9706
10079
  var init_branch_metadata = __esm({
9707
10080
  "dist/branch-metadata.js"() {
@@ -9730,8 +10103,8 @@ var init_branch_metadata = __esm({
9730
10103
  };
9731
10104
  BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
9732
10105
  constructor(branchRoot, baseRoot, options) {
9733
- this.branchRoot = path29.resolve(branchRoot);
9734
- this.filePath = path29.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10106
+ this.branchRoot = path30.resolve(branchRoot);
10107
+ this.filePath = path30.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
9735
10108
  this.baseRoot = baseRoot;
9736
10109
  this.settleMs = options?.settleMs;
9737
10110
  }
@@ -9740,8 +10113,8 @@ var init_branch_metadata = __esm({
9740
10113
  * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
9741
10114
  */
9742
10115
  static async loadOnly(branchRoot) {
9743
- const resolvedRoot = path29.resolve(branchRoot);
9744
- const filePath = path29.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
10116
+ const resolvedRoot = path30.resolve(branchRoot);
10117
+ const filePath = path30.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
9745
10118
  try {
9746
10119
  const raw = await fs27.readFile(filePath, "utf8");
9747
10120
  return JSON.parse(raw);
@@ -9902,7 +10275,7 @@ __export(migrate_exports, {
9902
10275
  slugifyName: () => slugifyName
9903
10276
  });
9904
10277
  import fs28 from "node:fs/promises";
9905
- import path30 from "node:path";
10278
+ import path31 from "node:path";
9906
10279
  import * as p3 from "@clack/prompts";
9907
10280
  function slugifyName(name) {
9908
10281
  const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -9936,7 +10309,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
9936
10309
  if (entry.name.startsWith("."))
9937
10310
  continue;
9938
10311
  if (entry.isDirectory()) {
9939
- const subPath = path30.join(dirPath, entry.name);
10312
+ const subPath = path31.join(dirPath, entry.name);
9940
10313
  const sub = await planDirectory(subPath, false, ctx);
9941
10314
  if (!sub.hasContent)
9942
10315
  continue;
@@ -9944,10 +10317,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
9944
10317
  ops.push(...sub.ops);
9945
10318
  if (!extractIdFromFilename2(entry.name)) {
9946
10319
  const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
9947
- ops.push({ kind: "rename", from: subPath, to: path30.join(dirPath, newName) });
10320
+ ops.push({ kind: "rename", from: subPath, to: path31.join(dirPath, newName) });
9948
10321
  }
9949
10322
  } else if (entry.isFile()) {
9950
- const ext = path30.extname(entry.name).slice(1);
10323
+ const ext = path31.extname(entry.name).slice(1);
9951
10324
  const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
9952
10325
  if (!matchesFormat)
9953
10326
  continue;
@@ -9958,17 +10331,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
9958
10331
  const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
9959
10332
  ops.push({
9960
10333
  kind: "rename",
9961
- from: path30.join(dirPath, entry.name),
9962
- to: path30.join(dirPath, newName)
10334
+ from: path31.join(dirPath, entry.name),
10335
+ to: path31.join(dirPath, newName)
9963
10336
  });
9964
10337
  }
9965
10338
  }
9966
10339
  }
9967
10340
  const needsMeta = isRoot ? hasOwnFiles : hasContent;
9968
- const metaPath = path30.join(dirPath, ".collection.json");
10341
+ const metaPath = path31.join(dirPath, ".collection.json");
9969
10342
  if (needsMeta && !await filePathExists(metaPath)) {
9970
10343
  const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
9971
- const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path30.basename(dirPath))), entries: [entryTypeMeta] };
10344
+ const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path31.basename(dirPath))), entries: [entryTypeMeta] };
9972
10345
  ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
9973
10346
  }
9974
10347
  return { ops, hasContent };
@@ -9977,7 +10350,7 @@ async function migrate(options) {
9977
10350
  const { projectDir } = options;
9978
10351
  const contentRoot = options.contentRoot || "content";
9979
10352
  p3.intro("CanopyCMS migrate");
9980
- const contentDir = path30.join(projectDir, contentRoot);
10353
+ const contentDir = path31.join(projectDir, contentRoot);
9981
10354
  if (!await filePathExists(contentDir)) {
9982
10355
  throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
9983
10356
  }
@@ -10061,7 +10434,7 @@ async function migrate(options) {
10061
10434
  }
10062
10435
  };
10063
10436
  if (isBranchClone) {
10064
- const lockPath = path30.join(projectDir, ".canopy-meta", "schema");
10437
+ const lockPath = path31.join(projectDir, ".canopy-meta", "schema");
10065
10438
  try {
10066
10439
  await withLock2(lockPath, () => withOccFileLock2(lockPath, applyOps));
10067
10440
  } catch (err) {
@@ -10109,7 +10482,7 @@ var init_migrate = __esm({
10109
10482
  this.name = "MigrateError";
10110
10483
  }
10111
10484
  };
10112
- describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path30.relative(baseDir, op.from)} \u2192 ${path30.basename(op.to)}` : `create ${path30.relative(baseDir, op.filePath)}`;
10485
+ describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path31.relative(baseDir, op.from)} \u2192 ${path31.basename(op.to)}` : `create ${path31.relative(baseDir, op.filePath)}`;
10113
10486
  }
10114
10487
  });
10115
10488